mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:14:03 +01:00
New sidebar and folder view (#263)
This commit is contained in:
@@ -1007,6 +1007,35 @@ async fn cmd_save_response<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_send_folder<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
environment_id: Option<String>,
|
||||
cookie_jar_id: Option<String>,
|
||||
folder_id: &str,
|
||||
) -> YaakResult<()> {
|
||||
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
|
||||
for request in requests {
|
||||
let app_handle = app_handle.clone();
|
||||
let window = window.clone();
|
||||
let environment_id = environment_id.clone();
|
||||
let cookie_jar_id = cookie_jar_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = cmd_send_http_request(
|
||||
app_handle,
|
||||
window,
|
||||
environment_id.as_deref(),
|
||||
cookie_jar_id.as_deref(),
|
||||
request,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_send_http_request<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
@@ -1386,6 +1415,7 @@ pub fn run() {
|
||||
cmd_save_response,
|
||||
cmd_send_ephemeral_request,
|
||||
cmd_send_http_request,
|
||||
cmd_send_folder,
|
||||
cmd_template_functions,
|
||||
cmd_template_tokens_to_string,
|
||||
//
|
||||
@@ -1511,14 +1541,17 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
warn!("Failed to handle plugin event: {e:?}");
|
||||
let _ = app_handle.emit("show_toast", InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
||||
message: e.to_string(),
|
||||
color: Some(Color::Danger),
|
||||
icon: None,
|
||||
timeout: Some(30000),
|
||||
}));
|
||||
let _ = app_handle.emit(
|
||||
"show_toast",
|
||||
InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
||||
message: e.to_string(),
|
||||
color: Some(Color::Danger),
|
||||
icon: None,
|
||||
timeout: Some(30000),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let plugin_manager: State<'_, PluginManager> = app_handle.state();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::Result;
|
||||
use crate::window_menu::app_menu;
|
||||
use log::{info, warn};
|
||||
use rand::random;
|
||||
@@ -6,7 +7,6 @@ use tauri::{
|
||||
};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::mpsc;
|
||||
use crate::error::Result;
|
||||
|
||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||
@@ -49,7 +49,6 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
.resizable(true)
|
||||
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
|
||||
.fullscreen(false)
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
|
||||
|
||||
if let Some(key) = config.data_dir_key {
|
||||
@@ -216,10 +215,10 @@ pub(crate) fn create_child_window(
|
||||
) -> Result<WebviewWindow> {
|
||||
let app_handle = parent_window.app_handle();
|
||||
let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
|
||||
let scale_factor = parent_window.scale_factor().unwrap();
|
||||
let scale_factor = parent_window.scale_factor()?;
|
||||
|
||||
let current_pos = parent_window.inner_position().unwrap().to_logical::<f64>(scale_factor);
|
||||
let current_size = parent_window.inner_size().unwrap().to_logical::<f64>(scale_factor);
|
||||
let current_pos = parent_window.inner_position()?.to_logical::<f64>(scale_factor);
|
||||
let current_size = parent_window.inner_size()?.to_logical::<f64>(scale_factor);
|
||||
|
||||
// Position the new window in the middle of the parent
|
||||
let position = (
|
||||
|
||||
@@ -81,11 +81,12 @@ export function getAnyModel(id: string): AnyModel | null {
|
||||
}
|
||||
|
||||
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
|
||||
modelType: M | M[],
|
||||
modelType: M | ReadonlyArray<M>,
|
||||
id: string,
|
||||
): T | null {
|
||||
let data = mustStore().get(modelStoreDataAtom);
|
||||
for (const t of Array.isArray(modelType) ? modelType : [modelType]) {
|
||||
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
|
||||
for (const t of types) {
|
||||
let v = data[t][id];
|
||||
if (v?.model === t) return v as T;
|
||||
}
|
||||
@@ -139,7 +140,7 @@ export async function deleteModel<M extends AnyModel['model'], T extends Extract
|
||||
export function duplicateModelById<
|
||||
M extends AnyModel['model'],
|
||||
T extends ExtractModel<AnyModel, M>,
|
||||
>(modelType: M | M[], id: string) {
|
||||
>(modelType: M | ReadonlyArray<M>, id: string) {
|
||||
let model = getModel<M, T>(modelType, id);
|
||||
return duplicateModel(model);
|
||||
}
|
||||
@@ -150,6 +151,8 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
|
||||
if (model == null) {
|
||||
throw new Error('Failed to delete null model');
|
||||
}
|
||||
if ('sortPriority' in model) model.sortPriority = model.sortPriority + 0.0001;
|
||||
|
||||
return invoke<string>('plugin:yaak-models|duplicate', { model });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||
use crate::util::UpdateSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -89,4 +89,18 @@ impl<'a> DbContext<'a> {
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
pub fn list_http_requests_for_folder_recursive(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
) -> Result<Vec<HttpRequest>> {
|
||||
let mut children = Vec::new();
|
||||
for m in self.find_many::<Folder>(FolderIden::FolderId, folder_id, None)? {
|
||||
children.extend(self.list_http_requests_for_folder_recursive(&m.id)?);
|
||||
}
|
||||
for m in self.find_many::<HttpRequest>(FolderIden::FolderId, folder_id, None)? {
|
||||
children.push(m);
|
||||
}
|
||||
Ok(children)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
export * from './bindings/parser';
|
||||
import { Tokens } from './bindings/parser';
|
||||
import { parse_template } from './pkg';
|
||||
import { escape_template, parse_template, unescape_template } from './pkg';
|
||||
|
||||
export function parseTemplate(template: string) {
|
||||
return parse_template(template) as Tokens;
|
||||
}
|
||||
|
||||
export function escapeTemplate(template: string) {
|
||||
return escape_template(template) as string;
|
||||
}
|
||||
|
||||
export function unescapeTemplate(template: string) {
|
||||
return unescape_template(template) as string;
|
||||
}
|
||||
|
||||
166
src-tauri/yaak-templates/src/escape.rs
Normal file
166
src-tauri/yaak-templates/src/escape.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
pub fn escape_template(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
// Check if we're at "${["
|
||||
if i + 2 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' && chars[i + 2] == '[' {
|
||||
// Count preceding backslashes
|
||||
let mut backslash_count = 0;
|
||||
let mut j = i;
|
||||
while j > 0 && chars[j - 1] == '\\' {
|
||||
backslash_count += 1;
|
||||
j -= 1;
|
||||
}
|
||||
|
||||
// If odd number of backslashes, the $ is escaped
|
||||
// If even number (including 0), the $ is not escaped
|
||||
let already_escaped = backslash_count % 2 == 1;
|
||||
|
||||
if already_escaped {
|
||||
// Already escaped, just add the current character
|
||||
result.push(chars[i]);
|
||||
} else {
|
||||
// Not escaped, add backslash before $
|
||||
result.push('\\');
|
||||
result.push(chars[i]);
|
||||
}
|
||||
} else {
|
||||
result.push(chars[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn unescape_template(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
// Check if we're at "\${["
|
||||
if i + 3 < chars.len()
|
||||
&& chars[i] == '\\'
|
||||
&& chars[i + 1] == '$'
|
||||
&& chars[i + 2] == '{'
|
||||
&& chars[i + 3] == '['
|
||||
{
|
||||
// Count preceding backslashes (before the current backslash)
|
||||
let mut backslash_count = 0;
|
||||
let mut j = i;
|
||||
while j > 0 && chars[j - 1] == '\\' {
|
||||
backslash_count += 1;
|
||||
j -= 1;
|
||||
}
|
||||
|
||||
// If even number of preceding backslashes, this backslash escapes the $
|
||||
// If odd number, this backslash is itself escaped
|
||||
let escapes_dollar = backslash_count % 2 == 0;
|
||||
|
||||
if escapes_dollar {
|
||||
// Skip the backslash, just add the $
|
||||
result.push(chars[i + 1]);
|
||||
i += 1; // Skip the backslash
|
||||
} else {
|
||||
// This backslash is escaped itself, keep it
|
||||
result.push(chars[i]);
|
||||
}
|
||||
} else {
|
||||
result.push(chars[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::escape::{escape_template, unescape_template};
|
||||
|
||||
#[test]
|
||||
fn test_escape_simple() {
|
||||
let input = r#"${[foo]}"#;
|
||||
let expected = r#"\${[foo]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_already_escaped() {
|
||||
let input = r#"\${[bar]}"#;
|
||||
let expected = r#"\${[bar]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_backslash() {
|
||||
let input = r#"\\${[bar]}"#;
|
||||
let expected = r#"\\\${[bar]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_with_surrounding_text() {
|
||||
let input = r#"text ${[var]} more"#;
|
||||
let expected = r#"text \${[var]} more"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_already_escaped() {
|
||||
let input = r#"already \${[escaped]}"#;
|
||||
let expected = r#"already \${[escaped]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_occurrences() {
|
||||
let input = r#"${[one]} and ${[two]}"#;
|
||||
let expected = r#"\${[one]} and \${[two]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_escaped_and_unescaped() {
|
||||
let input = r#"mixed \${[esc]} and ${[unesc]}"#;
|
||||
let expected = r#"mixed \${[esc]} and \${[unesc]}"#;
|
||||
assert_eq!(escape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_simple() {
|
||||
let input = r#"\${[foo]}"#;
|
||||
let expected = r#"${[foo]}"#;
|
||||
assert_eq!(unescape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_with_text() {
|
||||
let input = r#"text \${[var]} more"#;
|
||||
let expected = r#"text ${[var]} more"#;
|
||||
assert_eq!(unescape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_multiple() {
|
||||
let input = r#"\${[one]} and \${[two]}"#;
|
||||
let expected = r#"${[one]} and ${[two]}"#;
|
||||
assert_eq!(unescape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_double_backslash() {
|
||||
let input = r#"\\\${[bar]}"#;
|
||||
let expected = r#"\\${[bar]}"#;
|
||||
assert_eq!(unescape_template(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_plain_text() {
|
||||
let input = r#"${[foo]}"#;
|
||||
let expected = r#"${[foo]}"#;
|
||||
assert_eq!(unescape_template(input), expected);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod error;
|
||||
pub mod escape;
|
||||
pub mod format;
|
||||
pub mod parser;
|
||||
pub mod renderer;
|
||||
pub mod error;
|
||||
pub mod wasm;
|
||||
|
||||
pub use parser::*;
|
||||
|
||||
@@ -170,7 +170,13 @@ impl Parser {
|
||||
let start_pos = self.pos;
|
||||
|
||||
while self.pos < self.chars.len() {
|
||||
if self.match_str("${[") {
|
||||
if self.match_str(r#"\\"#) {
|
||||
// Skip double-escapes so we don't trigger our own escapes in the next case
|
||||
self.curr_text += r#"\\"#;
|
||||
} else if self.match_str(r#"\${["#) {
|
||||
// Unescaped template syntax so we treat it as a string
|
||||
self.curr_text += "${[";
|
||||
} else if self.match_str("${[") {
|
||||
let start_curr = self.pos;
|
||||
if let Some(t) = self.parse_tag()? {
|
||||
self.push_token(t);
|
||||
@@ -490,6 +496,39 @@ mod tests {
|
||||
use crate::error::Result;
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn escaped() -> Result<()> {
|
||||
let mut p = Parser::new(r#"\${[ foo ]}"#);
|
||||
assert_eq!(
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Raw {
|
||||
text: "${[ foo ]}".to_string()
|
||||
},
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_tricky() -> Result<()> {
|
||||
let mut p = Parser::new(r#"\\${[ foo ]}"#);
|
||||
assert_eq!(
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Raw {
|
||||
text: r#"\\"#.to_string()
|
||||
},
|
||||
Token::Tag {
|
||||
val: Val::Var { name: "foo".into() }
|
||||
},
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_simple() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo ]}");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error::Result;
|
||||
use crate::Parser;
|
||||
use crate::{escape, Parser};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
@@ -7,4 +7,16 @@ use wasm_bindgen::JsValue;
|
||||
pub fn parse_template(template: &str) -> Result<JsValue> {
|
||||
let tokens = Parser::new(template).parse()?;
|
||||
Ok(serde_wasm_bindgen::to_value(&tokens).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn escape_template(template: &str) -> Result<JsValue> {
|
||||
let escaped = escape::escape_template(template);
|
||||
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn unescape_template(template: &str) -> Result<JsValue> {
|
||||
let escaped = escape::unescape_template(template);
|
||||
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user