mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-18 07:23:51 +01:00
Initial settings implementation
This commit is contained in:
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO settings (id)\n VALUES ('default')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88"
|
||||
}
|
||||
56
src-tauri/.sqlx/query-a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067.json
generated
Normal file
56
src-tauri/.sqlx/query-a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n follow_redirects,\n validate_certificates,\n theme\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "follow_redirects",
|
||||
"ordinal": 4,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "validate_certificates",
|
||||
"ordinal": 5,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "theme",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127.json
generated
Normal file
12
src-tauri/.sqlx/query-daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n follow_redirects,\n validate_certificates,\n theme\n ) = (?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127"
|
||||
}
|
||||
11
src-tauri/migrations/20240111221224_settings.sql
Normal file
11
src-tauri/migrations/20240111221224_settings.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE settings
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'settings' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
follow_redirects BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
validate_certificates BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
theme TEXT DEFAULT 'system' NOT NULL
|
||||
);
|
||||
@@ -521,6 +521,31 @@ async fn list_environments(
|
||||
Ok(environments)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_settings(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_or_create_settings(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_settings(
|
||||
settings: models::Settings,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_settings = models::update_settings(pool, settings)
|
||||
.await
|
||||
.expect("Failed to update settings");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_folder(
|
||||
id: &str,
|
||||
@@ -731,6 +756,7 @@ fn main() {
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_request,
|
||||
get_settings,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
@@ -747,6 +773,7 @@ fn main() {
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
update_settings,
|
||||
update_workspace,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
|
||||
@@ -3,11 +3,25 @@ use std::fs;
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
// Settings
|
||||
pub validate_certificates: bool,
|
||||
pub follow_redirects: bool,
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
@@ -192,7 +206,11 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<String> {
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Option<String> {
|
||||
let kv = get_key_value(namespace, key, pool).await?;
|
||||
let result = serde_json::from_str(&kv.value);
|
||||
match result {
|
||||
@@ -283,6 +301,64 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Settings,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
theme
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
let existing = get_settings(pool).await;
|
||||
if let Ok(s) = existing {
|
||||
Ok(s)
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO settings (id)
|
||||
VALUES ('default')
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_settings(
|
||||
pool: &Pool<Sqlite>,
|
||||
settings: Settings,
|
||||
) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
theme
|
||||
) = (?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.follow_redirects,
|
||||
settings.validate_certificates,
|
||||
settings.theme,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
|
||||
@@ -34,12 +34,19 @@ pub async fn actually_send_request(
|
||||
url_string = format!("http://{}", url_string);
|
||||
}
|
||||
|
||||
let settings = models::get_or_create_settings(pool)
|
||||
.await
|
||||
.expect("Failed to get settings");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none()) // TODO: Handle redirect manually
|
||||
.danger_accept_invalid_certs(false) // TODO: Make this configurable
|
||||
.redirect(match settings.follow_redirects {
|
||||
true => Policy::limited(10), // TODO: Handle redirects natively
|
||||
false => Policy::none(),
|
||||
})
|
||||
.danger_accept_invalid_certs(!settings.validate_certificates)
|
||||
.connection_verbose(true) // TODO: Capture this log somehow
|
||||
.tls_info(true) // TODO: Capture this log somehow
|
||||
// .use_rustls_tls() // TODO: Make this configurable
|
||||
.tls_info(true)
|
||||
// .use_rustls_tls() // TODO: Make this configurable (maybe)
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
|
||||
@@ -117,7 +124,9 @@ pub async fn actually_send_request(
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for p in request.url_parameters.0 {
|
||||
if !p.enabled || p.name.is_empty() { continue; }
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
query_params.push((
|
||||
render::render(&p.name, &workspace, environment_ref),
|
||||
render::render(&p.value, &workspace, environment_ref),
|
||||
@@ -131,18 +140,38 @@ pub async fn actually_send_request(
|
||||
let request_body = request.body.0;
|
||||
|
||||
if request_body.contains_key("text") {
|
||||
let raw_text = request_body.get("text").unwrap_or(empty_string).as_str().unwrap_or("");
|
||||
let raw_text = request_body
|
||||
.get("text")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let body = render::render(raw_text, &workspace, environment_ref);
|
||||
request_builder = request_builder.body(body);
|
||||
} else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") {
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
let mut form_params = Vec::new();
|
||||
let form = request_body.get("form");
|
||||
if let Some(f) = form {
|
||||
for p in f.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
form_params.push((
|
||||
render::render(name, &workspace, environment_ref),
|
||||
render::render(value, &workspace, environment_ref),
|
||||
@@ -154,17 +183,41 @@ pub async fn actually_send_request(
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = p.get("file").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let file = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
multipart_form = multipart_form.part(
|
||||
render::render(name, &workspace, environment_ref),
|
||||
match !file.is_empty() {
|
||||
true => multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?),
|
||||
false => multipart::Part::text(render::render(value, &workspace, environment_ref)),
|
||||
true => {
|
||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
||||
}
|
||||
false => multipart::Part::text(render::render(
|
||||
value,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -243,6 +296,6 @@ pub async fn actually_send_request(
|
||||
Err(e) => {
|
||||
println!("Yo: {}", e);
|
||||
response_err(response, e.to_string(), app_handle, pool).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
src-web/components/SettingsDialog.tsx
Normal file
33
src-web/components/SettingsDialog.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
|
||||
if (settings == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<Checkbox
|
||||
checked={settings.validateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(validateCertificates) =>
|
||||
updateSettings.mutateAsync({ ...settings, validateCertificates })
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.followRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(followRedirects) => updateSettings.mutateAsync({ ...settings, followRedirects })}
|
||||
/>
|
||||
<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
@@ -61,16 +62,11 @@ export function SettingsDropdown() {
|
||||
leftSlot: <Icon icon="upload" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: 'Toggle Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotkeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
@@ -79,7 +75,20 @@ export function SettingsDropdown() {
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotkeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
@@ -8,33 +8,47 @@ interface Props {
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(!checked);
|
||||
}, [onChange, checked]);
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
||||
return (
|
||||
<button
|
||||
role="checkbox"
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
|
||||
'focus:border-focus',
|
||||
'disabled:opacity-disabled',
|
||||
checked && 'bg-gray-200/10',
|
||||
// Remove focus style
|
||||
'outline-none',
|
||||
)}
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(className, disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
<div className="relative flex">
|
||||
<input
|
||||
aria-hidden
|
||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* role="checkbox"*/}
|
||||
{/* aria-checked={checked ? 'true' : 'false'}*/}
|
||||
{/* disabled={disabled}*/}
|
||||
{/* onClick={handleClick}*/}
|
||||
{/* title={title}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* className,*/}
|
||||
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
|
||||
{/* 'focus:border-focus',*/}
|
||||
{/* 'disabled:opacity-disabled',*/}
|
||||
{/* checked && 'bg-gray-200/10',*/}
|
||||
{/* // Remove focus style*/}
|
||||
{/* 'outline-none',*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{/*</button>*/}
|
||||
{!hideLabel && title}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -479,11 +479,6 @@ interface MenuItemHotKeyProps {
|
||||
}
|
||||
|
||||
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
|
||||
if (action) {
|
||||
console.log('MENU ITEM HOTKEY', action, item);
|
||||
}
|
||||
useHotKey(action ?? null, () => {
|
||||
onSelect(item);
|
||||
});
|
||||
useHotKey(action ?? null, () => onSelect(item));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -290,13 +290,29 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (onChange && update.docChanged) {
|
||||
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
|
||||
// changing pages (one request to another in header editor)
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
// Make sure document has changed, ensuring user events like selections don't count.
|
||||
if (viewUpdate.docChanged) {
|
||||
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
|
||||
for (const transaction of viewUpdate.transactions) {
|
||||
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
|
||||
const userEventType = transaction.annotation(Transaction.userEvent);
|
||||
if (userEventType) return userEventType;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
className = '',
|
||||
|
||||
@@ -335,7 +335,8 @@ const FormRow = memo(function FormRow({
|
||||
<span className="w-3" />
|
||||
)}
|
||||
<Checkbox
|
||||
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
|
||||
hideLabel
|
||||
title={pairContainer.pair.enabled ? 'Disable item' : 'Enable item'}
|
||||
disabled={isLast}
|
||||
checked={isLast ? false : !!pairContainer.pair.enabled}
|
||||
className={classNames('mr-2', isLast && '!opacity-disabled')}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'form';
|
||||
as?: ComponentType | 'ul' | 'label' | 'form';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
|
||||
@@ -11,7 +11,8 @@ export type HotkeyAction =
|
||||
| 'sidebar.focus'
|
||||
| 'urlBar.focus'
|
||||
| 'environmentEditor.toggle'
|
||||
| 'hotkeys.showHelp';
|
||||
| 'hotkeys.showHelp'
|
||||
| 'settings.show';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
@@ -22,6 +23,7 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+/'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
@@ -32,7 +34,8 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'sidebar.focus': 'Focus Sidebar',
|
||||
'urlBar.focus': 'Focus URL',
|
||||
'environmentEditor.toggle': 'Edit Environments',
|
||||
'hotkeys.showHelp': 'Show Hotkeys',
|
||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||
'settings.show': 'Open Settings',
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
18
src-web/hooks/useSettings.ts
Normal file
18
src-web/hooks/useSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Settings } from '../lib/models';
|
||||
|
||||
export function settingsQueryKey() {
|
||||
return ['settings'];
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return (
|
||||
useQuery({
|
||||
queryKey: settingsQueryKey(),
|
||||
queryFn: async () => {
|
||||
return (await invoke('get_settings')) as Settings;
|
||||
},
|
||||
}).data ?? undefined
|
||||
);
|
||||
}
|
||||
18
src-web/hooks/useUpdateSettings.ts
Normal file
18
src-web/hooks/useUpdateSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, Settings } from '../lib/models';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { settingsQueryKey } from './useSettings';
|
||||
|
||||
export function useUpdateSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, Settings>({
|
||||
mutationFn: async (settings) => {
|
||||
await invoke('update_settings', { settings });
|
||||
},
|
||||
onMutate: async (settings) => {
|
||||
queryClient.setQueryData<Settings>(settingsQueryKey(), settings);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
export const AUTH_TYPE_BEARER = 'bearer';
|
||||
|
||||
export type Model = Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
|
||||
export interface BaseModel {
|
||||
readonly id: string;
|
||||
@@ -17,6 +17,13 @@ export interface BaseModel {
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Settings extends BaseModel {
|
||||
readonly model: 'settings';
|
||||
validateCertificates: boolean;
|
||||
followRedirects: boolean;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface Workspace extends BaseModel {
|
||||
readonly model: 'workspace';
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user