diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5abb731d..94a29937 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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, url: &str) -> Result<(), String> { + create_window(&window.app_handle(), Some(url)); + Ok(()) +} + #[tauri::command] async fn delete_workspace( window: Window, @@ -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) -> Window { +fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { 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) -> Window { 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) -> Window { "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(); diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 692473e6..791af81d 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -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, ) -> Result { + 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) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 87e4e151..4394bd44 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Yaak", - "version": "2023.0.14" + "version": "2023.0.15" }, "tauri": { "windows": [], diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index 8326624c..eda8655a 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -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: , + element: , }, { path: routePaths.request({ @@ -38,6 +40,23 @@ export function AppRouter() { return ; } +function WorkspaceOrRedirect() { + const recentRequests = useRecentRequests(); + const requests = useRequests(); + const request = requests.find((r) => r.id === recentRequests[0]); + const routes = useAppRoutes(); + + if (request === undefined) { + return ; + } + + return ( + + ); +} + function Layout() { return ( <> diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 8d54836f..642547d9 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -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) => { diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index 883ea4d6..72b00e0e 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -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 ? : , - 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 {w.name}? + + ), + render: ({ hide }) => { + return ( + + + + + ); + }, + }); }, })); diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index c4a5a196..7e25fbb4 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -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 & { - to?: string; +export type ButtonProps = HTMLAttributes & { color?: keyof typeof colorStyles; isLoading?: boolean; size?: 'sm' | 'md' | 'xs'; @@ -25,12 +23,12 @@ export type ButtonProps = HTMLAttributes & { forDropdown?: boolean; disabled?: boolean; title?: string; + rightSlot?: ReactNode; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const _Button = forwardRef(function Button( +const _Button = forwardRef(function Button( { - to, isLoading, className, children, @@ -39,6 +37,7 @@ const _Button = forwardRef(function Button( type = 'button', justify = 'center', size = 'md', + rightSlot, disabled, ...props }: ButtonProps, @@ -48,7 +47,7 @@ const _Button = forwardRef(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(function Button( [className, disabled, color, justify, size], ); - if (typeof to === 'string') { - return ( - - {children} - {forDropdown && } - - ); - } else { - return ( - - ); - } + return ( + + ); }); export const Button = memo(_Button); diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index c0246504..6d89962d 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -62,6 +62,13 @@ export function Dialog({ size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]', )} > + + {title} + + {description &&

{description}

} +
{children}
+ + {/*Put close at the end so that it's the last thing to be tabbed to*/} {!hideX && ( )} - - {title} - - {description &&

{description}

} -
{children}
diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index 5b94fc24..a8780486 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -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 ; return ( - + ); } diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 88675aac..90ac35e1 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -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, diff --git a/src-web/hooks/useRecentRequests.ts b/src-web/hooks/useRecentRequests.ts index 8c84aa3c..68368685 100644 --- a/src-web/hooks/useRecentRequests.ts +++ b/src-web/hooks/useRecentRequests.ts @@ -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([]); export function useRecentRequests() { - const [history, setHistory] = useHistoryState(); + const activeWorkspaceId = useActiveWorkspaceId(); const activeRequestId = useActiveRequestId(); - const [lsState, setLSState] = useLocalStorage('recent_requests', []); + const [history, setHistory] = useHistoryState(); + const [lsState, setLSState] = useLocalStorage( + 'recent_requests::' + activeWorkspaceId, + [], + ); useEffect(() => { setLSState(history);