diff --git a/plugins/auth-bearer/src/index.ts b/plugins/auth-bearer/src/index.ts index b753d012..1d785148 100644 --- a/plugins/auth-bearer/src/index.ts +++ b/plugins/auth-bearer/src/index.ts @@ -1,3 +1,4 @@ +import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins'; import type { PluginDefinition } from '@yaakapp/api'; export const plugin: PluginDefinition = { @@ -5,17 +6,34 @@ export const plugin: PluginDefinition = { name: 'bearer', label: 'Bearer Token', shortLabel: 'Bearer', - args: [{ - type: 'text', - name: 'token', - label: 'Token', - optional: true, - password: true, - }], + args: [ + { + type: 'text', + name: 'token', + label: 'Token', + optional: true, + password: true, + }, + { + type: 'text', + name: 'prefix', + label: 'Prefix', + optional: true, + placeholder: '', + defaultValue: 'Bearer', + description: + 'The prefix to use for the Authorization header, which will be of the format " ".', + }, + ], async onApply(_ctx, { values }) { - const { token } = values; - const value = `Bearer ${token}`.trim(); - return { setHeaders: [{ name: 'Authorization', value }] }; + return { setHeaders: [generateAuthorizationHeader(values)] }; }, }, }; + +function generateAuthorizationHeader(values: CallHttpAuthenticationRequest['values']) { + const token = String(values.token || '').trim(); + const prefix = String(values.prefix || '').trim(); + const value = `${prefix} ${token}`.trim(); + return { name: 'Authorization', value }; +} diff --git a/plugins/auth-bearer/tests/index.test.ts b/plugins/auth-bearer/tests/index.test.ts new file mode 100644 index 00000000..72858ef9 --- /dev/null +++ b/plugins/auth-bearer/tests/index.test.ts @@ -0,0 +1,67 @@ +import type { Context } from '@yaakapp/api'; +import { describe, expect, test } from 'vitest'; +import { plugin } from '../src'; + +const ctx = {} as Context; + +describe('auth-bearer', () => { + test('No values', async () => { + expect( + await plugin.authentication!.onApply(ctx, { + values: {}, + headers: [], + url: 'https://yaak.app', + method: 'POST', + contextId: '111', + }), + ).toEqual({ setHeaders: [{ name: 'Authorization', value: '' }] }); + }); + + test('Only token', async () => { + expect( + await plugin.authentication!.onApply(ctx, { + values: { token: 'my-token' }, + headers: [], + url: 'https://yaak.app', + method: 'POST', + contextId: '111', + }), + ).toEqual({ setHeaders: [{ name: 'Authorization', value: 'my-token' }] }); + }); + + test('Only prefix', async () => { + expect( + await plugin.authentication!.onApply(ctx, { + values: { prefix: 'Hello' }, + headers: [], + url: 'https://yaak.app', + method: 'POST', + contextId: '111', + }), + ).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello' }] }); + }); + + test('Prefix and token', async () => { + expect( + await plugin.authentication!.onApply(ctx, { + values: { prefix: 'Hello', token: 'my-token' }, + headers: [], + url: 'https://yaak.app', + method: 'POST', + contextId: '111', + }), + ).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] }); + }); + + test('Extra spaces', async () => { + expect( + await plugin.authentication!.onApply(ctx, { + values: { prefix: '\t Hello ', token: ' \nmy-token ' }, + headers: [], + url: 'https://yaak.app', + method: 'POST', + contextId: '111', + }), + ).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] }); + }); +}); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc12e592..b9f14e16 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,13 +35,7 @@ use yaak_models::models::{ }; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; -use yaak_plugins::events::{ - CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, - CallHttpRequestActionRequest, FilterResponse, GetGrpcRequestActionsResponse, - GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, - GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, - InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, -}; +use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest}; use yaak_plugins::manager::PluginManager; use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -1053,21 +1047,6 @@ async fn cmd_install_plugin( )?) } -#[tauri::command] -async fn cmd_uninstall_plugin( - plugin_id: &str, - plugin_manager: State<'_, PluginManager>, - window: WebviewWindow, - app_handle: AppHandle, -) -> YaakResult { - let plugin = - app_handle.db().delete_plugin_by_id(plugin_id, &UpdateSource::from_window(&window))?; - - plugin_manager.uninstall(&PluginWindowContext::new(&window), plugin.directory.as_str()).await?; - - Ok(plugin) -} - #[tauri::command] async fn cmd_create_grpc_request( workspace_id: &str, @@ -1256,6 +1235,14 @@ pub fn run() { for url in event.urls() { if let Err(e) = handle_deep_link(&app_handle, &url).await { warn!("Failed to handle deep link {}: {e:?}", url.to_string()); + let _ = app_handle.emit( + "show_toast", + ShowToastRequest { + message: format!("Error handling deep link: {}", e.to_string()), + color: Some(Color::Danger), + icon: None, + }, + ); }; } }); @@ -1316,7 +1303,6 @@ pub fn run() { cmd_send_http_request, cmd_template_functions, cmd_template_tokens_to_string, - cmd_uninstall_plugin, // // // Migrated commands diff --git a/src-tauri/src/uri_scheme.rs b/src-tauri/src/uri_scheme.rs index e3a201cb..fd4daaea 100644 --- a/src-tauri/src/uri_scheme.rs +++ b/src-tauri/src/uri_scheme.rs @@ -2,8 +2,11 @@ use crate::error::Result; use crate::import::import_data; use log::{info, warn}; use std::collections::HashMap; +use std::fs; use tauri::{AppHandle, Emitter, Manager, Runtime, Url}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; +use yaak_common::api_client::yaak_api_client; +use yaak_models::util::generate_id; use yaak_plugins::events::{Color, ShowToastRequest}; use yaak_plugins::install::download_and_install; @@ -25,9 +28,12 @@ pub(crate) async fn handle_deep_link( _ = window.set_focus(); let confirmed_install = app_handle .dialog() - .message(format!("Install plugin {name} {version:?}?",)) + .message(format!("Install plugin {name} {version:?}?")) .kind(MessageDialogKind::Info) - .buttons(MessageDialogButtons::OkCustom("Install".to_string())) + .buttons(MessageDialogButtons::OkCancelCustom( + "Install".to_string(), + "Cancel".to_string(), + )) .blocking_show(); if !confirmed_install { // Cancelled installation @@ -45,8 +51,51 @@ pub(crate) async fn handle_deep_link( )?; } "import-data" => { - let file_path = query_map.get("path").unwrap(); - let results = import_data(window, file_path).await?; + let mut file_path = query_map.get("path").map(|s| s.to_owned()); + let name = query_map.get("name").map(|s| s.to_owned()).unwrap_or("data".to_string()); + + if let Some(file_url) = query_map.get("url") { + let confirmed_import = app_handle + .dialog() + .message(format!("Import {name} from {file_url}?")) + .kind(MessageDialogKind::Info) + .buttons(MessageDialogButtons::OkCancelCustom( + "Import".to_string(), + "Cancel".to_string(), + )) + .blocking_show(); + if !confirmed_import { + return Ok(()); + } + + let resp = yaak_api_client(app_handle)?.get(file_url).send().await?; + let json = resp.bytes().await?; + let p = app_handle + .path() + .temp_dir()? + .join(format!("import-{}", generate_id())) + .to_string_lossy() + .to_string(); + fs::write(&p, json)?; + file_path = Some(p); + } + + let file_path = match file_path { + Some(p) => p, + None => { + app_handle.emit( + "show_toast", + ShowToastRequest { + message: "Failed to import data".to_string(), + color: Some(Color::Danger), + icon: None, + }, + )?; + return Ok(()); + } + }; + + let results = import_data(window, &file_path).await?; _ = window.set_focus(); window.emit( "show_toast", diff --git a/src-tauri/yaak-plugins/build.rs b/src-tauri/yaak-plugins/build.rs index 7a8b73a3..adfe92bc 100644 --- a/src-tauri/yaak-plugins/build.rs +++ b/src-tauri/yaak-plugins/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["search", "install", "updates"]; +const COMMANDS: &[&str] = &["search", "install", "updates", "uninstall"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/src-tauri/yaak-plugins/index.ts b/src-tauri/yaak-plugins/index.ts index 2b147163..e2e1d42f 100644 --- a/src-tauri/yaak-plugins/index.ts +++ b/src-tauri/yaak-plugins/index.ts @@ -13,6 +13,10 @@ export async function installPlugin(name: string, version: string | null) { return invoke('plugin:yaak-plugins|install', { name, version }); } +export async function uninstallPlugin(pluginId: string) { + return invoke('plugin:yaak-plugins|uninstall', { pluginId }); +} + export async function checkPluginUpdates() { return invoke('plugin:yaak-plugins|updates', {}); } diff --git a/src-tauri/yaak-plugins/permissions/default.toml b/src-tauri/yaak-plugins/permissions/default.toml index 7a3aea26..ab5fd24b 100644 --- a/src-tauri/yaak-plugins/permissions/default.toml +++ b/src-tauri/yaak-plugins/permissions/default.toml @@ -1,3 +1,3 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-search", "allow-install", "allow-updates"] +permissions = ["allow-search", "allow-install", "allow-uninstall", "allow-updates"] diff --git a/src-tauri/yaak-plugins/src/commands.rs b/src-tauri/yaak-plugins/src/commands.rs index 2b06660f..ccc51bfa 100644 --- a/src-tauri/yaak-plugins/src/commands.rs +++ b/src-tauri/yaak-plugins/src/commands.rs @@ -1,9 +1,10 @@ use crate::api::{ - PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins, + check_plugin_updates, search_plugins, PluginSearchResponse, PluginUpdatesResponse, }; use crate::error::Result; -use crate::install::download_and_install; -use tauri::{AppHandle, Runtime, WebviewWindow, command}; +use crate::install::{delete_and_uninstall, download_and_install}; +use tauri::{command, AppHandle, Runtime, WebviewWindow}; +use yaak_models::models::Plugin; #[command] pub(crate) async fn search( @@ -23,6 +24,14 @@ pub(crate) async fn install( Ok(()) } +#[command] +pub(crate) async fn uninstall( + plugin_id: &str, + window: WebviewWindow, +) -> Result { + delete_and_uninstall(&window, plugin_id).await +} + #[command] pub(crate) async fn updates(app_handle: AppHandle) -> Result { check_plugin_updates(&app_handle).await diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs index 9cc691f6..fdc182fe 100644 --- a/src-tauri/yaak-plugins/src/install.rs +++ b/src-tauri/yaak-plugins/src/install.rs @@ -13,6 +13,13 @@ use yaak_models::models::Plugin; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; +pub async fn delete_and_uninstall(window: &WebviewWindow, plugin_id: &str) -> Result { + let plugin_manager = window.state::(); + let plugin = window.db().delete_plugin_by_id(plugin_id, &UpdateSource::from_window(&window))?; + plugin_manager.uninstall(&PluginWindowContext::new(&window), plugin.directory.as_str()).await?; + Ok(plugin) +} + pub async fn download_and_install( window: &WebviewWindow, name: &str, diff --git a/src-tauri/yaak-plugins/src/lib.rs b/src-tauri/yaak-plugins/src/lib.rs index abb821c7..e19bf456 100644 --- a/src-tauri/yaak-plugins/src/lib.rs +++ b/src-tauri/yaak-plugins/src/lib.rs @@ -1,9 +1,9 @@ -use crate::commands::{install, search, updates}; +use crate::commands::{install, search, uninstall, updates}; use crate::manager::PluginManager; use log::info; use std::process::exit; use tauri::plugin::{Builder, TauriPlugin}; -use tauri::{Manager, RunEvent, Runtime, State, generate_handler}; +use tauri::{generate_handler, Manager, RunEvent, Runtime, State}; mod commands; pub mod error; @@ -22,7 +22,7 @@ pub mod plugin_meta; pub fn init() -> TauriPlugin { Builder::new("yaak-plugins") - .invoke_handler(generate_handler![search, install, updates]) + .invoke_handler(generate_handler![search, install, uninstall, updates]) .setup(|app_handle, _| { let manager = PluginManager::new(app_handle.clone()); app_handle.manage(manager.clone()); diff --git a/src-web/commands/openWorkspaceSettings.tsx b/src-web/commands/openWorkspaceSettings.tsx index 48ae0682..3ce40ce8 100644 --- a/src-web/commands/openWorkspaceSettings.tsx +++ b/src-web/commands/openWorkspaceSettings.tsx @@ -9,14 +9,15 @@ import { jotaiStore } from '../lib/jotai'; export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) { const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); + if (workspaceId == null) return; showDialog({ id: 'workspace-settings', title: 'Workspace Settings', - size: 'lg', - className: 'h-[50rem]', + size: 'md', + className: 'h-[calc(100vh-5rem)] max-h-[40rem]', noPadding: true, - render({ hide }) { - return ; - }, + render: ({ hide }) => ( + + ), }); } diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index a92a5144..5265f9f3 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -1,18 +1,10 @@ -import { emit } from '@tauri-apps/api/event'; -import type { InternalEvent } from '@yaakapp-internal/plugins'; -import type { ShowToastRequest } from '@yaakapp/api'; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication'; -import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; -import { useNotificationToast } from '../hooks/useNotificationToast'; import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; -import { generateId } from '../lib/generateId'; -import { showPrompt } from '../lib/prompt'; -import { showToast } from '../lib/toast'; export function GlobalHooks() { useSyncZoomSetting(); @@ -25,32 +17,7 @@ export function GlobalHooks() { useSubscribeHttpAuthentication(); // Other useful things - useNotificationToast(); useActiveWorkspaceChangedToast(); - // Listen for toasts - useListenToTauriEvent('show_toast', (event) => { - showToast({ ...event.payload }); - }); - - // Listen for plugin events - useListenToTauriEvent('plugin_event', async ({ payload: event }) => { - if (event.payload.type === 'prompt_text_request') { - const value = await showPrompt(event.payload); - const result: InternalEvent = { - id: generateId(), - replyId: event.id, - pluginName: event.pluginName, - pluginRefId: event.pluginRefId, - windowContext: event.windowContext, - payload: { - type: 'prompt_text_response', - value, - }, - }; - await emit(event.id, result); - } - }); - return null; } diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index b1001ddc..6c64239b 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -3,7 +3,12 @@ import { openUrl } from '@tauri-apps/plugin-opener'; import type { Plugin } from '@yaakapp-internal/models'; import { pluginsAtom } from '@yaakapp-internal/models'; import type { PluginVersion } from '@yaakapp-internal/plugins'; -import { checkPluginUpdates, installPlugin, searchPlugins } from '@yaakapp-internal/plugins'; +import { + checkPluginUpdates, + installPlugin, + searchPlugins, + uninstallPlugin, +} from '@yaakapp-internal/plugins'; import type { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api'; import { useAtomValue } from 'jotai'; import React, { useState } from 'react'; @@ -11,8 +16,10 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useInstallPlugin } from '../../hooks/useInstallPlugin'; import { usePluginInfo } from '../../hooks/usePluginInfo'; import { useRefreshPlugins } from '../../hooks/usePlugins'; -import { useUninstallPlugin } from '../../hooks/useUninstallPlugin'; +import { showConfirmDelete } from '../../lib/confirm'; +import { minPromiseMillis } from '../../lib/minPromiseMillis'; import { Button } from '../core/Button'; +import { CountBadge } from '../core/CountBadge'; import { IconButton } from '../core/IconButton'; import { InlineCode } from '../core/InlineCode'; import { Link } from '../core/Link'; @@ -26,6 +33,7 @@ import { SelectFile } from '../SelectFile'; export function SettingsPlugins() { const [directory, setDirectory] = React.useState(null); + const plugins = useAtomValue(pluginsAtom); const createPlugin = useInstallPlugin(); const refreshPlugins = useRefreshPlugins(); const [tab, setTab] = useState(); @@ -39,7 +47,11 @@ export function SettingsPlugins() { tabListClassName="!-ml-3" tabs={[ { label: 'Marketplace', value: 'search' }, - { label: 'Installed', value: 'installed' }, + { + label: 'Installed', + value: 'installed', + rightSlot: , + }, ]} > @@ -103,32 +115,36 @@ function PluginTableRow({ updates: PluginUpdatesResponse | null; }) { const pluginInfo = usePluginInfo(plugin.id); - const uninstallPlugin = useUninstallPlugin(); const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version; const installPluginMutation = useMutation({ mutationKey: ['install_plugin', plugin.id], mutationFn: (name: string) => installPlugin(name, null), }); - if (pluginInfo.data == null) return null; + + const displayName = pluginInfo.data?.displayName ?? 'Unknown'; + const uninstallPluginMutation = usePromptUninstall(plugin.id, displayName); + + if (pluginInfo.isPending) { + return null; + } return ( {plugin.url ? ( - {pluginInfo.data.displayName} + {displayName} ) : ( - pluginInfo.data.displayName + displayName )} - {pluginInfo.data?.version} + {pluginInfo.data?.version ?? 'n/a'} - {pluginInfo.data.description} - - {latestVersion != null && ( + + {pluginInfo.data && latestVersion != null && ( @@ -173,7 +192,7 @@ function PluginSearch() { defaultValue={query} /> -
+
{results.data == null ? ( @@ -186,7 +205,6 @@ function PluginSearch() { Name Version - Description @@ -201,11 +219,8 @@ function PluginSearch() { {plugin.version} - - {plugin.description ?? 'n/a'} - - - + + ))} @@ -217,12 +232,12 @@ function PluginSearch() { ); } -function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { +function InstallPluginButton({ pluginVersion }: { pluginVersion: PluginVersion }) { const plugins = useAtomValue(pluginsAtom); - const uninstallPlugin = useUninstallPlugin(); - const installed = plugins?.some((p) => p.id === plugin.id); + const installed = plugins?.some((p) => p.id === pluginVersion.id); + const uninstallPluginMutation = usePromptUninstall(pluginVersion.id, pluginVersion.displayName); const installPluginMutation = useMutation({ - mutationKey: ['install_plugin', plugin.id], + mutationKey: ['install_plugin', pluginVersion.id], mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null), }); @@ -230,14 +245,14 @@ function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { - {workspaceId} + + {workspaceId} + + + + + patchModel(workspaceMeta, { settingSyncDir: filePath })} + /> + + + ); } diff --git a/src-web/components/core/Confirm.tsx b/src-web/components/core/Confirm.tsx index 2b15fbc0..c88a0f25 100644 --- a/src-web/components/core/Confirm.tsx +++ b/src-web/components/core/Confirm.tsx @@ -1,33 +1,62 @@ import type { Color } from '@yaakapp-internal/plugins'; +import type { FormEvent } from 'react'; +import { useState } from 'react'; import { Button } from './Button'; +import { PlainInput } from './PlainInput'; import { HStack } from './Stacks'; export interface ConfirmProps { onHide: () => void; onResult: (result: boolean) => void; confirmText?: string; + requireTyping?: string; color?: Color; } -export function Confirm({ onHide, onResult, confirmText, color = 'primary' }: ConfirmProps) { +export function Confirm({ + onHide, + onResult, + confirmText, + requireTyping, + color = 'primary', +}: ConfirmProps) { + const [confirm, setConfirm] = useState(''); const handleHide = () => { onResult(false); onHide(); }; - const handleSuccess = () => { - onResult(true); - onHide(); + const didConfirm = !requireTyping || confirm === requireTyping; + + const handleSuccess = (e: FormEvent) => { + e.preventDefault(); + if (didConfirm) { + onResult(true); + onHide(); + } }; return ( - - - - +
+ {requireTyping && ( + + Type {requireTyping} to confirm + + } + /> + )} + + + + + ); } diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index 715c7cb8..a050714f 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -77,9 +77,9 @@ export function Dialog({ 'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]', 'min-h-[10rem]', 'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]', - size === 'sm' && 'w-[28rem]', - size === 'md' && 'w-[45rem]', - size === 'lg' && 'w-[65rem]', + size === 'sm' && 'w-[30rem]', + size === 'md' && 'w-[50rem]', + size === 'lg' && 'w-[70rem]', size === 'full' && 'w-[100vw] h-[100vh]', size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]', )} diff --git a/src-web/components/core/Link.tsx b/src-web/components/core/Link.tsx index 82b8774f..df3a3467 100644 --- a/src-web/components/core/Link.tsx +++ b/src-web/components/core/Link.tsx @@ -28,7 +28,7 @@ export function Link({ href, children, noUnderline, className, ...other }: Props rel="noopener noreferrer" className={classNames( className, - 'pr-4 inline-flex items-center hover:underline', + 'pr-4 inline-flex items-center hover:underline group', !noUnderline && 'underline', )} onClick={(e) => { @@ -36,8 +36,8 @@ export function Link({ href, children, noUnderline, className, ...other }: Props }} {...other} > - {children} - + {children} + ); } diff --git a/src-web/components/core/Table.tsx b/src-web/components/core/Table.tsx index ff9e6f3c..eba48232 100644 --- a/src-web/components/core/Table.tsx +++ b/src-web/components/core/Table.tsx @@ -27,7 +27,7 @@ export function TableCell({ children, className }: { children: ReactNode; classN {children} @@ -57,7 +57,7 @@ export function TableHeaderCell({ className?: string; }) { return ( - + {children} ); diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index d476e133..ea3eb86d 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -84,7 +84,7 @@ export function Tabs({ tabListClassName, addBorders && '!-ml-1', 'flex items-center hide-scrollbars mb-2', - layout === 'horizontal' && 'h-full overflow-auto pt-1 px-2', + layout === 'horizontal' && 'h-full overflow-auto px-2', layout === 'vertical' && 'overflow-x-auto overflow-y-visible ', // Give space for button focus states within overflow boundary. layout === 'vertical' && 'py-1 -ml-5 pl-3 pr-1', @@ -92,7 +92,7 @@ export function Tabs({ >
@@ -104,8 +104,11 @@ export function Tabs({ addBorders && 'border', isActive ? 'text-text' : 'text-text-subtle hover:text-text', isActive && addBorders - ? 'border-border-subtle bg-surface-active' - : 'border-transparent', + ? 'border-surface-active bg-surface-active' + : layout === 'vertical' + ? 'border-border-subtle' + : 'border-transparent', + layout === 'horizontal' && 'flex justify-between', ); if ('options' in t) { @@ -121,7 +124,7 @@ export function Tabs({ > - ) : null, - }); - }); -} diff --git a/src-web/hooks/useUninstallPlugin.tsx b/src-web/hooks/useUninstallPlugin.tsx deleted file mode 100644 index 038c541e..00000000 --- a/src-web/hooks/useUninstallPlugin.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { InlineCode } from '../components/core/InlineCode'; -import { showConfirmDelete } from '../lib/confirm'; -import { invokeCmd } from '../lib/tauri'; -import { useFastMutation } from './useFastMutation'; - -export function useUninstallPlugin() { - return useFastMutation({ - mutationKey: ['uninstall_plugin'], - mutationFn: async ({ pluginId, name }: { pluginId: string; name: string }) => { - const confirmed = await showConfirmDelete({ - id: 'uninstall-plugin-' + name, - title: 'Uninstall Plugin', - confirmText: 'Uninstall', - description: ( - <> - Permanently uninstall {name}? - - ), - }); - if (confirmed) { - await invokeCmd('cmd_uninstall_plugin', { pluginId }); - } - }, - }); -} diff --git a/src-web/lib/confirm.ts b/src-web/lib/confirm.ts index 2eda76d6..cad3031f 100644 --- a/src-web/lib/confirm.ts +++ b/src-web/lib/confirm.ts @@ -6,28 +6,29 @@ import { showDialog } from './dialog'; type ConfirmArgs = { id: string; } & Pick & - Pick; + Pick; -export async function showConfirm({ id, title, description, color, confirmText }: ConfirmArgs) { +export async function showConfirm({ + color, + confirmText, + requireTyping, + ...extraProps +}: ConfirmArgs) { return new Promise((onResult: ConfirmProps['onResult']) => { showDialog({ - id, - title, - description, + ...extraProps, hideX: true, size: 'sm', disableBackdropClose: true, // Prevent accidental dismisses - render: ({ hide }) => Confirm({ onHide: hide, color, onResult, confirmText }), + render: ({ hide }) => Confirm({ onHide: hide, color, onResult, confirmText, requireTyping }), }); }); } -export async function showConfirmDelete({ id, title, description }: ConfirmArgs) { +export async function showConfirmDelete({ confirmText, color, ...extraProps }: ConfirmArgs) { return showConfirm({ - id, - title, - description, - color: 'danger', - confirmText: 'Delete', + color: color ?? 'danger', + confirmText: confirmText ?? 'Delete', + ...extraProps, }); } diff --git a/src-web/lib/deleteModelWithConfirm.tsx b/src-web/lib/deleteModelWithConfirm.tsx index ff42409c..6801a5ff 100644 --- a/src-web/lib/deleteModelWithConfirm.tsx +++ b/src-web/lib/deleteModelWithConfirm.tsx @@ -4,8 +4,11 @@ import { InlineCode } from '../components/core/InlineCode'; import { showConfirmDelete } from './confirm'; import { resolvedModelName } from './resolvedModelName'; -export async function deleteModelWithConfirm(model: AnyModel | null): Promise { - if (model == null ) { +export async function deleteModelWithConfirm( + model: AnyModel | null, + options: { confirmName?: string } = {}, +): Promise { + if (model == null) { console.warn('Tried to delete null model'); return false; } @@ -13,6 +16,7 @@ export async function deleteModelWithConfirm(model: AnyModel | null): Promise Permanently delete {resolvedModelName(model)}? diff --git a/src-web/lib/initGlobalListeners.tsx b/src-web/lib/initGlobalListeners.tsx new file mode 100644 index 00000000..f75ff578 --- /dev/null +++ b/src-web/lib/initGlobalListeners.tsx @@ -0,0 +1,77 @@ +import { emit } from '@tauri-apps/api/event'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import type { InternalEvent } from '@yaakapp-internal/plugins'; +import type { ShowToastRequest } from '@yaakapp/api'; +import { openSettings } from '../commands/openSettings'; +import { Button } from '../components/core/Button'; + +// Listen for toasts +import { listenToTauriEvent } from '../hooks/useListenToTauriEvent'; +import { generateId } from './generateId'; +import { showPrompt } from './prompt'; +import { invokeCmd } from './tauri'; +import { showToast } from './toast'; + +export function initGlobalListeners() { + listenToTauriEvent('show_toast', (event) => { + showToast({ ...event.payload }); + }); + + listenToTauriEvent('settings', () => openSettings.mutate(null)); + + // Listen for plugin events + listenToTauriEvent('plugin_event', async ({ payload: event }) => { + if (event.payload.type === 'prompt_text_request') { + const value = await showPrompt(event.payload); + const result: InternalEvent = { + id: generateId(), + replyId: event.id, + pluginName: event.pluginName, + pluginRefId: event.pluginRefId, + windowContext: event.windowContext, + payload: { + type: 'prompt_text_response', + value, + }, + }; + await emit(event.id, result); + } + }); + + listenToTauriEvent<{ + id: string; + timestamp: string; + message: string; + timeout?: number | null; + action?: null | { + url: string; + label: string; + }; + }>('notification', ({ payload }) => { + console.log('Got notification event', payload); + const actionUrl = payload.action?.url; + const actionLabel = payload.action?.label; + showToast({ + id: payload.id, + timeout: payload.timeout ?? undefined, + message: payload.message, + onClose: () => { + invokeCmd('cmd_dismiss_notification', { notificationId: payload.id }).catch(console.error); + }, + action: ({ hide }) => + actionLabel && actionUrl ? ( + + ) : null, + }); + }); +} diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index 8ee1a755..fd020df9 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -39,8 +39,7 @@ type TauriCmd = | 'cmd_send_http_request' | 'cmd_show_workspace_key' | 'cmd_template_functions' - | 'cmd_template_tokens_to_string' - | 'cmd_uninstall_plugin'; + | 'cmd_template_tokens_to_string'; export async function invokeCmd(cmd: TauriCmd, args?: InvokeArgs): Promise { // console.log('RUN COMMAND', cmd, args); diff --git a/src-web/main.tsx b/src-web/main.tsx index 07f81ed6..33584ca3 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -6,6 +6,7 @@ import { changeModelStoreWorkspace, initModelStore } from '@yaakapp-internal/mod import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { initSync } from './init/sync'; +import { initGlobalListeners } from './lib/initGlobalListeners'; import { jotaiStore } from './lib/jotai'; import { router } from './lib/router'; @@ -44,6 +45,7 @@ window.addEventListener('keydown', (e) => { // Initialize a bunch of watchers initSync(); initModelStore(jotaiStore); +initGlobalListeners(); await changeModelStoreWorkspace(null); // Load global models console.log('Creating React root');