mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:38:26 +02:00
Tweak workspace settings and a bunch of small things
This commit is contained in:
@@ -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 };
|
||||||
|
}
|
||||||
|
|||||||
67
plugins/auth-bearer/tests/index.test.ts
Normal file
67
plugins/auth-bearer/tests/index.test.ts
Normal 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' }] });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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', {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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} />
|
||||||
},
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>?
|
||||||
|
|||||||
77
src-web/lib/initGlobalListeners.tsx
Normal file
77
src-web/lib/initGlobalListeners.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user