mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 16:01:18 +02:00
Rename workspace
This commit is contained in:
@@ -506,6 +506,16 @@
|
|||||||
},
|
},
|
||||||
"query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n "
|
"query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n "
|
||||||
},
|
},
|
||||||
|
"e0f41023d877d94b7609ce910a71bd89c4827a558654b8ae14d85e6ba86990cf": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE workspaces SET (name, updated_at) =\n (?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
|
||||||
|
},
|
||||||
"e3ade0a69348d512e47e964bded9d7d890b92fdc1e01c6c22fa5e91f943639f2": {
|
"e3ade0a69348d512e47e964bded9d7d890b92fdc1e01c6c22fa5e91f943639f2": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|||||||
@@ -362,6 +362,21 @@ async fn duplicate_request(
|
|||||||
emit_and_return(&window, "updated_model", request)
|
emit_and_return(&window, "updated_model", request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update_workspace(
|
||||||
|
workspace: models::Workspace,
|
||||||
|
window: Window<Wry>,
|
||||||
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
) -> Result<models::Workspace, String> {
|
||||||
|
let pool = &*db_instance.lock().await;
|
||||||
|
|
||||||
|
let updated_workspace = models::update_workspace(workspace, pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update request");
|
||||||
|
|
||||||
|
emit_and_return(&window, "updated_model", updated_workspace)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn update_request(
|
async fn update_request(
|
||||||
request: models::HttpRequest,
|
request: models::HttpRequest,
|
||||||
@@ -436,6 +451,17 @@ async fn get_request(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_workspace(
|
||||||
|
id: &str,
|
||||||
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
|
) -> Result<models::Workspace, String> {
|
||||||
|
let pool = &*db_instance.lock().await;
|
||||||
|
models::get_workspace(id, pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn responses(
|
async fn responses(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
@@ -546,8 +572,10 @@ fn main() {
|
|||||||
send_ephemeral_request,
|
send_ephemeral_request,
|
||||||
duplicate_request,
|
duplicate_request,
|
||||||
create_request,
|
create_request,
|
||||||
|
get_workspace,
|
||||||
create_workspace,
|
create_workspace,
|
||||||
delete_workspace,
|
delete_workspace,
|
||||||
|
update_workspace,
|
||||||
update_request,
|
update_request,
|
||||||
delete_request,
|
delete_request,
|
||||||
responses,
|
responses,
|
||||||
|
|||||||
@@ -423,6 +423,24 @@ pub async fn update_response_if_id(
|
|||||||
return update_response(response, pool).await;
|
return update_response(response, pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_workspace(
|
||||||
|
workspace: Workspace,
|
||||||
|
pool: &Pool<Sqlite>,
|
||||||
|
) -> Result<Workspace, sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE workspaces SET (name, updated_at) =
|
||||||
|
(?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||||
|
"#,
|
||||||
|
workspace.name,
|
||||||
|
workspace.id,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update workspace");
|
||||||
|
get_workspace(&workspace.id, pool).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_response(
|
pub async fn update_response(
|
||||||
response: HttpResponse,
|
response: HttpResponse,
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
|
|||||||
@@ -45,13 +45,20 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<DialogContext.Provider value={state}>
|
<DialogContext.Provider value={state}>
|
||||||
{children}
|
{children}
|
||||||
{dialogs.map(({ id, render, ...props }) => (
|
{dialogs.map((props: DialogEntry) => (
|
||||||
<Dialog open key={id} onClose={() => actions.hide(id)} {...props}>
|
<DialogInstance key={props.id} {...props} />
|
||||||
{render({ hide: () => actions.hide(id) })}
|
|
||||||
</Dialog>
|
|
||||||
))}
|
))}
|
||||||
</DialogContext.Provider>
|
</DialogContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function DialogInstance({ id, render, ...props }: DialogEntry) {
|
||||||
|
const { actions } = useContext(DialogContext);
|
||||||
|
return (
|
||||||
|
<Dialog open onClose={() => actions.hide(id)} {...props}>
|
||||||
|
{render({ hide: () => actions.hide(id) })}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const useDialog = () => useContext(DialogContext).actions;
|
export const useDialog = () => useContext(DialogContext).actions;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
{activeResponse && (
|
{activeResponse && (
|
||||||
<HStack alignItems="center" className="w-full">
|
<HStack alignItems="center" className="w-full">
|
||||||
<div className="whitespace-nowrap px-3">
|
<div className="whitespace-nowrap px-3">
|
||||||
<StatusTag response={activeResponse} />
|
<StatusTag showReason response={activeResponse} />
|
||||||
{activeResponse.elapsed > 0 && <> • {activeResponse.elapsed}ms</>}
|
{activeResponse.elapsed > 0 && <> • {activeResponse.elapsed}ms</>}
|
||||||
{activeResponse.body.length > 0 && (
|
{activeResponse.body.length > 0 && (
|
||||||
<> • {(activeResponse.body.length / 1000).toFixed(1)} KB</>
|
<> • {(activeResponse.body.length / 1000).toFixed(1)} KB</>
|
||||||
@@ -165,7 +165,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="body">
|
<TabContent value="body">
|
||||||
{!activeResponse.body ? (
|
{!activeResponse.body ? (
|
||||||
<EmptyStateText>No Response</EmptyStateText>
|
<EmptyStateText>Empty Body</EmptyStateText>
|
||||||
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
||||||
<Webview
|
<Webview
|
||||||
body={activeResponse.body}
|
body={activeResponse.body}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { memo, useMemo } from 'react';
|
|||||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||||
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useRoutes } from '../hooks/useRoutes';
|
import { useRoutes } from '../hooks/useRoutes';
|
||||||
|
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import type { DropdownItem } from './core/Dropdown';
|
import type { DropdownItem } from './core/Dropdown';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { Dropdown } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -19,7 +22,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
|||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||||
|
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
|
const prompt = usePrompt();
|
||||||
const routes = useRoutes();
|
const routes = useRoutes();
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(() => {
|
const items: DropdownItem[] = useMemo(() => {
|
||||||
@@ -32,31 +37,59 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const activeWorkspaceItems: DropdownItem[] =
|
||||||
|
workspaces.length <= 1
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...workspaceItems,
|
||||||
|
{
|
||||||
|
type: 'separator',
|
||||||
|
label: activeWorkspace?.name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...workspaceItems,
|
...activeWorkspaceItems,
|
||||||
{
|
{
|
||||||
type: 'separator',
|
label: 'Rename',
|
||||||
label: 'Actions',
|
leftSlot: <Icon icon="pencil" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const name = await prompt({
|
||||||
|
title: 'Rename Workspace',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Enter a new name for <InlineCode>{activeWorkspace?.name}</InlineCode>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
defaultValue: activeWorkspace?.name,
|
||||||
|
});
|
||||||
|
updateWorkspace.mutate({ name });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'New Workspace',
|
label: 'Delete',
|
||||||
leftSlot: <Icon icon="plus" />,
|
|
||||||
onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Delete Workspace',
|
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
onSelect: deleteWorkspace.mutate,
|
onSelect: deleteWorkspace.mutate,
|
||||||
|
variant: 'danger',
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Create Workspace',
|
||||||
|
leftSlot: <Icon icon="plus" />,
|
||||||
|
onSelect: () => createWorkspace.mutate({ name: 'Workspace' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
workspaces,
|
workspaces,
|
||||||
|
deleteWorkspace.mutate,
|
||||||
activeWorkspaceId,
|
activeWorkspaceId,
|
||||||
routes,
|
routes,
|
||||||
createWorkspace,
|
prompt,
|
||||||
confirm,
|
|
||||||
activeWorkspace?.name,
|
activeWorkspace?.name,
|
||||||
deleteWorkspace,
|
updateWorkspace,
|
||||||
|
createWorkspace,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
|
|||||||
children,
|
children,
|
||||||
forDropdown,
|
forDropdown,
|
||||||
color,
|
color,
|
||||||
|
type = 'button',
|
||||||
justify = 'center',
|
justify = 'center',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
...props
|
...props
|
||||||
@@ -68,7 +69,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<button ref={ref} className={classes} {...props}>
|
<button ref={ref} type={type} className={classes} {...props}>
|
||||||
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />}
|
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />}
|
||||||
{children}
|
{children}
|
||||||
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
|
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function Dialog({
|
|||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
{description && <p id={descriptionId}>{description}</p>}
|
{description && <p id={descriptionId}>{description}</p>}
|
||||||
<div className="mt-6">{children}</div>
|
<div className="mt-4">{children}</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import classnames from 'classnames';
|
|||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
|
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||||
import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, {
|
||||||
|
Children,
|
||||||
|
cloneElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { useKeyPressEvent } from 'react-use';
|
import { useKeyPressEvent } from 'react-use';
|
||||||
import { Portal } from '../Portal';
|
import { Portal } from '../Portal';
|
||||||
import { Separator } from './Separator';
|
import { Separator } from './Separator';
|
||||||
@@ -17,6 +25,7 @@ export type DropdownItem =
|
|||||||
| {
|
| {
|
||||||
type?: 'default';
|
type?: 'default';
|
||||||
label: string;
|
label: string;
|
||||||
|
variant?: 'danger';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
@@ -94,6 +103,17 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
|
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Close menu on space bar
|
||||||
|
const handleMenuKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
useKeyPressEvent('Escape', (e) => {
|
useKeyPressEvent('Escape', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onClose();
|
onClose();
|
||||||
@@ -181,6 +201,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
<div tabIndex={-1} aria-hidden className="fixed inset-0" onClick={onClose} />
|
<div tabIndex={-1} aria-hidden className="fixed inset-0" onClick={onClose} />
|
||||||
<motion.div
|
<motion.div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
onKeyDown={handleMenuKeyDown}
|
||||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
role="menu"
|
role="menu"
|
||||||
@@ -268,6 +289,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
|||||||
className,
|
className,
|
||||||
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
|
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
|
||||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||||
|
item.variant === 'danger' && 'text-red-600',
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
PaperPlaneIcon,
|
PaperPlaneIcon,
|
||||||
|
Pencil2Icon,
|
||||||
PlusCircledIcon,
|
PlusCircledIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
QuestionMarkIcon,
|
QuestionMarkIcon,
|
||||||
@@ -65,6 +66,7 @@ const icons = {
|
|||||||
magnifyingGlass: MagnifyingGlassIcon,
|
magnifyingGlass: MagnifyingGlassIcon,
|
||||||
moon: MoonIcon,
|
moon: MoonIcon,
|
||||||
paperPlane: PaperPlaneIcon,
|
paperPlane: PaperPlaneIcon,
|
||||||
|
pencil: Pencil2Icon,
|
||||||
plus: PlusIcon,
|
plus: PlusIcon,
|
||||||
plusCircle: PlusCircledIcon,
|
plusCircle: PlusCircledIcon,
|
||||||
question: QuestionMarkIcon,
|
question: QuestionMarkIcon,
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import classnames from 'classnames';
|
|||||||
import type { HttpResponse } from '../../lib/models';
|
import type { HttpResponse } from '../../lib/models';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: Pick<HttpResponse, 'status' | 'error'>;
|
response: Pick<HttpResponse, 'status' | 'statusReason' | 'error'>;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showReason?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusTag({ response, className }: Props) {
|
export function StatusTag({ response, className, showReason }: Props) {
|
||||||
const { status, error } = response;
|
const { status, error } = response;
|
||||||
const label = error ? 'ERR' : status;
|
const label = error ? 'ERR' : status;
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +23,7 @@ export function StatusTag({ response, className }: Props) {
|
|||||||
status >= 500 && 'text-red-600',
|
status >= 500 && 'text-red-600',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label} {showReason && response.statusReason && response.statusReason}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,17 +73,17 @@ export function Tabs({
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
tabListClassName,
|
tabListClassName,
|
||||||
'flex items-center overflow-x-auto hide-scrollbars mt-1 mb-2',
|
'flex items-center overflow-x-auto overflow-y-visible hide-scrollbars mt-1 mb-2',
|
||||||
// Give space for button focus states within overflow boundary.
|
// Give space for button focus states within overflow boundary.
|
||||||
'px-2 -mx-2',
|
'-mx-5 pl-3 py-1',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HStack space={1} className="flex-shrink-0">
|
<HStack space={2} className="flex-shrink-0">
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => {
|
||||||
const isActive = t.value === value;
|
const isActive = t.value === value;
|
||||||
const btnClassName = classnames(
|
const btnClassName = classnames(
|
||||||
isActive ? '' : 'text-gray-600 hover:text-gray-800',
|
isActive ? '' : 'text-gray-600 hover:text-gray-800',
|
||||||
'!px-0 mr-4 ml-[1px]',
|
'!px-2 ml-[1px]',
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('options' in t) {
|
if ('options' in t) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from '../components/core/Button';
|
|||||||
import { HStack } from '../components/core/Stacks';
|
import { HStack } from '../components/core/Stacks';
|
||||||
|
|
||||||
export interface ConfirmProps {
|
export interface ConfirmProps {
|
||||||
hide: () => void;
|
onHide: () => void;
|
||||||
onResult: (result: boolean) => void;
|
onResult: (result: boolean) => void;
|
||||||
variant?: 'delete' | 'confirm';
|
variant?: 'delete' | 'confirm';
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> =
|
|||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Confirm({ hide, onResult, variant = 'confirm' }: ConfirmProps) {
|
export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) {
|
||||||
const focusRef = (el: HTMLButtonElement | null) => {
|
const focusRef = (el: HTMLButtonElement | null) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
el?.focus();
|
el?.focus();
|
||||||
@@ -27,16 +27,16 @@ export function Confirm({ hide, onResult, variant = 'confirm' }: ConfirmProps) {
|
|||||||
|
|
||||||
const handleHide = () => {
|
const handleHide = () => {
|
||||||
onResult(false);
|
onResult(false);
|
||||||
hide();
|
onHide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuccess = () => {
|
const handleSuccess = () => {
|
||||||
onResult(true);
|
onResult(true);
|
||||||
hide();
|
onHide();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack space={2} justifyContent="end">
|
<HStack space={2} justifyContent="end" className="mt-6">
|
||||||
<Button className="focus" color="gray" onClick={handleHide}>
|
<Button className="focus" color="gray" onClick={handleHide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
48
src-web/hooks/Prompt.tsx
Normal file
48
src-web/hooks/Prompt.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { FormEvent } from 'react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Button } from '../components/core/Button';
|
||||||
|
import type { InputProps } from '../components/core/Input';
|
||||||
|
import { Input } from '../components/core/Input';
|
||||||
|
import { HStack, VStack } from '../components/core/Stacks';
|
||||||
|
|
||||||
|
export interface PromptProps {
|
||||||
|
onHide: () => void;
|
||||||
|
onResult: (value: string) => void;
|
||||||
|
label: InputProps['label'];
|
||||||
|
name: InputProps['name'];
|
||||||
|
defaultValue: InputProps['defaultValue'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Prompt({ onHide, label, name, defaultValue, onResult }: PromptProps) {
|
||||||
|
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onHide();
|
||||||
|
onResult(value);
|
||||||
|
},
|
||||||
|
[onHide, onResult, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<VStack space={6}>
|
||||||
|
<Input
|
||||||
|
hideLabel
|
||||||
|
label={label}
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
<HStack space={2} justifyContent="end">
|
||||||
|
<Button className="focus" color="gray" onClick={onHide}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="focus" color="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ export function useConfirm() {
|
|||||||
description,
|
description,
|
||||||
hideX: true,
|
hideX: true,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
render: ({ hide }) => Confirm({ hide, variant, onResult }),
|
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function useDeleteRequest(id: string | null) {
|
|||||||
variant: 'delete',
|
variant: 'delete',
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
|
Permanently delete <InlineCode>{request?.name}</InlineCode>?
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
|||||||
variant: 'delete',
|
variant: 'delete',
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Are you sure you want to delete <InlineCode>{workspace?.name}</InlineCode>?
|
Permanently delete <InlineCode>{workspace?.name}</InlineCode>?
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
30
src-web/hooks/usePrompt.ts
Normal file
30
src-web/hooks/usePrompt.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { DialogProps } from '../components/core/Dialog';
|
||||||
|
import { useDialog } from '../components/DialogContext';
|
||||||
|
import type { PromptProps } from './Prompt';
|
||||||
|
import { Prompt } from './Prompt';
|
||||||
|
|
||||||
|
export function usePrompt() {
|
||||||
|
const dialog = useDialog();
|
||||||
|
return ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
}: {
|
||||||
|
title: DialogProps['title'];
|
||||||
|
description?: DialogProps['description'];
|
||||||
|
name: PromptProps['name'];
|
||||||
|
label: PromptProps['label'];
|
||||||
|
defaultValue: PromptProps['defaultValue'];
|
||||||
|
}) =>
|
||||||
|
new Promise((onResult: PromptProps['onResult']) => {
|
||||||
|
dialog.show({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
hideX: true,
|
||||||
|
size: 'sm',
|
||||||
|
render: ({ hide }) => Prompt({ onHide: hide, onResult, name, label, defaultValue }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
29
src-web/hooks/useUpdateWorkspace.ts
Normal file
29
src-web/hooks/useUpdateWorkspace.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import type { Workspace } from '../lib/models';
|
||||||
|
import { getWorkspace } from '../lib/store';
|
||||||
|
import { workspacesQueryKey } from './useWorkspaces';
|
||||||
|
|
||||||
|
export function useUpdateWorkspace(id: string | null) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, unknown, Partial<Workspace> | ((w: Workspace) => Workspace)>({
|
||||||
|
mutationFn: async (v) => {
|
||||||
|
const workspace = await getWorkspace(id);
|
||||||
|
if (workspace == null) {
|
||||||
|
throw new Error("Can't update a null workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
|
||||||
|
await invoke('update_workspace', { workspace: newWorkspace });
|
||||||
|
},
|
||||||
|
onMutate: async (v) => {
|
||||||
|
const workspace = await getWorkspace(id);
|
||||||
|
if (workspace === null) return;
|
||||||
|
|
||||||
|
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
|
||||||
|
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(workspace), (workspaces) =>
|
||||||
|
(workspaces ?? []).map((w) => (w.id === newWorkspace.id ? newWorkspace : w)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { HttpRequest } from './models';
|
import type { HttpRequest, Workspace } from './models';
|
||||||
|
|
||||||
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
||||||
if (id === null) return null;
|
if (id === null) return null;
|
||||||
@@ -9,3 +9,12 @@ export async function getRequest(id: string | null): Promise<HttpRequest | null>
|
|||||||
}
|
}
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkspace(id: string | null): Promise<Workspace | null> {
|
||||||
|
if (id === null) return null;
|
||||||
|
const workspace: Workspace = (await invoke('get_workspace', { id })) ?? null;
|
||||||
|
if (workspace == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user