Tweak workspace settings and a bunch of small things

This commit is contained in:
Gregory Schier
2025-07-18 08:47:14 -07:00
parent 4c375ed3e9
commit bcde4de4a7
28 changed files with 450 additions and 271 deletions

View File

@@ -1,3 +1,4 @@
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
import type { PluginDefinition } from '@yaakapp/api'; import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
@@ -5,17 +6,34 @@ export const plugin: PluginDefinition = {
name: 'bearer', name: 'bearer',
label: 'Bearer Token', label: 'Bearer Token',
shortLabel: 'Bearer', shortLabel: 'Bearer',
args: [{ args: [
type: 'text', {
name: 'token', type: 'text',
label: 'Token', name: 'token',
optional: true, label: 'Token',
password: true, 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 "<PREFIX> <TOKEN>".',
},
],
async onApply(_ctx, { values }) { async onApply(_ctx, { values }) {
const { token } = values; return { setHeaders: [generateAuthorizationHeader(values)] };
const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: 'Authorization', value }] };
}, },
}, },
}; };
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 };
}

View File

@@ -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' }] });
});
});

View File

@@ -35,13 +35,7 @@ use yaak_models::models::{
}; };
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{ use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest};
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose,
};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -1053,21 +1047,6 @@ async fn cmd_install_plugin<R: Runtime>(
)?) )?)
} }
#[tauri::command]
async fn cmd_uninstall_plugin<R: Runtime>(
plugin_id: &str,
plugin_manager: State<'_, PluginManager>,
window: WebviewWindow<R>,
app_handle: AppHandle<R>,
) -> YaakResult<Plugin> {
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] #[tauri::command]
async fn cmd_create_grpc_request<R: Runtime>( async fn cmd_create_grpc_request<R: Runtime>(
workspace_id: &str, workspace_id: &str,
@@ -1256,6 +1235,14 @@ pub fn run() {
for url in event.urls() { for url in event.urls() {
if let Err(e) = handle_deep_link(&app_handle, &url).await { if let Err(e) = handle_deep_link(&app_handle, &url).await {
warn!("Failed to handle deep link {}: {e:?}", url.to_string()); 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_send_http_request,
cmd_template_functions, cmd_template_functions,
cmd_template_tokens_to_string, cmd_template_tokens_to_string,
cmd_uninstall_plugin,
// //
// //
// Migrated commands // Migrated commands

View File

@@ -2,8 +2,11 @@ use crate::error::Result;
use crate::import::import_data; use crate::import::import_data;
use log::{info, warn}; use log::{info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use tauri::{AppHandle, Emitter, Manager, Runtime, Url}; use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; 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::events::{Color, ShowToastRequest};
use yaak_plugins::install::download_and_install; use yaak_plugins::install::download_and_install;
@@ -25,9 +28,12 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
_ = window.set_focus(); _ = window.set_focus();
let confirmed_install = app_handle let confirmed_install = app_handle
.dialog() .dialog()
.message(format!("Install plugin {name} {version:?}?",)) .message(format!("Install plugin {name} {version:?}?"))
.kind(MessageDialogKind::Info) .kind(MessageDialogKind::Info)
.buttons(MessageDialogButtons::OkCustom("Install".to_string())) .buttons(MessageDialogButtons::OkCancelCustom(
"Install".to_string(),
"Cancel".to_string(),
))
.blocking_show(); .blocking_show();
if !confirmed_install { if !confirmed_install {
// Cancelled installation // Cancelled installation
@@ -45,8 +51,51 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
)?; )?;
} }
"import-data" => { "import-data" => {
let file_path = query_map.get("path").unwrap(); let mut file_path = query_map.get("path").map(|s| s.to_owned());
let results = import_data(window, file_path).await?; 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.set_focus();
window.emit( window.emit(
"show_toast", "show_toast",

View File

@@ -1,4 +1,4 @@
const COMMANDS: &[&str] = &["search", "install", "updates"]; const COMMANDS: &[&str] = &["search", "install", "updates", "uninstall"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS).build(); tauri_plugin::Builder::new(COMMANDS).build();

View File

@@ -13,6 +13,10 @@ export async function installPlugin(name: string, version: string | null) {
return invoke<string>('plugin:yaak-plugins|install', { name, version }); return invoke<string>('plugin:yaak-plugins|install', { name, version });
} }
export async function uninstallPlugin(pluginId: string) {
return invoke<string>('plugin:yaak-plugins|uninstall', { pluginId });
}
export async function checkPluginUpdates() { export async function checkPluginUpdates() {
return invoke<PluginUpdatesResponse>('plugin:yaak-plugins|updates', {}); return invoke<PluginUpdatesResponse>('plugin:yaak-plugins|updates', {});
} }

View File

@@ -1,3 +1,3 @@
[default] [default]
description = "Default permissions for the plugin" description = "Default permissions for the plugin"
permissions = ["allow-search", "allow-install", "allow-updates"] permissions = ["allow-search", "allow-install", "allow-uninstall", "allow-updates"]

View File

@@ -1,9 +1,10 @@
use crate::api::{ use crate::api::{
PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins, check_plugin_updates, search_plugins, PluginSearchResponse, PluginUpdatesResponse,
}; };
use crate::error::Result; use crate::error::Result;
use crate::install::download_and_install; use crate::install::{delete_and_uninstall, download_and_install};
use tauri::{AppHandle, Runtime, WebviewWindow, command}; use tauri::{command, AppHandle, Runtime, WebviewWindow};
use yaak_models::models::Plugin;
#[command] #[command]
pub(crate) async fn search<R: Runtime>( pub(crate) async fn search<R: Runtime>(
@@ -23,6 +24,14 @@ pub(crate) async fn install<R: Runtime>(
Ok(()) Ok(())
} }
#[command]
pub(crate) async fn uninstall<R: Runtime>(
plugin_id: &str,
window: WebviewWindow<R>,
) -> Result<Plugin> {
delete_and_uninstall(&window, plugin_id).await
}
#[command] #[command]
pub(crate) async fn updates<R: Runtime>(app_handle: AppHandle<R>) -> Result<PluginUpdatesResponse> { pub(crate) async fn updates<R: Runtime>(app_handle: AppHandle<R>) -> Result<PluginUpdatesResponse> {
check_plugin_updates(&app_handle).await check_plugin_updates(&app_handle).await

View File

@@ -13,6 +13,13 @@ use yaak_models::models::Plugin;
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
pub async fn delete_and_uninstall<R: Runtime>(window: &WebviewWindow<R>, plugin_id: &str) -> Result<Plugin> {
let plugin_manager = window.state::<PluginManager>();
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<R: Runtime>( pub async fn download_and_install<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
name: &str, name: &str,

View File

@@ -1,9 +1,9 @@
use crate::commands::{install, search, updates}; use crate::commands::{install, search, uninstall, updates};
use crate::manager::PluginManager; use crate::manager::PluginManager;
use log::info; use log::info;
use std::process::exit; use std::process::exit;
use tauri::plugin::{Builder, TauriPlugin}; use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, RunEvent, Runtime, State, generate_handler}; use tauri::{generate_handler, Manager, RunEvent, Runtime, State};
mod commands; mod commands;
pub mod error; pub mod error;
@@ -22,7 +22,7 @@ pub mod plugin_meta;
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-plugins") Builder::new("yaak-plugins")
.invoke_handler(generate_handler![search, install, updates]) .invoke_handler(generate_handler![search, install, uninstall, updates])
.setup(|app_handle, _| { .setup(|app_handle, _| {
let manager = PluginManager::new(app_handle.clone()); let manager = PluginManager::new(app_handle.clone());
app_handle.manage(manager.clone()); app_handle.manage(manager.clone());

View File

@@ -9,14 +9,15 @@ import { jotaiStore } from '../lib/jotai';
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) { export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
showDialog({ showDialog({
id: 'workspace-settings', id: 'workspace-settings',
title: 'Workspace Settings', title: 'Workspace Settings',
size: 'lg', size: 'md',
className: 'h-[50rem]', className: 'h-[calc(100vh-5rem)] max-h-[40rem]',
noPadding: true, noPadding: true,
render({ hide }) { render: ({ hide }) => (
return <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />; <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
}, ),
}); });
} }

View File

@@ -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 { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { generateId } from '../lib/generateId';
import { showPrompt } from '../lib/prompt';
import { showToast } from '../lib/toast';
export function GlobalHooks() { export function GlobalHooks() {
useSyncZoomSetting(); useSyncZoomSetting();
@@ -25,32 +17,7 @@ export function GlobalHooks() {
useSubscribeHttpAuthentication(); useSubscribeHttpAuthentication();
// Other useful things // Other useful things
useNotificationToast();
useActiveWorkspaceChangedToast(); useActiveWorkspaceChangedToast();
// Listen for toasts
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
showToast({ ...event.payload });
});
// Listen for plugin events
useListenToTauriEvent<InternalEvent>('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; return null;
} }

View File

@@ -3,7 +3,12 @@ import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models'; import type { Plugin } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models'; import { pluginsAtom } from '@yaakapp-internal/models';
import type { PluginVersion } from '@yaakapp-internal/plugins'; 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 type { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import React, { useState } from 'react'; import React, { useState } from 'react';
@@ -11,8 +16,10 @@ import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useInstallPlugin } from '../../hooks/useInstallPlugin'; import { useInstallPlugin } from '../../hooks/useInstallPlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo'; import { usePluginInfo } from '../../hooks/usePluginInfo';
import { useRefreshPlugins } from '../../hooks/usePlugins'; 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 { Button } from '../core/Button';
import { CountBadge } from '../core/CountBadge';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode'; import { InlineCode } from '../core/InlineCode';
import { Link } from '../core/Link'; import { Link } from '../core/Link';
@@ -26,6 +33,7 @@ import { SelectFile } from '../SelectFile';
export function SettingsPlugins() { export function SettingsPlugins() {
const [directory, setDirectory] = React.useState<string | null>(null); const [directory, setDirectory] = React.useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const createPlugin = useInstallPlugin(); const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins(); const refreshPlugins = useRefreshPlugins();
const [tab, setTab] = useState<string>(); const [tab, setTab] = useState<string>();
@@ -39,7 +47,11 @@ export function SettingsPlugins() {
tabListClassName="!-ml-3" tabListClassName="!-ml-3"
tabs={[ tabs={[
{ label: 'Marketplace', value: 'search' }, { label: 'Marketplace', value: 'search' },
{ label: 'Installed', value: 'installed' }, {
label: 'Installed',
value: 'installed',
rightSlot: <CountBadge count={plugins.length} />,
},
]} ]}
> >
<TabContent value="search"> <TabContent value="search">
@@ -103,32 +115,36 @@ function PluginTableRow({
updates: PluginUpdatesResponse | null; updates: PluginUpdatesResponse | null;
}) { }) {
const pluginInfo = usePluginInfo(plugin.id); const pluginInfo = usePluginInfo(plugin.id);
const uninstallPlugin = useUninstallPlugin();
const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version; const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version;
const installPluginMutation = useMutation({ const installPluginMutation = useMutation({
mutationKey: ['install_plugin', plugin.id], mutationKey: ['install_plugin', plugin.id],
mutationFn: (name: string) => installPlugin(name, null), 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 ( return (
<TableRow> <TableRow>
<TableCell className="font-semibold"> <TableCell className="font-semibold">
{plugin.url ? ( {plugin.url ? (
<Link noUnderline href={plugin.url}> <Link noUnderline href={plugin.url}>
{pluginInfo.data.displayName} {displayName}
</Link> </Link>
) : ( ) : (
pluginInfo.data.displayName displayName
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
<InlineCode>{pluginInfo.data?.version}</InlineCode> <InlineCode>{pluginInfo.data?.version ?? 'n/a'}</InlineCode>
</TableCell> </TableCell>
<TableCell className="w-full text-text-subtle">{pluginInfo.data.description}</TableCell>
<TableCell> <TableCell>
<HStack> <HStack justifyContent="end">
{latestVersion != null && ( {pluginInfo.data && latestVersion != null && (
<Button <Button
variant="border" variant="border"
color="success" color="success"
@@ -140,14 +156,17 @@ function PluginTableRow({
Update Update
</Button> </Button>
)} )}
<IconButton <Button
size="sm" size="xs"
icon="trash"
title="Uninstall plugin" title="Uninstall plugin"
variant="border"
isLoading={uninstallPluginMutation.isPending}
onClick={async () => { onClick={async () => {
uninstallPlugin.mutate({ pluginId: plugin.id, name: pluginInfo.data.displayName }); uninstallPluginMutation.mutate();
}} }}
/> >
Uninstall
</Button>
</HStack> </HStack>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -173,7 +192,7 @@ function PluginSearch() {
defaultValue={query} defaultValue={query}
/> />
</HStack> </HStack>
<div className="w-full h-full overflow-auto"> <div className="w-full h-full">
{results.data == null ? ( {results.data == null ? (
<EmptyStateText> <EmptyStateText>
<LoadingIcon size="xl" className="text-text-subtlest" /> <LoadingIcon size="xl" className="text-text-subtlest" />
@@ -186,7 +205,6 @@ function PluginSearch() {
<TableRow> <TableRow>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell> <TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
<TableHeaderCell /> <TableHeaderCell />
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -201,11 +219,8 @@ function PluginSearch() {
<TableCell> <TableCell>
<InlineCode>{plugin.version}</InlineCode> <InlineCode>{plugin.version}</InlineCode>
</TableCell> </TableCell>
<TableCell className="w-full text-text-subtle"> <TableCell className="w-[6rem]">
{plugin.description ?? 'n/a'} <InstallPluginButton pluginVersion={plugin} />
</TableCell>
<TableCell>
<InstallPluginButton plugin={plugin} />
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -217,12 +232,12 @@ function PluginSearch() {
); );
} }
function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { function InstallPluginButton({ pluginVersion }: { pluginVersion: PluginVersion }) {
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom);
const uninstallPlugin = useUninstallPlugin(); const installed = plugins?.some((p) => p.id === pluginVersion.id);
const installed = plugins?.some((p) => p.id === plugin.id); const uninstallPluginMutation = usePromptUninstall(pluginVersion.id, pluginVersion.displayName);
const installPluginMutation = useMutation({ const installPluginMutation = useMutation({
mutationKey: ['install_plugin', plugin.id], mutationKey: ['install_plugin', pluginVersion.id],
mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null), mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null),
}); });
@@ -230,14 +245,14 @@ function InstallPluginButton({ plugin }: { plugin: PluginVersion }) {
<Button <Button
size="xs" size="xs"
variant="border" variant="border"
color={installed ? 'secondary' : 'primary'} color={installed ? 'default' : 'primary'}
className="ml-auto" className="ml-auto"
isLoading={installPluginMutation.isPending} isLoading={installPluginMutation.isPending || uninstallPluginMutation.isPending}
onClick={async () => { onClick={async () => {
if (installed) { if (installed) {
uninstallPlugin.mutate({ pluginId: plugin.id, name: plugin.displayName }); uninstallPluginMutation.mutate();
} else { } else {
installPluginMutation.mutate(plugin); installPluginMutation.mutate(pluginVersion);
} }
}} }}
> >
@@ -267,7 +282,6 @@ function InstalledPlugins() {
<TableRow> <TableRow>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell> <TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
<TableHeaderCell /> <TableHeaderCell />
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -279,3 +293,24 @@ function InstalledPlugins() {
</Table> </Table>
); );
} }
function usePromptUninstall(pluginId: string, name: string) {
return useMutation({
mutationKey: ['uninstall_plugin', pluginId],
mutationFn: async () => {
const confirmed = await showConfirmDelete({
id: 'uninstall-plugin-' + pluginId,
title: 'Uninstall Plugin',
confirmText: 'Uninstall',
description: (
<>
Permanently uninstall <InlineCode>{name}</InlineCode>?
</>
),
});
if (confirmed) {
await minPromiseMillis(uninstallPlugin(pluginId), 700);
}
},
});
}

View File

@@ -4,7 +4,6 @@ import { useRef } from 'react';
import { openSettings } from '../commands/openSettings'; import { openSettings } from '../commands/openSettings';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates'; import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useExportData } from '../hooks/useExportData'; import { useExportData } from '../hooks/useExportData';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { appInfo } from '../lib/appInfo'; import { appInfo } from '../lib/appInfo';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
import { importData } from '../lib/importData'; import { importData } from '../lib/importData';
@@ -20,8 +19,6 @@ export function SettingsDropdown() {
const checkForUpdates = useCheckForUpdates(); const checkForUpdates = useCheckForUpdates();
const { check } = useLicense(); const { check } = useLicense();
useListenToTauriEvent('settings', () => openSettings.mutate(null));
return ( return (
<Dropdown <Dropdown
ref={dropdownRef} ref={dropdownRef}

View File

@@ -6,11 +6,11 @@ import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { router } from '../lib/router'; import { router } from '../lib/router';
import { CopyIconButton } from './CopyIconButton';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { PlainInput } from './core/PlainInput'; import { PlainInput } from './core/PlainInput';
import { Separator } from './core/Separator';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from './HeadersEditor';
@@ -20,13 +20,13 @@ import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting'; import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
interface Props { interface Props {
workspaceId: string | null; workspaceId: string;
hide: () => void; hide: () => void;
tab?: WorkspaceSettingsTab; tab?: WorkspaceSettingsTab;
} }
const TAB_AUTH = 'auth'; const TAB_AUTH = 'auth';
const TAB_DESCRIPTION = 'description'; const TAB_DATA = 'data';
const TAB_HEADERS = 'headers'; const TAB_HEADERS = 'headers';
const TAB_GENERAL = 'general'; const TAB_GENERAL = 'general';
@@ -34,9 +34,9 @@ export type WorkspaceSettingsTab =
| typeof TAB_AUTH | typeof TAB_AUTH
| typeof TAB_HEADERS | typeof TAB_HEADERS
| typeof TAB_GENERAL | typeof TAB_GENERAL
| typeof TAB_DESCRIPTION; | typeof TAB_DATA;
const DEFAULT_TAB: WorkspaceSettingsTab = TAB_DESCRIPTION; const DEFAULT_TAB: WorkspaceSettingsTab = TAB_GENERAL;
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) { export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId); const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
@@ -63,25 +63,26 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
return ( return (
<Tabs <Tabs
layout="horizontal"
value={activeTab} value={activeTab}
onChangeValue={setActiveTab} onChangeValue={setActiveTab}
label="Folder Settings" label="Folder Settings"
className="px-1.5 pb-2" className="pt-2 pb-2 pl-3 pr-1"
addBorders addBorders
tabs={[ tabs={[
{ value: TAB_DESCRIPTION, label: 'Description' }, { value: TAB_GENERAL, label: 'General' },
{ {
value: TAB_GENERAL, value: TAB_DATA,
label: 'General', label: 'Directory Sync',
}, },
...authTab, ...authTab,
...headersTab, ...headersTab,
]} ]}
> >
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4"> <TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
<HttpAuthenticationEditor model={workspace} /> <HttpAuthenticationEditor model={workspace} />
</TabContent> </TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4"> <TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
<HeadersEditor <HeadersEditor
inheritedHeaders={inheritedHeaders} inheritedHeaders={inheritedHeaders}
forceUpdateKey={workspace.id} forceUpdateKey={workspace.id}
@@ -90,7 +91,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
stateKey={`headers.${workspace.id}`} stateKey={`headers.${workspace.id}`}
/> />
</TabContent> </TabContent>
<TabContent value={TAB_DESCRIPTION} className="pt-3 overflow-y-auto h-full px-4"> <TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full"> <VStack space={4} alignItems="start" className="pb-3 h-full">
<PlainInput <PlainInput
required required
@@ -111,23 +112,13 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
onChange={(description) => patchModel(workspace, { description })} onChange={(description) => patchModel(workspace, { description })}
heightMode="auto" heightMode="auto"
/> />
</VStack>
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full">
<SyncToFilesystemSetting
value={{ filePath: workspaceMeta.settingSyncDir }}
onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/>
<WorkspaceEncryptionSetting size="xs" />
<Separator className="my-4" />
<HStack alignItems="center" justifyContent="between" className="w-full"> <HStack alignItems="center" justifyContent="between" className="w-full">
<Button <Button
onClick={async () => { onClick={async () => {
const didDelete = await deleteModelWithConfirm(workspace); const didDelete = await deleteModelWithConfirm(workspace, {
confirmName: workspace.name,
});
if (didDelete) { if (didDelete) {
hide(); // Only hide if actually deleted workspace hide(); // Only hide if actually deleted workspace
await router.navigate({ to: '/' }); await router.navigate({ to: '/' });
@@ -139,10 +130,29 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
> >
Delete Workspace Delete Workspace
</Button> </Button>
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode> <InlineCode className="flex gap-1 items-center text-primary pl-2.5">
{workspaceId}
<CopyIconButton
className="opacity-70 !text-primary"
size="2xs"
iconSize="sm"
title="Copy workspace ID"
text={workspaceId}
/>
</InlineCode>
</HStack> </HStack>
</VStack> </VStack>
</TabContent> </TabContent>
<TabContent value={TAB_DATA} className="overflow-y-auto h-full px-4">
<VStack space={4} alignItems="start" className="pb-3 h-full">
<SyncToFilesystemSetting
value={{ filePath: workspaceMeta.settingSyncDir }}
onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/>
<WorkspaceEncryptionSetting size="xs" />
</VStack>
</TabContent>
</Tabs> </Tabs>
); );
} }

View File

@@ -1,33 +1,62 @@
import type { Color } from '@yaakapp-internal/plugins'; import type { Color } from '@yaakapp-internal/plugins';
import type { FormEvent } from 'react';
import { useState } from 'react';
import { Button } from './Button'; import { Button } from './Button';
import { PlainInput } from './PlainInput';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
export interface ConfirmProps { export interface ConfirmProps {
onHide: () => void; onHide: () => void;
onResult: (result: boolean) => void; onResult: (result: boolean) => void;
confirmText?: string; confirmText?: string;
requireTyping?: string;
color?: Color; 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<string>('');
const handleHide = () => { const handleHide = () => {
onResult(false); onResult(false);
onHide(); onHide();
}; };
const handleSuccess = () => { const didConfirm = !requireTyping || confirm === requireTyping;
onResult(true);
onHide(); const handleSuccess = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (didConfirm) {
onResult(true);
onHide();
}
}; };
return ( return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse"> <form className="flex flex-col" onSubmit={handleSuccess}>
<Button color={color} onClick={handleSuccess}> {requireTyping && (
{confirmText ?? 'Confirm'} <PlainInput
</Button> autoFocus
<Button onClick={handleHide} variant="border"> onChange={setConfirm}
Cancel label={
</Button> <>
</HStack> Type <strong>{requireTyping}</strong> to confirm
</>
}
/>
)}
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button type="submit" color={color} disabled={!didConfirm}>
{confirmText ?? 'Confirm'}
</Button>
<Button onClick={handleHide} variant="border">
Cancel
</Button>
</HStack>
</form>
); );
} }

View File

@@ -77,9 +77,9 @@ export function Dialog({
'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]', 'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]',
'min-h-[10rem]', 'min-h-[10rem]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]', 'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]',
size === 'sm' && 'w-[28rem]', size === 'sm' && 'w-[30rem]',
size === 'md' && 'w-[45rem]', size === 'md' && 'w-[50rem]',
size === 'lg' && 'w-[65rem]', size === 'lg' && 'w-[70rem]',
size === 'full' && 'w-[100vw] h-[100vh]', size === 'full' && 'w-[100vw] h-[100vh]',
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]', size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]',
)} )}

View File

@@ -28,7 +28,7 @@ export function Link({ href, children, noUnderline, className, ...other }: Props
rel="noopener noreferrer" rel="noopener noreferrer"
className={classNames( className={classNames(
className, className,
'pr-4 inline-flex items-center hover:underline', 'pr-4 inline-flex items-center hover:underline group',
!noUnderline && 'underline', !noUnderline && 'underline',
)} )}
onClick={(e) => { onClick={(e) => {
@@ -36,8 +36,8 @@ export function Link({ href, children, noUnderline, className, ...other }: Props
}} }}
{...other} {...other}
> >
<span>{children}</span> <span className="pr-0.5">{children}</span>
<Icon className="inline absolute right-0.5 top-[0.3em]" size="xs" icon="external_link" /> <Icon className="inline absolute right-0.5 top-[0.3em] opacity-70 group-hover:opacity-100" size="xs" icon="external_link" />
</a> </a>
); );
} }

View File

@@ -27,7 +27,7 @@ export function TableCell({ children, className }: { children: ReactNode; classN
<td <td
className={classNames( className={classNames(
className, className,
'py-2 [&:not(:first-child)]:pl-4 text-left w-0 whitespace-nowrap', 'py-2 [&:not(:first-child)]:pl-4 text-left whitespace-nowrap',
)} )}
> >
{children} {children}
@@ -57,7 +57,7 @@ export function TableHeaderCell({
className?: string; className?: string;
}) { }) {
return ( return (
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left w-0 text-text-subtle')}> <th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle')}>
{children} {children}
</th> </th>
); );

View File

@@ -84,7 +84,7 @@ export function Tabs({
tabListClassName, tabListClassName,
addBorders && '!-ml-1', addBorders && '!-ml-1',
'flex items-center hide-scrollbars mb-2', '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 ', layout === 'vertical' && 'overflow-x-auto overflow-y-visible ',
// Give space for button focus states within overflow boundary. // Give space for button focus states within overflow boundary.
layout === 'vertical' && 'py-1 -ml-5 pl-3 pr-1', layout === 'vertical' && 'py-1 -ml-5 pl-3 pr-1',
@@ -92,7 +92,7 @@ export function Tabs({
> >
<div <div
className={classNames( className={classNames(
layout === 'horizontal' && 'flex flex-col gap-1 w-full mt-1 pb-3 mb-auto', layout === 'horizontal' && 'flex flex-col gap-1 w-full pb-3 mb-auto',
layout === 'vertical' && 'flex flex-row flex-shrink-0 gap-2 w-full', layout === 'vertical' && 'flex flex-row flex-shrink-0 gap-2 w-full',
)} )}
> >
@@ -104,8 +104,11 @@ export function Tabs({
addBorders && 'border', addBorders && 'border',
isActive ? 'text-text' : 'text-text-subtle hover:text-text', isActive ? 'text-text' : 'text-text-subtle hover:text-text',
isActive && addBorders isActive && addBorders
? 'border-border-subtle bg-surface-active' ? 'border-surface-active bg-surface-active'
: 'border-transparent', : layout === 'vertical'
? 'border-border-subtle'
: 'border-transparent',
layout === 'horizontal' && 'flex justify-between',
); );
if ('options' in t) { if ('options' in t) {
@@ -121,7 +124,7 @@ export function Tabs({
> >
<button <button
onClick={isActive ? undefined : () => onChangeValue(t.value)} onClick={isActive ? undefined : () => onChangeValue(t.value)}
className={btnClassName} className={classNames(btnClassName)}
> >
{option && 'shortLabel' in option && option.shortLabel {option && 'shortLabel' in option && option.shortLabel
? option.shortLabel ? option.shortLabel

View File

@@ -1,12 +0,0 @@
import { copyToClipboard } from '../lib/copy';
import { getThemes } from '../lib/theme/themes';
import { getThemeCSS } from '../lib/theme/window';
import { useListenToTauriEvent } from './useListenToTauriEvent';
export function useGenerateThemeCss() {
useListenToTauriEvent('generate_theme_css', async () => {
const themes = await getThemes();
const themesCss = themes.themes.map(getThemeCSS).join('\n\n');
copyToClipboard(themesCss);
});
}

View File

@@ -1,48 +0,0 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { Button } from '../components/core/Button';
import { invokeCmd } from '../lib/tauri';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { showToast } from '../lib/toast';
export function useNotificationToast() {
const markRead = (id: string) => {
invokeCmd('cmd_dismiss_notification', { notificationId: id }).catch(console.error);
};
useListenToTauriEvent<{
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: () => {
markRead(payload.id)
},
action: ({ hide }) =>
actionLabel && actionUrl ? (
<Button
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
hide();
return openUrl(actionUrl);
}}
>
{actionLabel}
</Button>
) : null,
});
});
}

View File

@@ -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 <InlineCode>{name}</InlineCode>?
</>
),
});
if (confirmed) {
await invokeCmd('cmd_uninstall_plugin', { pluginId });
}
},
});
}

View File

@@ -6,28 +6,29 @@ import { showDialog } from './dialog';
type ConfirmArgs = { type ConfirmArgs = {
id: string; id: string;
} & Pick<DialogProps, 'title' | 'description'> & } & Pick<DialogProps, 'title' | 'description'> &
Pick<ConfirmProps, 'color' | 'confirmText'>; Pick<ConfirmProps, 'color' | 'confirmText' | 'requireTyping'>;
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']) => { return new Promise((onResult: ConfirmProps['onResult']) => {
showDialog({ showDialog({
id, ...extraProps,
title,
description,
hideX: true, hideX: true,
size: 'sm', size: 'sm',
disableBackdropClose: true, // Prevent accidental dismisses 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({ return showConfirm({
id, color: color ?? 'danger',
title, confirmText: confirmText ?? 'Delete',
description, ...extraProps,
color: 'danger',
confirmText: 'Delete',
}); });
} }

View File

@@ -4,8 +4,11 @@ import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from './confirm'; import { showConfirmDelete } from './confirm';
import { resolvedModelName } from './resolvedModelName'; import { resolvedModelName } from './resolvedModelName';
export async function deleteModelWithConfirm(model: AnyModel | null): Promise<boolean> { export async function deleteModelWithConfirm(
if (model == null ) { model: AnyModel | null,
options: { confirmName?: string } = {},
): Promise<boolean> {
if (model == null) {
console.warn('Tried to delete null model'); console.warn('Tried to delete null model');
return false; return false;
} }
@@ -13,6 +16,7 @@ export async function deleteModelWithConfirm(model: AnyModel | null): Promise<bo
const confirmed = await showConfirmDelete({ const confirmed = await showConfirmDelete({
id: 'delete-model-' + model.id, id: 'delete-model-' + model.id,
title: 'Delete ' + modelTypeLabel(model), title: 'Delete ' + modelTypeLabel(model),
requireTyping: options.confirmName,
description: ( description: (
<> <>
Permanently delete <InlineCode>{resolvedModelName(model)}</InlineCode>? Permanently delete <InlineCode>{resolvedModelName(model)}</InlineCode>?

View File

@@ -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<ShowToastRequest>('show_toast', (event) => {
showToast({ ...event.payload });
});
listenToTauriEvent('settings', () => openSettings.mutate(null));
// Listen for plugin events
listenToTauriEvent<InternalEvent>('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 ? (
<Button
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
hide();
return openUrl(actionUrl);
}}
>
{actionLabel}
</Button>
) : null,
});
});
}

View File

@@ -39,8 +39,7 @@ type TauriCmd =
| 'cmd_send_http_request' | 'cmd_send_http_request'
| 'cmd_show_workspace_key' | 'cmd_show_workspace_key'
| 'cmd_template_functions' | 'cmd_template_functions'
| 'cmd_template_tokens_to_string' | 'cmd_template_tokens_to_string';
| 'cmd_uninstall_plugin';
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> { export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
// console.log('RUN COMMAND', cmd, args); // console.log('RUN COMMAND', cmd, args);

View File

@@ -6,6 +6,7 @@ import { changeModelStoreWorkspace, initModelStore } from '@yaakapp-internal/mod
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { initSync } from './init/sync'; import { initSync } from './init/sync';
import { initGlobalListeners } from './lib/initGlobalListeners';
import { jotaiStore } from './lib/jotai'; import { jotaiStore } from './lib/jotai';
import { router } from './lib/router'; import { router } from './lib/router';
@@ -44,6 +45,7 @@ window.addEventListener('keydown', (e) => {
// Initialize a bunch of watchers // Initialize a bunch of watchers
initSync(); initSync();
initModelStore(jotaiStore); initModelStore(jotaiStore);
initGlobalListeners();
await changeModelStoreWorkspace(null); // Load global models await changeModelStoreWorkspace(null); // Load global models
console.log('Creating React root'); console.log('Creating React root');