Better opening workspaces and redirect workspace to recent request

This commit is contained in:
Gregory Schier
2023-04-11 11:11:36 -07:00
parent f802d9d655
commit 11200677a0
11 changed files with 146 additions and 70 deletions

View File

@@ -21,7 +21,9 @@ use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
use tauri::regex::Regex;
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, TitleBarStyle, Window, Wry};
use tauri::{
AppHandle, Menu, MenuItem, RunEvent, State, Submenu, TitleBarStyle, Window, WindowUrl, Wry,
};
use tauri::{CustomMenuItem, Manager, WindowEvent};
use tokio::sync::Mutex;
@@ -516,6 +518,12 @@ async fn workspaces(
}
}
#[tauri::command]
async fn new_window(window: Window<Wry>, url: &str) -> Result<(), String> {
create_window(&window.app_handle(), Some(url));
Ok(())
}
#[tauri::command]
async fn delete_workspace(
window: Window<Wry>,
@@ -529,11 +537,6 @@ async fn delete_workspace(
emit_and_return(&window, "deleted_model", workspace)
}
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
tauri::Builder::default()
.setup(|app| {
@@ -564,7 +567,7 @@ fn main() {
})
})
.invoke_handler(tauri::generate_handler![
greet,
new_window,
workspaces,
get_request,
requests,
@@ -588,7 +591,7 @@ fn main() {
.expect("error while running tauri application")
.run(|app_handle, event| match event {
RunEvent::Ready => {
create_window(app_handle);
create_window(app_handle, None);
}
// ExitRequested { api, .. } => {
@@ -602,7 +605,7 @@ fn is_dev() -> bool {
env.unwrap_or("production") != "production"
}
fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
let default_menu = Menu::os_default("Yaak".to_string().as_str());
let mut test_menu = Menu::new()
.add_item(
@@ -661,19 +664,23 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
let window_num = handle.windows().len();
let window_id = format!("wnd_{}", window_num);
let menu = default_menu.add_submenu(submenu);
let win = tauri::WindowBuilder::new(handle, window_id, tauri::WindowUrl::App("".into()))
.menu(menu)
.fullscreen(false)
.resizable(true)
.inner_size(1100.0, 600.0)
.hidden_title(true)
.title(match is_dev() {
true => "Yaak Dev",
false => "Yaak",
})
.title_bar_style(TitleBarStyle::Overlay)
.build()
.expect("failed to build window");
let win = tauri::WindowBuilder::new(
handle,
window_id,
WindowUrl::App(url.unwrap_or_default().into()),
)
.menu(menu)
.fullscreen(false)
.resizable(true)
.inner_size(1100.0, 600.0)
.hidden_title(true)
.title(match is_dev() {
true => "Yaak Dev",
false => "Yaak",
})
.title_bar_style(TitleBarStyle::Overlay)
.build()
.expect("failed to build window");
let win2 = win.clone();
let handle2 = handle.clone();
@@ -691,7 +698,7 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
"toggle_settings" => _ = win2.emit("toggle_settings", true).unwrap(),
"duplicate_request" => _ = win2.emit("duplicate_request", true).unwrap(),
"refresh" => win2.eval("location.reload()").unwrap(),
"new_window" => _ = create_window(&handle2),
"new_window" => _ = create_window(&handle2, None),
"toggle_devtools" => {
if win2.is_devtools_open() {
win2.close_devtools();

View File

@@ -244,6 +244,7 @@ pub async fn upsert_request(
};
let headers_json = Json(headers);
let auth_json = Json(authentication);
let trimmed_name = name.trim();
sqlx::query!(
r#"
INSERT INTO http_requests (
@@ -274,7 +275,7 @@ pub async fn upsert_request(
"#,
id,
workspace_id,
name,
trimmed_name,
url,
method,
body,
@@ -427,12 +428,13 @@ pub async fn update_workspace(
workspace: Workspace,
pool: &Pool<Sqlite>,
) -> Result<Workspace, sqlx::Error> {
let trimmed_name = workspace.name.trim();
sqlx::query!(
r#"
UPDATE workspaces SET (name, updated_at) =
(?, CURRENT_TIMESTAMP) WHERE id = ?;
"#,
workspace.name,
trimmed_name,
workspace.id,
)
.execute(pool)

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Yaak",
"version": "2023.0.14"
"version": "2023.0.15"
},
"tauri": {
"windows": [],

View File

@@ -1,5 +1,7 @@
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
import { routePaths } from '../hooks/useAppRoutes';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests';
import { GlobalHooks } from './GlobalHooks';
import RouteError from './RouteError';
import Workspace from './Workspace';
@@ -21,7 +23,7 @@ const router = createBrowserRouter([
},
{
path: routePaths.workspace({ workspaceId: ':workspaceId' }),
element: <Workspace />,
element: <WorkspaceOrRedirect />,
},
{
path: routePaths.request({
@@ -38,6 +40,23 @@ export function AppRouter() {
return <RouterProvider router={router} />;
}
function WorkspaceOrRedirect() {
const recentRequests = useRecentRequests();
const requests = useRequests();
const request = requests.find((r) => r.id === recentRequests[0]);
const routes = useAppRoutes();
if (request === undefined) {
return <Workspace />;
}
return (
<Navigate
to={routes.paths.request({ workspaceId: request.workspaceId, requestId: request.id })}
/>
);
}
function Layout() {
return (
<>

View File

@@ -57,16 +57,21 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
(requestId: string) => {
const index = requests.findIndex((r) => r.id === requestId);
const request = requests[index];
if (!request || request.id === activeRequestId) return;
if (!request) return;
routes.navigate('request', { requestId, workspaceId: request.workspaceId });
setSelectedIndex(index);
focusActiveRequest(index);
},
[activeRequestId, focusActiveRequest, requests, routes],
[focusActiveRequest, requests, routes],
);
const handleFocus = useCallback(() => focusActiveRequest(), [focusActiveRequest]);
const handleFocus = useCallback(() => {
if (hasFocus) return;
focusActiveRequest(selectedIndex ?? 0);
}, [focusActiveRequest, hasFocus, selectedIndex]);
const handleBlur = useCallback(() => setHasFocus(false), []);
const handleDeleteKey = useCallback(
(e: KeyboardEvent) => {
if (!hasFocus) return;
@@ -85,11 +90,11 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
useTauriEvent(
'focus_sidebar',
() => {
if (hidden) return;
if (hidden || hasFocus) return;
// Select 0 index on focus if none selected
focusActiveRequest(selectedIndex ?? 0);
},
[focusActiveRequest, hidden],
[focusActiveRequest, hidden, activeRequestId],
);
useKeyPressEvent('Enter', (e) => {

View File

@@ -1,3 +1,4 @@
import { invoke } from '@tauri-apps/api';
import classnames from 'classnames';
import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
@@ -12,6 +13,8 @@ import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext';
type Props = {
className?: string;
@@ -24,6 +27,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const dialog = useDialog();
const prompt = usePrompt();
const routes = useAppRoutes();
@@ -31,10 +35,46 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
const workspaceItems = workspaces.map((w) => ({
key: w.id,
label: w.name,
leftSlot: activeWorkspaceId === w.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => {
if (w.id === activeWorkspaceId) return;
routes.navigate('workspace', { workspaceId: w.id });
onSelect: async () => {
dialog.show({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
description: (
<>
Where would you like to open <InlineCode>{w.name}</InlineCode>?
</>
),
render: ({ hide }) => {
return (
<HStack space={2} justifyContent="end" className="mt-6">
<Button
className="focus"
color="gray"
rightSlot={<Icon icon="openNewWindow" />}
onClick={async () => {
hide();
await invoke('new_window', {
url: routes.paths.workspace({ workspaceId: w.id }),
});
}}
>
New Window
</Button>
<Button
className="focus"
color="gray"
onClick={() => {
hide();
routes.navigate('workspace', { workspaceId: w.id });
}}
>
This Window
</Button>
</HStack>
);
},
});
},
}));

View File

@@ -1,7 +1,6 @@
import classnames from 'classnames';
import type { HTMLAttributes } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, memo, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Icon } from './Icon';
const colorStyles = {
@@ -15,8 +14,7 @@ const colorStyles = {
danger: 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
};
export type ButtonProps = HTMLAttributes<HTMLElement> & {
to?: string;
export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
color?: keyof typeof colorStyles;
isLoading?: boolean;
size?: 'sm' | 'md' | 'xs';
@@ -25,12 +23,12 @@ export type ButtonProps = HTMLAttributes<HTMLElement> & {
forDropdown?: boolean;
disabled?: boolean;
title?: string;
rightSlot?: ReactNode;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _Button = forwardRef<any, ButtonProps>(function Button(
const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{
to,
isLoading,
className,
children,
@@ -39,6 +37,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
type = 'button',
justify = 'center',
size = 'md',
rightSlot,
disabled,
...props
}: ButtonProps,
@@ -48,7 +47,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
() =>
classnames(
className,
'outline-none whitespace-nowrap',
'flex-shrink-0 outline-none whitespace-nowrap',
'focus-visible-or-class:ring',
'rounded-md flex items-center',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
@@ -62,22 +61,14 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
[className, disabled, color, justify, size],
);
if (typeof to === 'string') {
return (
<Link ref={ref} to={to} className={classes} {...props}>
{children}
{forDropdown && <Icon icon="chevronDown" className="ml-1 -mr-1" />}
</Link>
);
} else {
return (
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}>
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />}
{children}
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
</button>
);
}
return (
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}>
{isLoading && <Icon icon="update" size={size} className="animate-spin mr-1" />}
{children}
{rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
</button>
);
});
export const Button = memo(_Button);

View File

@@ -62,6 +62,13 @@ export function Dialog({
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
)}
>
<Heading className="text-xl font-semibold w-full" id={titleId}>
{title}
</Heading>
{description && <p id={descriptionId}>{description}</p>}
<div className="mt-4">{children}</div>
{/*Put close at the end so that it's the last thing to be tabbed to*/}
{!hideX && (
<IconButton
onClick={onClose}
@@ -72,11 +79,6 @@ export function Dialog({
className="ml-auto absolute right-1 top-1"
/>
)}
<Heading className="text-xl font-semibold w-full" id={titleId}>
{title}
</Heading>
{description && <p id={descriptionId}>{description}</p>}
<div className="mt-4">{children}</div>
</motion.div>
</div>
</div>

View File

@@ -15,6 +15,7 @@ import React, {
} from 'react';
import { useKey, useKeyPressEvent } from 'react-use';
import { Portal } from '../Portal';
import { Button } from './Button';
import { Separator } from './Separator';
import { VStack } from './Stacks';
@@ -346,16 +347,18 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
if (item.type === 'separator') return <Separator className="my-1.5" />;
return (
<button
<Button
ref={initRef}
size="xs"
tabIndex={-1}
onMouseEnter={(e) => e.currentTarget.focus()}
onMouseLeave={(e) => e.currentTarget.blur()}
onFocus={handleFocus}
onClick={handleClick}
justify="start"
className={classnames(
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 flex text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded',
item.variant === 'danger' && 'text-red-600',
)}
@@ -371,6 +374,6 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
{item.label}
</div>
{item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
</button>
</Button>
);
}

View File

@@ -22,6 +22,7 @@ import {
MagicWandIcon,
MagnifyingGlassIcon,
MoonIcon,
OpenInNewWindowIcon,
PaperPlaneIcon,
Pencil2Icon,
PlusCircledIcon,
@@ -66,6 +67,7 @@ const icons = {
magicWand: MagicWandIcon,
magnifyingGlass: MagnifyingGlassIcon,
moon: MoonIcon,
openNewWindow: OpenInNewWindowIcon,
paperPlane: PaperPlaneIcon,
pencil: Pencil2Icon,
plus: PlusIcon,

View File

@@ -1,13 +1,18 @@
import { useEffect } from 'react';
import { createGlobalState, useEffectOnce, useLocalStorage } from 'react-use';
import { useActiveRequestId } from './useActiveRequestId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
const useHistoryState = createGlobalState<string[]>([]);
export function useRecentRequests() {
const [history, setHistory] = useHistoryState();
const activeWorkspaceId = useActiveWorkspaceId();
const activeRequestId = useActiveRequestId();
const [lsState, setLSState] = useLocalStorage<string[]>('recent_requests', []);
const [history, setHistory] = useHistoryState();
const [lsState, setLSState] = useLocalStorage<string[]>(
'recent_requests::' + activeWorkspaceId,
[],
);
useEffect(() => {
setLSState(history);