mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 15:47:47 +01:00
Compare commits
15 Commits
v2025.2.0-
...
v2025.2.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736025b12f | ||
|
|
cb9e9a67a3 | ||
|
|
93c323458f | ||
|
|
6f8c03d8c1 | ||
|
|
afd4228fcf | ||
|
|
d478e5a12e | ||
|
|
0db9ebe67d | ||
|
|
80ea5e6b91 | ||
|
|
cb773babe1 | ||
|
|
b9ed554aca | ||
|
|
f42f3d0e27 | ||
|
|
93ba5b6e5c | ||
|
|
be11d5968e | ||
|
|
0828599e4f | ||
|
|
f47d22c395 |
@@ -60,3 +60,10 @@ Run the app to apply the migrations.
|
||||
If nothing happens, try `cargo clean` and run the app again.
|
||||
|
||||
_Note: Development builds use a separate database location from production builds._
|
||||
|
||||
## Lezer Grammer Generation
|
||||
|
||||
```sh
|
||||
# Example
|
||||
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
|
||||
```
|
||||
|
||||
64
package-lock.json
generated
64
package-lock.json
generated
@@ -2772,9 +2772,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz",
|
||||
"integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.3.0.tgz",
|
||||
"integrity": "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -2981,63 +2981,63 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-clipboard-manager": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
|
||||
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.2.2.tgz",
|
||||
"integrity": "sha512-bZvDLMqfcNmsw7Ag8I49jlaCjdpDvvlJHnpp6P+Gg/3xtpSERdwlDxm7cKGbs2mj46dsw4AuG3RoAgcpwgioUA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0.tgz",
|
||||
"integrity": "sha512-ApNkejXP2jpPBSifznPPcHTXxu9/YaRW+eJ+8+nYwqp0lLUtebFHG4QhxitM43wwReHE81WAV1DQ/b+2VBftOA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.0.tgz",
|
||||
"integrity": "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-fs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0.tgz",
|
||||
"integrity": "sha512-BNEeQQ5aH8J5SwYuWgRszVyItsmquRuzK2QRkVj8Z0sCsLnSvJFYI3JHRzzr3ltZGq1nMPtblrlZzuKqVzRawA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.0.tgz",
|
||||
"integrity": "sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0.tgz",
|
||||
"integrity": "sha512-C+NII9vzswqnOQE8k7oRtnaw0z5TZsMmnirRhXkCKDEhQQH9841Us/PC1WHtGiAaJ8za1A1JB2xXndEq/47X/w==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
|
||||
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.2.tgz",
|
||||
"integrity": "sha512-E/XIHKqGV+FT8PDdkfMETmgPUxcR79Rk8USuzbadD/ZdvsKCfQR5q+6rpZC9zEnG2wzi9lVQM4D3xwrtGGIB8A==",
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
|
||||
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-os": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
|
||||
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.2.1.tgz",
|
||||
"integrity": "sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz",
|
||||
"integrity": "sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz",
|
||||
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
@@ -15699,14 +15699,14 @@
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-router": "^1.111.3",
|
||||
"@tanstack/react-virtual": "^3.13.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||
"@tauri-apps/plugin-log": "^2.0.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.2",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@tauri-apps/api": "^2.3.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-log": "^2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
|
||||
520
src-tauri/Cargo.lock
generated
520
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ strip = true # Automatically strip symbols from the binary.
|
||||
cargo-clippy = []
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.5", features = [] }
|
||||
tauri-build = { version = "2.0.6", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
@@ -58,15 +58,15 @@ rustls-platform-verifier = "0.5.0"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||
tauri-plugin-clipboard-manager = "2.2.1"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-log = { version = "2.2.1", features = ["colored"] }
|
||||
tauri-plugin-opener = "2.2.5"
|
||||
tauri-plugin-os = "2.2.0"
|
||||
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-single-instance = "2.2.1"
|
||||
tauri-plugin-updater = "2.5.0"
|
||||
tauri-plugin-single-instance = "2.2.2"
|
||||
tauri-plugin-updater = "2.6.1"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
tokio = { version = "1.43.0", features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::http_request::send_http_request;
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_grpc_request, render_template};
|
||||
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||
use crate::uri_scheme::handle_uri_scheme;
|
||||
use error::Result as YaakResult;
|
||||
use eventsource_client::{EventParser, SSE};
|
||||
use log::{debug, error, warn};
|
||||
@@ -78,6 +79,7 @@ mod render;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod tauri_plugin_mac_window;
|
||||
mod updates;
|
||||
mod uri_scheme;
|
||||
mod window;
|
||||
mod window_menu;
|
||||
|
||||
@@ -114,8 +116,8 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_parse_template(template: &str) -> Result<Tokens, String> {
|
||||
Ok(Parser::new(template).parse())
|
||||
async fn cmd_parse_template(template: &str) -> YaakResult<Tokens> {
|
||||
Ok(Parser::new(template).parse()?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1905,10 +1907,7 @@ pub fn run() {
|
||||
cmd_update_workspace_meta,
|
||||
cmd_write_file_dev,
|
||||
])
|
||||
.register_uri_scheme_protocol("yaak", |_app, _req| {
|
||||
debug!("Testing yaak protocol");
|
||||
tauri::http::Response::builder().body("Success".as_bytes().to_vec()).unwrap()
|
||||
})
|
||||
.register_uri_scheme_protocol("yaak", handle_uri_scheme)
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application")
|
||||
.run(|app_handle, event| {
|
||||
|
||||
@@ -93,11 +93,12 @@ impl YaakNotifier {
|
||||
let seen = get_kv(window).await?;
|
||||
if seen.contains(¬ification.id) || (age > Duration::days(2)) {
|
||||
debug!("Already seen notification {}", notification.id);
|
||||
return Ok(());
|
||||
continue;
|
||||
}
|
||||
debug!("Got notification {:?}", notification);
|
||||
|
||||
let _ = window.emit_to(window.label(), "notification", notification.clone());
|
||||
break; // Only show one notification
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
25
src-tauri/src/uri_scheme.rs
Normal file
25
src-tauri/src/uri_scheme.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use log::{info, warn};
|
||||
use tauri::{Manager, Runtime, UriSchemeContext};
|
||||
|
||||
pub(crate) fn handle_uri_scheme<R: Runtime>(
|
||||
a: UriSchemeContext<R>,
|
||||
req: http::Request<Vec<u8>>,
|
||||
) -> http::Response<Vec<u8>> {
|
||||
println!("------------- Yaak URI scheme invoked!");
|
||||
let uri = req.uri();
|
||||
let window = a
|
||||
.app_handle()
|
||||
.get_webview_window(a.webview_label())
|
||||
.expect("Failed to get webview window for URI scheme event");
|
||||
info!("Yaak URI scheme invoked with {uri:?} {window:?}");
|
||||
|
||||
let path = uri.path();
|
||||
if path == "/data/import" {
|
||||
warn!("TODO: import data")
|
||||
} else if path == "/plugins/install" {
|
||||
warn!("TODO: install plugin")
|
||||
}
|
||||
|
||||
let msg = format!("No handler found for {path}");
|
||||
tauri::http::Response::builder().status(404).body(msg.as_bytes().to_vec()).unwrap()
|
||||
}
|
||||
@@ -117,9 +117,9 @@ pub fn git_commit(dir: &Path, message: &str) -> Result<()> {
|
||||
let tree = repo.find_tree(tree_oid)?;
|
||||
|
||||
// Make the signature
|
||||
let config = git2::Config::open_default()?.snapshot()?;
|
||||
let name = config.get_str("user.name").unwrap_or("Change Me");
|
||||
let email = config.get_str("user.email").unwrap_or("change_me@example.com");
|
||||
let config = repo.config()?.snapshot()?;
|
||||
let name = config.get_str("user.name").unwrap_or("Unknown");
|
||||
let email = config.get_str("user.email")?;
|
||||
let sig = git2::Signature::now(name, email)?;
|
||||
|
||||
// Get the current HEAD commit (if it exists)
|
||||
|
||||
@@ -5,9 +5,10 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.39.3", features = ["macros", "rt"] }
|
||||
ts-rs = { version = "10.0.0" }
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use crate::error::Error::RenderError;
|
||||
use crate::error::Result;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use ts_rs::TS;
|
||||
@@ -43,7 +47,13 @@ pub enum Val {
|
||||
impl Display for Val {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str = match self {
|
||||
Val::Str { text } => format!("'{}'", text.to_string().replace("'", "\'")),
|
||||
Val::Str { text } => {
|
||||
if text.chars().all(|c| c.is_alphanumeric() || c == ' ' || c == '_' || c == '_') {
|
||||
format!("'{}'", text)
|
||||
} else {
|
||||
format!("b64'{}'", BASE64_URL_SAFE_NO_PAD.encode(text))
|
||||
}
|
||||
}
|
||||
Val::Var { name } => name.to_string(),
|
||||
Val::Bool { value } => value.to_string(),
|
||||
Val::Fn { name, args } => {
|
||||
@@ -108,13 +118,13 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> Tokens {
|
||||
pub fn parse(&mut self) -> Result<Tokens> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
while self.pos < self.chars.len() {
|
||||
if self.match_str("${[") {
|
||||
let start_curr = self.pos;
|
||||
if let Some(t) = self.parse_tag() {
|
||||
if let Some(t) = self.parse_tag()? {
|
||||
self.push_token(t);
|
||||
} else {
|
||||
self.pos = start_curr;
|
||||
@@ -131,29 +141,29 @@ impl Parser {
|
||||
}
|
||||
|
||||
self.push_token(Token::Eof);
|
||||
Tokens {
|
||||
Ok(Tokens {
|
||||
tokens: self.tokens.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_tag(&mut self) -> Option<Token> {
|
||||
fn parse_tag(&mut self) -> Result<Option<Token>> {
|
||||
// Parse up to first identifier
|
||||
// ${[ my_var...
|
||||
self.skip_whitespace();
|
||||
|
||||
let val = match self.parse_value() {
|
||||
let val = match self.parse_value()? {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Parse to closing tag
|
||||
// ${[ my_var(a, b, c) ]}
|
||||
self.skip_whitespace();
|
||||
if !self.match_str("]}") {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Some(Token::Tag { val })
|
||||
Ok(Some(Token::Tag { val }))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -167,9 +177,11 @@ impl Parser {
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_value(&mut self) -> Option<Val> {
|
||||
if let Some((name, args)) = self.parse_fn() {
|
||||
fn parse_value(&mut self) -> Result<Option<Val>> {
|
||||
let v = if let Some((name, args)) = self.parse_fn()? {
|
||||
Some(Val::Fn { name, args })
|
||||
} else if let Some(v) = self.parse_string()? {
|
||||
Some(Val::Str { text: v })
|
||||
} else if let Some(v) = self.parse_ident() {
|
||||
if v == "null" {
|
||||
Some(Val::Null)
|
||||
@@ -180,38 +192,38 @@ impl Parser {
|
||||
} else {
|
||||
Some(Val::Var { name: v })
|
||||
}
|
||||
} else if let Some(v) = self.parse_string() {
|
||||
Some(Val::Str { text: v })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
|
||||
fn parse_fn(&mut self) -> Result<Option<(String, Vec<FnArg>)>> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let name = match self.parse_fn_name() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let args = match self.parse_fn_args() {
|
||||
let args = match self.parse_fn_args()? {
|
||||
Some(args) => args,
|
||||
None => {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
Some((name, args))
|
||||
Ok(Some((name, args)))
|
||||
}
|
||||
|
||||
fn parse_fn_args(&mut self) -> Option<Vec<FnArg>> {
|
||||
fn parse_fn_args(&mut self) -> Result<Option<Vec<FnArg>>> {
|
||||
if !self.match_str("(") {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let start_pos = self.pos;
|
||||
@@ -221,7 +233,7 @@ impl Parser {
|
||||
// Fn closed immediately
|
||||
self.skip_whitespace();
|
||||
if self.match_str(")") {
|
||||
return Some(args);
|
||||
return Ok(Some(args));
|
||||
}
|
||||
|
||||
while self.pos < self.chars.len() {
|
||||
@@ -231,7 +243,7 @@ impl Parser {
|
||||
self.skip_whitespace();
|
||||
self.match_str("=");
|
||||
self.skip_whitespace();
|
||||
let value = self.parse_value();
|
||||
let value = self.parse_value()?;
|
||||
self.skip_whitespace();
|
||||
|
||||
if let (Some(name), Some(value)) = (name.clone(), value.clone()) {
|
||||
@@ -239,7 +251,7 @@ impl Parser {
|
||||
} else {
|
||||
// Didn't find valid thing, so return
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if self.match_str(")") {
|
||||
@@ -251,7 +263,7 @@ impl Parser {
|
||||
// If we don't find a comma, that's bad
|
||||
if !args.is_empty() && !self.match_str(",") {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
@@ -259,7 +271,7 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
Some(args)
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
fn parse_ident(&mut self) -> Option<String> {
|
||||
@@ -319,12 +331,17 @@ impl Parser {
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> Option<String> {
|
||||
fn parse_string(&mut self) -> Result<Option<String>> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let mut text = String::new();
|
||||
if !self.match_str("'") {
|
||||
return None;
|
||||
let mut is_b64 = false;
|
||||
if self.match_str("b64'") {
|
||||
is_b64 = true;
|
||||
} else if self.match_str("'") {
|
||||
// Nothing
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut found_closing = false;
|
||||
@@ -350,10 +367,21 @@ impl Parser {
|
||||
|
||||
if !found_closing {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Some(text)
|
||||
let final_text = if is_b64 {
|
||||
let decoded = BASE64_URL_SAFE_NO_PAD
|
||||
.decode(text.clone())
|
||||
.map_err(|_| RenderError(format!("Failed to decode string {text}")))?;
|
||||
let decoded = String::from_utf8(decoded)
|
||||
.map_err(|_| RenderError(format!("Failed to decode utf8 string {text}")))?;
|
||||
decoded
|
||||
} else {
|
||||
text
|
||||
};
|
||||
|
||||
Ok(Some(final_text))
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
@@ -410,14 +438,15 @@ impl Parser {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::Result;
|
||||
use crate::Val::Null;
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn var_simple() {
|
||||
fn var_simple() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Var { name: "foo".into() }
|
||||
@@ -425,13 +454,14 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_dashes() {
|
||||
fn var_dashes() -> Result<()> {
|
||||
let mut p = Parser::new("${[ a-b ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Var { name: "a-b".into() }
|
||||
@@ -439,13 +469,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_underscores() {
|
||||
fn var_underscores() -> Result<()> {
|
||||
let mut p = Parser::new("${[ a_b ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Var { name: "a_b".into() }
|
||||
@@ -453,13 +485,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_prefixes() {
|
||||
fn var_prefixes() -> Result<()> {
|
||||
let mut p = Parser::new("${[ -a ]}${[ 0a ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Raw {
|
||||
// Shouldn't be parsed, because they're invalid
|
||||
@@ -468,13 +502,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_underscore_prefix() {
|
||||
fn var_underscore_prefix() -> Result<()> {
|
||||
let mut p = Parser::new("${[ _a ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Var { name: "_a".into() }
|
||||
@@ -482,13 +518,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_boolean() {
|
||||
fn var_boolean() -> Result<()> {
|
||||
let mut p = Parser::new("${[ true ]}${[ false ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Bool { value: true },
|
||||
@@ -499,13 +537,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_multiple_names_invalid() {
|
||||
fn var_multiple_names_invalid() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo bar ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Raw {
|
||||
text: "${[ foo bar ]}".into()
|
||||
@@ -513,13 +553,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_string() {
|
||||
fn tag_string() -> Result<()> {
|
||||
let mut p = Parser::new(r#"${[ 'foo \'bar\' baz' ]}"#);
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Str {
|
||||
@@ -529,13 +571,33 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_surrounded() {
|
||||
fn tag_b64_string() -> Result<()> {
|
||||
let mut p = Parser::new(r#"${[ b64'Zm9vICdiYXInIGJheg' ]}"#);
|
||||
assert_eq!(
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Str {
|
||||
text: r#"foo 'bar' baz"#.into()
|
||||
}
|
||||
},
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_surrounded() -> Result<()> {
|
||||
let mut p = Parser::new("Hello ${[ foo ]}!");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Raw {
|
||||
text: "Hello ".to_string()
|
||||
@@ -549,13 +611,15 @@ mod tests {
|
||||
Token::Eof,
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_simple() {
|
||||
fn fn_simple() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo() ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -566,13 +630,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dot_name() {
|
||||
fn fn_dot_name() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo.bar.baz() ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -583,13 +649,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_ident_arg() {
|
||||
fn fn_ident_arg() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo(a=bar) ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -603,13 +671,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_ident_args() {
|
||||
fn fn_ident_args() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -633,13 +703,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_mixed_args() {
|
||||
fn fn_mixed_args() -> Result<()> {
|
||||
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb='baz \'hi\'', c=qux, z=true ) ]}"#);
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -669,13 +741,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_nested() {
|
||||
fn fn_nested() -> Result<()> {
|
||||
let mut p = Parser::new("${[ foo(b=bar()) ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -692,13 +766,15 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_nested_args() {
|
||||
fn fn_nested_args() -> Result<()> {
|
||||
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b='i'), c='o') ]}"#);
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
p.parse()?.tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -730,10 +806,12 @@ mod tests {
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_display_var() {
|
||||
fn token_display_var() -> Result<()> {
|
||||
assert_eq!(
|
||||
Val::Var {
|
||||
name: "foo".to_string()
|
||||
@@ -741,21 +819,38 @@ mod tests {
|
||||
.to_string(),
|
||||
"foo"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_display_str() {
|
||||
fn token_display_str() -> Result<()> {
|
||||
assert_eq!(
|
||||
Val::Str {
|
||||
text: "Hello You".to_string()
|
||||
}
|
||||
.to_string(),
|
||||
"'Hello You'"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_display_complex_str() -> Result<()> {
|
||||
assert_eq!(
|
||||
Val::Str {
|
||||
text: "Hello 'You'".to_string()
|
||||
}
|
||||
.to_string(),
|
||||
"'Hello \'You\''"
|
||||
"b64'SGVsbG8gJ1lvdSc'"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_null_fn_arg() {
|
||||
fn token_null_fn_arg() -> Result<()> {
|
||||
assert_eq!(
|
||||
Val::Fn {
|
||||
name: "fn".to_string(),
|
||||
@@ -775,10 +870,12 @@ mod tests {
|
||||
.to_string(),
|
||||
r#"fn(a='aaa')"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_display_fn() {
|
||||
fn token_display_fn() -> Result<()> {
|
||||
assert_eq!(
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
@@ -787,7 +884,7 @@ mod tests {
|
||||
FnArg {
|
||||
name: "arg".to_string(),
|
||||
value: Val::Str {
|
||||
text: "v".to_string()
|
||||
text: "v 'x'".to_string()
|
||||
}
|
||||
},
|
||||
FnArg {
|
||||
@@ -800,12 +897,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
.to_string(),
|
||||
r#"${[ foo(arg='v', arg2=my_var) ]}"#
|
||||
r#"${[ foo(arg=b64'diAneCc', arg2=my_var) ]}"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_display() {
|
||||
fn tokens_display() -> Result<()> {
|
||||
assert_eq!(
|
||||
Tokens {
|
||||
tokens: vec![
|
||||
@@ -827,5 +926,7 @@ mod tests {
|
||||
.to_string(),
|
||||
r#"${[ my_var ]} Some cool text ${[ 'Hello World' ]}"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::error::Error::{RenderStackExceededError, VariableNotFound};
|
||||
use crate::error::Result;
|
||||
use crate::{FnArg, Parser, Token, Tokens, Val};
|
||||
use crate::{Parser, Token, Tokens, Val};
|
||||
use log::warn;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
@@ -44,14 +44,14 @@ pub async fn render_json_value_raw<T: TemplateCallback>(
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
async fn parse_and_render_with_depth<T: TemplateCallback>(
|
||||
async fn parse_and_render_at_depth<T: TemplateCallback>(
|
||||
template: &str,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: &T,
|
||||
depth: usize,
|
||||
) -> Result<String> {
|
||||
let mut p = Parser::new(template);
|
||||
let tokens = p.parse();
|
||||
let tokens = p.parse()?;
|
||||
render(tokens, vars, cb, depth + 1).await
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn parse_and_render<T: TemplateCallback>(
|
||||
vars: &HashMap<String, String>,
|
||||
cb: &T,
|
||||
) -> Result<String> {
|
||||
parse_and_render_with_depth(template, vars, cb, 1).await
|
||||
parse_and_render_at_depth(template, vars, cb, 1).await
|
||||
}
|
||||
|
||||
pub async fn render<T: TemplateCallback>(
|
||||
@@ -79,7 +79,7 @@ pub async fn render<T: TemplateCallback>(
|
||||
for t in tokens.tokens {
|
||||
match t {
|
||||
Token::Raw { text } => doc_str.push(text),
|
||||
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb, depth).await?),
|
||||
Token::Tag { val } => doc_str.push(render_value(val, &vars, cb, depth).await?),
|
||||
Token::Eof => {}
|
||||
}
|
||||
}
|
||||
@@ -87,44 +87,31 @@ pub async fn render<T: TemplateCallback>(
|
||||
Ok(doc_str.join(""))
|
||||
}
|
||||
|
||||
async fn render_tag<T: TemplateCallback>(
|
||||
async fn render_value<T: TemplateCallback>(
|
||||
val: Val,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: &T,
|
||||
depth: usize,
|
||||
) -> Result<String> {
|
||||
let v = match val {
|
||||
Val::Str { text } => text.into(),
|
||||
Val::Str { text } => {
|
||||
let r = Box::pin(parse_and_render_at_depth(&text, vars, cb, depth)).await?;
|
||||
r.to_string()
|
||||
}
|
||||
Val::Var { name } => match vars.get(name.as_str()) {
|
||||
Some(v) => {
|
||||
let r = Box::pin(parse_and_render_with_depth(v, vars, cb, depth)).await?;
|
||||
let r = Box::pin(parse_and_render_at_depth(v, vars, cb, depth)).await?;
|
||||
r.to_string()
|
||||
}
|
||||
None => return Err(VariableNotFound(name)),
|
||||
},
|
||||
Val::Bool { value } => value.to_string(),
|
||||
Val::Fn { name, args } => {
|
||||
let empty = "".to_string();
|
||||
// let empty = "".to_string();
|
||||
let mut resolved_args: HashMap<String, String> = HashMap::new();
|
||||
for a in args {
|
||||
let (k, v) = match a {
|
||||
FnArg {
|
||||
name,
|
||||
value: Val::Str { text },
|
||||
} => (name.to_string(), text.to_string()),
|
||||
FnArg {
|
||||
name,
|
||||
value: Val::Var { name: var_name },
|
||||
} => (
|
||||
name.to_string(),
|
||||
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
|
||||
),
|
||||
FnArg { name, value: val } => {
|
||||
let r = Box::pin(render_tag(val.clone(), vars, cb, depth)).await?;
|
||||
(name.to_string(), r)
|
||||
}
|
||||
};
|
||||
resolved_args.insert(k, v);
|
||||
let v = Box::pin(render_value(a.value, vars, cb, depth)).await?;
|
||||
resolved_args.insert(a.name, v);
|
||||
}
|
||||
match cb.run(name.as_str(), resolved_args.clone()).await {
|
||||
Ok(s) => s,
|
||||
@@ -253,6 +240,67 @@ mod parse_and_render_tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_fn_arg() -> Result<()> {
|
||||
let vars = HashMap::new();
|
||||
let template = r#"${[ upper(foo='bar') ]}"#;
|
||||
let result = r#"BAR"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_fn_b64_arg_template() -> Result<()> {
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("foo".to_string(), "bar".to_string());
|
||||
let template = r#"${[ upper(foo=b64'Zm9vICdiYXInIGJheg') ]}"#;
|
||||
let result = r#"FOO 'BAR' BAZ"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_fn_arg_template() -> Result<()> {
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("foo".to_string(), "bar".to_string());
|
||||
let template = r#"${[ upper(foo='${[ foo ]}') ]}"#;
|
||||
let result = r#"BAR"#;
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String> {
|
||||
Ok(match fn_name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_nested_fn() -> Result<()> {
|
||||
let vars = HashMap::new();
|
||||
@@ -277,7 +325,6 @@ mod parse_and_render_tests {
|
||||
async fn render_fn_err() -> Result<()> {
|
||||
let vars = HashMap::new();
|
||||
let template = r#"${[ error() ]}"#;
|
||||
let result = r#""#;
|
||||
|
||||
struct CB {}
|
||||
impl TemplateCallback for CB {
|
||||
@@ -286,7 +333,10 @@ mod parse_and_render_tests {
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(parse_and_render(template, &vars, &CB {}).await?, result.to_string());
|
||||
assert_eq!(
|
||||
parse_and_render(template, &vars, &CB {}).await,
|
||||
Err(RenderError("Failed to do it!".to_string()))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props<T> {
|
||||
request: T;
|
||||
}
|
||||
|
||||
export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
|
||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||
|
||||
return (
|
||||
<VStack className="py-2 overflow-y-auto h-full" space={2}>
|
||||
<Input
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
stateKey={`basic.username.${request.id}`}
|
||||
forceUpdateKey={request.id}
|
||||
placeholder="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
size="sm"
|
||||
defaultValue={`${request.authentication.username}`}
|
||||
onChange={(username: string) => {
|
||||
if (request.model === 'http_request') {
|
||||
updateHttpRequest.mutate({
|
||||
id: request.id,
|
||||
update: (r: HttpRequest) => ({
|
||||
...r,
|
||||
authentication: { password: r.authentication.password, username },
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
updateGrpcRequest.mutate({
|
||||
id: request.id,
|
||||
update: (r: GrpcRequest) => ({
|
||||
...r,
|
||||
authentication: { password: r.authentication.password, username },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
forceUpdateKey={request?.id}
|
||||
stateKey={`basic.password.${request.id}`}
|
||||
placeholder="password"
|
||||
label="Password"
|
||||
name="password"
|
||||
size="sm"
|
||||
type="password"
|
||||
defaultValue={`${request.authentication.password}`}
|
||||
onChange={(password: string) => {
|
||||
if (request.model === 'http_request') {
|
||||
updateHttpRequest.mutate({
|
||||
id: request.id,
|
||||
update: (r: HttpRequest) => ({
|
||||
...r,
|
||||
authentication: { username: r.authentication.username, password },
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
updateGrpcRequest.mutate({
|
||||
id: request.id,
|
||||
update: (r: GrpcRequest) => ({
|
||||
...r,
|
||||
authentication: { username: r.authentication.username, password },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props<T> {
|
||||
request: T;
|
||||
}
|
||||
|
||||
export function BearerAuth<T extends HttpRequest | GrpcRequest>({ request }: Props<T>) {
|
||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||
|
||||
return (
|
||||
<VStack className="my-2" space={2}>
|
||||
<Input
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
placeholder="token"
|
||||
stateKey={`bearer.${request.id}`}
|
||||
type="password"
|
||||
label="Token"
|
||||
name="token"
|
||||
size="sm"
|
||||
defaultValue={`${request.authentication.token}`}
|
||||
onChange={(token: string) => {
|
||||
if (request.model === 'http_request') {
|
||||
updateHttpRequest.mutate({
|
||||
id: request.id ?? null,
|
||||
update: (r: HttpRequest) => ({
|
||||
...r,
|
||||
authentication: { token },
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
updateGrpcRequest.mutate({
|
||||
id: request.id ?? null,
|
||||
update: (r: GrpcRequest) => ({
|
||||
...r,
|
||||
authentication: { token },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ interface Props<T> {
|
||||
inputs: FormInput[] | undefined | null;
|
||||
onChange: (value: T) => void;
|
||||
data: T;
|
||||
useTemplating?: boolean;
|
||||
autocompleteFunctions?: boolean;
|
||||
autocompleteVariables?: boolean;
|
||||
stateKey: string;
|
||||
disabled?: boolean;
|
||||
@@ -44,8 +44,8 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
inputs,
|
||||
data,
|
||||
onChange,
|
||||
useTemplating,
|
||||
autocompleteVariables,
|
||||
autocompleteFunctions,
|
||||
stateKey,
|
||||
disabled,
|
||||
}: Props<T>) {
|
||||
@@ -62,7 +62,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
inputs={inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
data={data}
|
||||
/>
|
||||
@@ -71,13 +71,16 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
||||
|
||||
function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs,
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
useTemplating,
|
||||
setDataAttr,
|
||||
data,
|
||||
disabled,
|
||||
}: Pick<Props<T>, 'inputs' | 'useTemplating' | 'autocompleteVariables' | 'stateKey' | 'data'> & {
|
||||
}: Pick<
|
||||
Props<T>,
|
||||
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
|
||||
> & {
|
||||
setDataAttr: (name: string, value: JsonPrimitive) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
@@ -112,7 +115,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
key={i}
|
||||
stateKey={stateKey}
|
||||
arg={input}
|
||||
useTemplating={useTemplating || false}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables || false}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
@@ -126,7 +129,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
key={i}
|
||||
stateKey={stateKey}
|
||||
arg={input}
|
||||
useTemplating={useTemplating || false}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables || false}
|
||||
onChange={(v) => setDataAttr(input.name, v)}
|
||||
value={
|
||||
@@ -175,7 +178,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</div>
|
||||
@@ -195,7 +198,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteFunctions={autocompleteFunctions || false}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</Banner>
|
||||
@@ -212,14 +215,14 @@ function TextArg({
|
||||
arg,
|
||||
onChange,
|
||||
value,
|
||||
useTemplating,
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
}: {
|
||||
arg: FormInputText;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
useTemplating: boolean;
|
||||
autocompleteFunctions: boolean;
|
||||
autocompleteVariables: boolean;
|
||||
stateKey: string;
|
||||
}) {
|
||||
@@ -237,7 +240,7 @@ function TextArg({
|
||||
hideLabel={arg.label == null}
|
||||
placeholder={arg.placeholder ?? undefined}
|
||||
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
stateKey={stateKey}
|
||||
forceUpdateKey={stateKey}
|
||||
@@ -249,14 +252,14 @@ function EditorArg({
|
||||
arg,
|
||||
onChange,
|
||||
value,
|
||||
useTemplating,
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
stateKey,
|
||||
}: {
|
||||
arg: FormInputEditor;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
useTemplating: boolean;
|
||||
autocompleteFunctions: boolean;
|
||||
autocompleteVariables: boolean;
|
||||
stateKey: string;
|
||||
}) {
|
||||
@@ -290,7 +293,7 @@ function EditorArg({
|
||||
heightMode="auto"
|
||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||
placeholder={arg.placeholder ?? undefined}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
stateKey={stateKey}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
|
||||
@@ -116,7 +116,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
};
|
||||
|
||||
const EnvironmentEditor = function ({
|
||||
environment,
|
||||
environment: activeEnvironment,
|
||||
className,
|
||||
}: {
|
||||
environment: Environment;
|
||||
@@ -127,8 +127,8 @@ const EnvironmentEditor = function ({
|
||||
key: 'environmentValueVisibility',
|
||||
fallback: true,
|
||||
});
|
||||
const { subEnvironments } = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
|
||||
const { allEnvironments } = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
|
||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||
(variables) => updateEnvironment.mutate({ variables }),
|
||||
[updateEnvironment],
|
||||
@@ -136,26 +136,28 @@ const EnvironmentEditor = function ({
|
||||
|
||||
// Gather a list of env names from other environments, to help the user get them aligned
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const allVariableNames =
|
||||
environment == null
|
||||
? [] // Nothing to autocomplete if we're in the base environment
|
||||
: subEnvironments
|
||||
.filter((e) => e.environmentId != null)
|
||||
.flatMap((e) => e.variables.map((v) => v.name));
|
||||
const options: GenericCompletionOption[] = [];
|
||||
const isBaseEnv = activeEnvironment.environmentId == null;
|
||||
if (isBaseEnv) {
|
||||
return { options };
|
||||
}
|
||||
|
||||
// Filter out empty strings and variables that already exist
|
||||
const variableNames = allVariableNames.filter(
|
||||
(name) => name != '' && !environment.variables.find((v) => v.name === name),
|
||||
);
|
||||
const uniqueVariableNames = [...new Set(variableNames)];
|
||||
const options = uniqueVariableNames.map(
|
||||
(name): GenericCompletionOption => ({
|
||||
const allVariables = allEnvironments.flatMap((e) => e?.variables);
|
||||
const allVariableNames = new Set(allVariables.map((v) => v?.name));
|
||||
for (const name of allVariableNames) {
|
||||
const containingEnvs = allEnvironments.filter((e) =>
|
||||
e.variables.some((v) => v.name === name),
|
||||
);
|
||||
const isAlreadyInActive = containingEnvs.find((e) => e.id === activeEnvironment.id);
|
||||
if (isAlreadyInActive) continue;
|
||||
options.push({
|
||||
label: name,
|
||||
type: 'constant',
|
||||
}),
|
||||
);
|
||||
detail: containingEnvs.map((e) => e.name).join(', '),
|
||||
});
|
||||
}
|
||||
return { options };
|
||||
}, [subEnvironments, environment]);
|
||||
}, [activeEnvironment.environmentId, activeEnvironment.id, allEnvironments]);
|
||||
|
||||
const validateName = useCallback((name: string) => {
|
||||
// Empty just means the variable doesn't have a name yet, and is unusable
|
||||
@@ -167,7 +169,7 @@ const EnvironmentEditor = function ({
|
||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<Heading className="w-full flex items-center gap-1">
|
||||
<div>{environment?.name}</div>
|
||||
<div>{activeEnvironment?.name}</div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={valueVisibility.value ? 'eye' : 'eye_closed'}
|
||||
@@ -180,17 +182,18 @@ const EnvironmentEditor = function ({
|
||||
</HStack>
|
||||
<div className="h-full pr-2 pb-2">
|
||||
<PairOrBulkEditor
|
||||
allowMultilineValues
|
||||
preferenceName="environment"
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteVariables={false}
|
||||
namePlaceholder="VAR_NAME"
|
||||
nameValidate={validateName}
|
||||
valueType={valueVisibility.value ? 'text' : 'password'}
|
||||
valueAutocompleteVariables={true}
|
||||
forceUpdateKey={environment.id}
|
||||
pairs={environment.variables}
|
||||
valueAutocompleteVariables
|
||||
valueAutocompleteFunctions
|
||||
forceUpdateKey={activeEnvironment.id}
|
||||
pairs={activeEnvironment.variables}
|
||||
onChange={handleChange}
|
||||
stateKey={`environment.${environment.id}`}
|
||||
stateKey={`environment.${activeEnvironment.id}`}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
|
||||
@@ -40,9 +40,12 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
|
||||
|
||||
return (
|
||||
<PairEditor
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
nameAutocompleteFunctions
|
||||
allowFileValues
|
||||
allowMultilineValues
|
||||
pairs={pairs}
|
||||
onChange={handleChange}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
|
||||
@@ -29,8 +29,11 @@ export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Prop
|
||||
|
||||
return (
|
||||
<PairOrBulkEditor
|
||||
allowMultilineValues
|
||||
preferenceName="form_urlencoded"
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteFunctions
|
||||
nameAutocompleteVariables
|
||||
namePlaceholder="entry_name"
|
||||
valuePlaceholder="Value"
|
||||
|
||||
@@ -183,7 +183,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
onChange={handleChangeVariables}
|
||||
placeholder="{}"
|
||||
stateKey={'graphql_vars.' + request.id}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
|
||||
@@ -181,8 +181,8 @@ export function GrpcEditor({
|
||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||
<Editor
|
||||
language="json"
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
useTemplating
|
||||
forceUpdateKey={request.id}
|
||||
defaultValue={request.message}
|
||||
heightMode="auto"
|
||||
|
||||
@@ -21,7 +21,9 @@ export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: P
|
||||
<PairOrBulkEditor
|
||||
preferenceName="headers"
|
||||
stateKey={stateKey}
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteFunctions
|
||||
nameAutocompleteVariables
|
||||
pairs={headers}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
<DynamicForm
|
||||
disabled={request.authentication.disabled}
|
||||
autocompleteVariables
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
stateKey={`auth.${request.id}.${request.authenticationType}`}
|
||||
inputs={authConfig.data.args}
|
||||
data={request.authentication}
|
||||
|
||||
@@ -411,7 +411,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
@@ -423,7 +423,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
@@ -462,7 +462,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
) : typeof activeRequest.bodyType === 'string' ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
language={languageFromContentType(contentType)}
|
||||
placeholder="..."
|
||||
|
||||
@@ -3,8 +3,8 @@ import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export function KeyboardShortcutsDialog() {
|
||||
return (
|
||||
<div className="h-full w-full pb-2">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
<div className="grid h-full">
|
||||
<HotKeyList hotkeys={hotkeyActions} className="pb-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,32 +2,19 @@ import type { LicenseCheckStatus } from '@yaakapp-internal/license';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import type { ReactNode } from 'react';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { appInfo } from '../hooks/useAppInfo';
|
||||
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import { Icon } from './core/Icon';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { SettingsTab } from './Settings/SettingsTab';
|
||||
|
||||
const details: Record<
|
||||
LicenseCheckStatus['type'] | 'dev' | 'beta',
|
||||
LicenseCheckStatus['type'],
|
||||
{ label: ReactNode; color: ButtonProps['color'] } | null
|
||||
> = {
|
||||
beta: {
|
||||
label: (
|
||||
<HStack space={1}>
|
||||
<span>Beta Feedback</span>
|
||||
<Icon size="xs" icon="external_link" />
|
||||
</HStack>
|
||||
),
|
||||
color: 'info',
|
||||
},
|
||||
dev: { label: 'Develop', color: 'secondary' },
|
||||
commercial_use: null,
|
||||
invalid_license: { label: 'License Error', color: 'danger' },
|
||||
personal_use: { label: 'Personal Use', color: 'success' },
|
||||
trialing: { label: 'Personal Use', color: 'success' },
|
||||
personal_use: { label: 'Personal Use', color: 'notice' },
|
||||
trialing: { label: 'Personal Use', color: 'notice' },
|
||||
};
|
||||
|
||||
export function LicenseBadge() {
|
||||
@@ -62,8 +49,7 @@ export function LicenseBadge() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkType = appInfo.version.includes('beta') ? 'beta' : check.data.type;
|
||||
const detail = details[checkType];
|
||||
const detail = details[check.data.type];
|
||||
if (detail == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import { useMemo, useState } from 'react';
|
||||
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
||||
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
||||
import { useToggle } from '../hooks/useToggle';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { Banner } from './core/Banner';
|
||||
|
||||
interface Props {
|
||||
templateFunction: TemplateFunction;
|
||||
@@ -18,6 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) {
|
||||
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
|
||||
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(() => {
|
||||
const initial: Record<string, string> = {};
|
||||
const initialArgs =
|
||||
@@ -77,27 +81,65 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
||||
|
||||
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 200);
|
||||
const rendered = useRenderTemplate(debouncedTagText);
|
||||
const tooLarge = (rendered.data ?? '').length > 10000;
|
||||
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
|
||||
const dataContainsSecrets = useMemo(() => {
|
||||
for (const [name, value] of Object.entries(argValues)) {
|
||||
const isPassword = templateFunction.args.some(
|
||||
(a) => a.type === 'text' && a.password && a.name === name,
|
||||
);
|
||||
if (isPassword && typeof value === 'string' && value && rendered.data?.includes(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Only update this on rendered data change to keep secrets hidden on input change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rendered.data]);
|
||||
|
||||
return (
|
||||
<VStack className="pb-3" space={4}>
|
||||
<h1 className="font-mono !text-base">{templateFunction.name}(…)</h1>
|
||||
<DynamicForm
|
||||
autocompleteVariables
|
||||
autocompleteFunctions
|
||||
inputs={templateFunction.args}
|
||||
data={argValues}
|
||||
onChange={setArgValues}
|
||||
stateKey={`template_function.${templateFunction.name}`}
|
||||
/>
|
||||
<VStack className="w-full">
|
||||
<div className="text-sm text-text-subtle">Preview</div>
|
||||
<InlineCode
|
||||
className={classNames(
|
||||
'whitespace-pre select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
||||
tooLarge && 'italic text-danger',
|
||||
)}
|
||||
>
|
||||
{tooLarge ? 'too large to preview' : rendered.data || <> </>}
|
||||
</InlineCode>
|
||||
<VStack className="w-full" space={1}>
|
||||
<HStack space={0.5}>
|
||||
<div className="text-sm text-text-subtle">Rendered Preview</div>
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
icon={showSecretsInPreview ? 'lock' : 'lock_open'}
|
||||
title={showSecretsInPreview ? 'Show preview' : 'Hide preview'}
|
||||
onClick={toggleShowSecretsInPreview}
|
||||
className={classNames(
|
||||
'ml-auto text-text-subtlest',
|
||||
!dataContainsSecrets && 'invisible',
|
||||
)}
|
||||
/>
|
||||
</HStack>
|
||||
{rendered.error || tagText.error ? (
|
||||
<Banner color="danger">{`${rendered.error || tagText.error}`}</Banner>
|
||||
) : (
|
||||
<InlineCode
|
||||
className={classNames(
|
||||
'whitespace-pre select-text cursor-text max-h-[10rem] overflow-y-auto hide-scrollbars',
|
||||
tooLarge && 'italic text-danger',
|
||||
)}
|
||||
>
|
||||
{dataContainsSecrets && !showSecretsInPreview ? (
|
||||
<span className="italic text-text-subtle">------ sensitive values hidden ------</span>
|
||||
) : tooLarge ? (
|
||||
'too large to preview'
|
||||
) : (
|
||||
rendered.data || <> </>
|
||||
)}
|
||||
</InlineCode>
|
||||
)}
|
||||
</VStack>
|
||||
<Button color="primary" onClick={handleDone}>
|
||||
Done
|
||||
|
||||
@@ -68,12 +68,12 @@ export const UrlBar = memo(function UrlBar({
|
||||
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
stateKey={stateKey}
|
||||
size="sm"
|
||||
wrapLines={isFocused}
|
||||
hideLabel
|
||||
useTemplating
|
||||
language="url"
|
||||
className="px-1.5 py-0.5"
|
||||
label="Enter URL"
|
||||
|
||||
@@ -33,13 +33,16 @@ export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey
|
||||
<VStack className="h-full">
|
||||
<PairOrBulkEditor
|
||||
ref={pairEditor}
|
||||
allowMultilineValues
|
||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||
nameAutocompleteFunctions
|
||||
nameAutocompleteVariables
|
||||
namePlaceholder="param_name"
|
||||
onChange={onChange}
|
||||
pairs={pairs}
|
||||
preferenceName="url_parameters"
|
||||
stateKey={stateKey}
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
valuePlaceholder="Value"
|
||||
/>
|
||||
|
||||
@@ -301,7 +301,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
<TabContent value={TAB_MESSAGE}>
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function AutoScroller<T>({ data, render, header }: Props<T>) {
|
||||
}, [autoScroll, data.length]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="h-full w-full relative grid grid-rows-[minmax(0,auto)_minmax(0,1fr)]">
|
||||
{!autoScroll && (
|
||||
<div className="absolute bottom-0 right-0 m-2">
|
||||
<IconButton
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { Editor } from './Editor/Editor';
|
||||
import type { PairEditorProps, PairWithId } from './PairEditor';
|
||||
import type { Pair, PairEditorProps, PairWithId } from './PairEditor';
|
||||
|
||||
type Props = PairEditorProps;
|
||||
|
||||
@@ -16,7 +16,7 @@ export function BulkPairEditor({
|
||||
const pairsText = useMemo(() => {
|
||||
return pairs
|
||||
.filter((p) => !(p.name.trim() === '' && p.value.trim() === ''))
|
||||
.map((p) => `${p.name}: ${p.value}`)
|
||||
.map(pairToLine)
|
||||
.join('\n');
|
||||
}, [pairs]);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function BulkPairEditor({
|
||||
|
||||
return (
|
||||
<Editor
|
||||
useTemplating
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
stateKey={`bulk_pair.${stateKey}`}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
@@ -45,12 +45,17 @@ export function BulkPairEditor({
|
||||
);
|
||||
}
|
||||
|
||||
function pairToLine(pair: Pair) {
|
||||
const value = pair.value.replaceAll('\n', '\\n');
|
||||
return `${pair.name}: ${value}`;
|
||||
}
|
||||
|
||||
function lineToPair(line: string): PairWithId {
|
||||
const [, name, value] = line.match(/^(:?[^:]+):\s+(.*)$/) ?? [];
|
||||
return {
|
||||
enabled: true,
|
||||
name: (name ?? '').trim(),
|
||||
value: (value ?? '').trim(),
|
||||
value: (value ?? '').replaceAll('\\n', '\n').trim(),
|
||||
id: generateId(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
@apply w-full;
|
||||
/* Important! Ensure it spans the entire width */
|
||||
@apply w-full text-text px-0;
|
||||
|
||||
/* So the search highlight border is not cut off by editor view */
|
||||
@apply pl-[1px];
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
@@ -127,6 +130,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Style search matches */
|
||||
.cm-searchMatch {
|
||||
@apply bg-transparent !important;
|
||||
@apply rounded-[2px] outline outline-1;
|
||||
&.cm-searchMatch-selected {
|
||||
@apply outline-text;
|
||||
@apply bg-text !important;
|
||||
&, * {
|
||||
@apply text-surface font-semibold !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*.cm-searchMatch {*/
|
||||
/* @apply bg-transparent !important;*/
|
||||
/* @apply outline outline-[1.5px] outline-text-subtlest rounded-sm;*/
|
||||
/* &.cm-searchMatch-selected {*/
|
||||
/* @apply outline-text;*/
|
||||
/* & * {*/
|
||||
/* @apply text-text font-semibold;*/
|
||||
/* }*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/* Obscure text for password fields */
|
||||
.cm-wrapper.cm-obscure-text .cm-line {
|
||||
-webkit-text-security: disc;
|
||||
|
||||
@@ -55,36 +55,36 @@ const keymapExtensions: Record<EditorKeymap, Extension> = {
|
||||
};
|
||||
|
||||
export interface EditorProps {
|
||||
id?: string;
|
||||
readOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: 'text' | 'password';
|
||||
className?: string;
|
||||
heightMode?: 'auto' | 'full';
|
||||
language?: EditorLanguage | 'pairs' | 'url';
|
||||
forceUpdateKey?: string | number;
|
||||
actions?: ReactNode;
|
||||
autoFocus?: boolean;
|
||||
autoSelect?: boolean;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
autocompleteFunctions?: boolean;
|
||||
autocompleteVariables?: boolean;
|
||||
className?: string;
|
||||
defaultValue?: string | null;
|
||||
placeholder?: string;
|
||||
tooltipContainer?: HTMLElement;
|
||||
useTemplating?: boolean;
|
||||
disableTabIndent?: boolean;
|
||||
disabled?: boolean;
|
||||
extraExtensions?: Extension[];
|
||||
forceUpdateKey?: string | number;
|
||||
format?: (v: string) => Promise<string>;
|
||||
heightMode?: 'auto' | 'full';
|
||||
hideGutter?: boolean;
|
||||
id?: string;
|
||||
language?: EditorLanguage | 'pairs' | 'url';
|
||||
onBlur?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: (e: ClipboardEvent, value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
singleLine?: boolean;
|
||||
wrapLines?: boolean;
|
||||
disableTabIndent?: boolean;
|
||||
format?: (v: string) => Promise<string>;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
autocompleteVariables?: boolean;
|
||||
extraExtensions?: Extension[];
|
||||
actions?: ReactNode;
|
||||
hideGutter?: boolean;
|
||||
stateKey: string | null;
|
||||
tooltipContainer?: HTMLElement;
|
||||
type?: 'text' | 'password';
|
||||
wrapLines?: boolean;
|
||||
}
|
||||
|
||||
const stateFields = { history: historyField, folds: foldState };
|
||||
@@ -94,34 +94,34 @@ const emptyExtension: Extension = [];
|
||||
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
{
|
||||
readOnly,
|
||||
type,
|
||||
heightMode,
|
||||
language,
|
||||
actions,
|
||||
autoFocus,
|
||||
autoSelect,
|
||||
placeholder,
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
className,
|
||||
defaultValue,
|
||||
disableTabIndent,
|
||||
disabled,
|
||||
extraExtensions,
|
||||
forceUpdateKey,
|
||||
format,
|
||||
heightMode,
|
||||
hideGutter,
|
||||
language,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
className,
|
||||
disabled,
|
||||
placeholder,
|
||||
readOnly,
|
||||
singleLine,
|
||||
format,
|
||||
autocomplete,
|
||||
extraExtensions,
|
||||
autocompleteVariables,
|
||||
actions,
|
||||
wrapLines,
|
||||
disableTabIndent,
|
||||
hideGutter,
|
||||
stateKey,
|
||||
type,
|
||||
wrapLines,
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
@@ -129,6 +129,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
|
||||
const allEnvironmentVariables = useActiveEnvironmentVariables();
|
||||
const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables;
|
||||
const useTemplating = !!(autocompleteFunctions || autocompleteVariables || autocomplete);
|
||||
|
||||
if (settings && wrapLines === undefined) {
|
||||
wrapLines = settings.editorSoftWrap;
|
||||
@@ -265,7 +266,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
||||
const initialTokens = await parseTemplate(tagValue);
|
||||
showDialog({
|
||||
id: 'template-function',
|
||||
id: 'template-function-'+Math.random(), // Allow multiple at once
|
||||
size: 'sm',
|
||||
title: 'Configure Function',
|
||||
description: fn.description,
|
||||
@@ -340,16 +341,19 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
[focusParamValue],
|
||||
);
|
||||
|
||||
const completionOptions = useTemplateFunctionCompletionOptions(onClickFunction);
|
||||
const completionOptions = useTemplateFunctionCompletionOptions(
|
||||
onClickFunction,
|
||||
!!autocompleteFunctions,
|
||||
);
|
||||
|
||||
// Update the language extension when the language changes
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
const { view, languageCompartment } = cm.current;
|
||||
const ext = getLanguageExtension({
|
||||
useTemplating,
|
||||
language,
|
||||
environmentVariables,
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
completionOptions,
|
||||
onClickVariable,
|
||||
@@ -360,13 +364,13 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}, [
|
||||
language,
|
||||
autocomplete,
|
||||
useTemplating,
|
||||
environmentVariables,
|
||||
onClickFunction,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
completionOptions,
|
||||
useTemplating,
|
||||
]);
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
@@ -381,8 +385,8 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
try {
|
||||
const languageCompartment = new Compartment();
|
||||
const langExt = getLanguageExtension({
|
||||
language,
|
||||
useTemplating,
|
||||
language,
|
||||
completionOptions,
|
||||
autocomplete,
|
||||
environmentVariables,
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@codemirror/language';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import {
|
||||
@@ -74,7 +74,7 @@ export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
|
||||
const syntaxTheme = EditorView.theme({}, { dark: true });
|
||||
|
||||
const closeBracketsExts: Extension = [closeBrackets(), keymap.of([...closeBracketsKeymap])];
|
||||
const closeBracketsExtensions: Extension = [closeBrackets(), keymap.of([...closeBracketsKeymap])];
|
||||
|
||||
const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSupport | null> = {
|
||||
graphql: null,
|
||||
@@ -88,11 +88,11 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
|
||||
markdown: markdown(),
|
||||
};
|
||||
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript'];
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript', 'graphql'];
|
||||
|
||||
export function getLanguageExtension({
|
||||
language,
|
||||
useTemplating = false,
|
||||
useTemplating,
|
||||
language = 'text',
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
onClickVariable,
|
||||
@@ -100,27 +100,33 @@ export function getLanguageExtension({
|
||||
onClickPathParameter,
|
||||
completionOptions,
|
||||
}: {
|
||||
useTemplating: boolean;
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
} & Pick<EditorProps, 'language' | 'useTemplating' | 'autocomplete'>) {
|
||||
if (language === 'graphql') {
|
||||
return graphql();
|
||||
}
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete'>) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
const base = syntaxExtensions[language ?? 'text'] ?? text();
|
||||
if (!useTemplating) {
|
||||
return base;
|
||||
if (language === 'url') {
|
||||
extraExtensions.push(pathParametersPlugin(onClickPathParameter));
|
||||
}
|
||||
|
||||
const extraExtensions: Extension[] =
|
||||
language === 'url' ? [pathParametersPlugin(onClickPathParameter)] : [];
|
||||
|
||||
// Only close brackets on languages that need it
|
||||
if (language && closeBracketsFor.includes(language)) {
|
||||
extraExtensions.push(closeBracketsExts);
|
||||
extraExtensions.push(closeBracketsExtensions);
|
||||
}
|
||||
|
||||
// GraphQL is a special exception
|
||||
if (language === 'graphql') {
|
||||
return [graphql(), extraExtensions];
|
||||
}
|
||||
|
||||
const base = syntaxExtensions[language ?? 'text'] ?? text();
|
||||
|
||||
if (!useTemplating) {
|
||||
return [base, extraExtensions];
|
||||
}
|
||||
|
||||
return twig({
|
||||
@@ -159,6 +165,7 @@ export const readonlyExtensions = [
|
||||
];
|
||||
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
search({ top: true }),
|
||||
hideGutter
|
||||
? []
|
||||
: [
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
export const HotKeyList = ({ hotkeys, bottomSlot, className }: Props) => {
|
||||
return (
|
||||
<div className={classNames(className, 'h-full flex items-center justify-center')}>
|
||||
<div className="px-4 grid gap-2 grid-cols-[auto_auto]">
|
||||
<div className="grid gap-2 grid-cols-[auto_auto]">
|
||||
{hotkeys.map((hotkey) => (
|
||||
<Fragment key={hotkey}>
|
||||
<HotKeyLabel className="truncate" action={hotkey} />
|
||||
|
||||
@@ -69,6 +69,7 @@ const icons = {
|
||||
left_panel_hidden: lucide.PanelLeftOpenIcon,
|
||||
left_panel_visible: lucide.PanelLeftCloseIcon,
|
||||
lock: lucide.LockIcon,
|
||||
lock_open: lucide.LockOpenIcon,
|
||||
magic_wand: lucide.Wand2Icon,
|
||||
merge: lucide.MergeIcon,
|
||||
minus: lucide.MinusIcon,
|
||||
|
||||
@@ -13,13 +13,13 @@ import { HStack } from './Stacks';
|
||||
export type InputProps = Pick<
|
||||
EditorProps,
|
||||
| 'language'
|
||||
| 'useTemplating'
|
||||
| 'autocomplete'
|
||||
| 'forceUpdateKey'
|
||||
| 'disabled'
|
||||
| 'autoFocus'
|
||||
| 'autoSelect'
|
||||
| 'autocompleteVariables'
|
||||
| 'autocompleteFunctions'
|
||||
| 'onKeyDown'
|
||||
| 'readOnly'
|
||||
> & {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatSize } from '@yaakapp-internal/lib/formatSize';
|
||||
import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import {
|
||||
@@ -24,7 +23,6 @@ import { Button } from './Button';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import type { DropdownItem } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import { Editor } from './Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
||||
import { Icon } from './Icon';
|
||||
@@ -41,9 +39,11 @@ export interface PairEditorRef {
|
||||
|
||||
export type PairEditorProps = {
|
||||
allowFileValues?: boolean;
|
||||
allowMultilineValues?: boolean;
|
||||
className?: string;
|
||||
forceUpdateKey?: string;
|
||||
nameAutocomplete?: GenericCompletionConfig;
|
||||
nameAutocompleteFunctions?: boolean;
|
||||
nameAutocompleteVariables?: boolean;
|
||||
namePlaceholder?: string;
|
||||
nameValidate?: InputProps['validate'];
|
||||
@@ -52,6 +52,7 @@ export type PairEditorProps = {
|
||||
pairs: Pair[];
|
||||
stateKey: InputProps['stateKey'];
|
||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||
valueAutocompleteFunctions?: boolean;
|
||||
valueAutocompleteVariables?: boolean;
|
||||
valuePlaceholder?: string;
|
||||
valueType?: 'text' | 'password';
|
||||
@@ -79,9 +80,11 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
{
|
||||
stateKey,
|
||||
allowFileValues,
|
||||
allowMultilineValues,
|
||||
className,
|
||||
forceUpdateKey,
|
||||
nameAutocomplete,
|
||||
nameAutocompleteFunctions,
|
||||
nameAutocompleteVariables,
|
||||
namePlaceholder,
|
||||
nameValidate,
|
||||
@@ -89,6 +92,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
onChange,
|
||||
pairs: originalPairs,
|
||||
valueAutocomplete,
|
||||
valueAutocompleteFunctions,
|
||||
valueAutocompleteVariables,
|
||||
valuePlaceholder,
|
||||
valueType,
|
||||
@@ -229,6 +233,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
{hoveredIndex === i && <DropMarker />}
|
||||
<PairEditorRow
|
||||
allowFileValues={allowFileValues}
|
||||
allowMultilineValues={allowMultilineValues}
|
||||
className="py-1"
|
||||
forceFocusNamePairId={forceFocusNamePairId}
|
||||
forceFocusValuePairId={forceFocusValuePairId}
|
||||
@@ -236,6 +241,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
index={i}
|
||||
isLast={isLast}
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteFunctions={nameAutocompleteFunctions}
|
||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||
namePlaceholder={namePlaceholder}
|
||||
nameValidate={nameValidate}
|
||||
@@ -247,6 +253,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
pair={p}
|
||||
stateKey={stateKey}
|
||||
valueAutocomplete={valueAutocomplete}
|
||||
valueAutocompleteFunctions={valueAutocompleteFunctions}
|
||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
valueType={valueType}
|
||||
@@ -256,12 +263,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
);
|
||||
})}
|
||||
{!showAll && pairs.length > MAX_INITIAL_PAIRS && (
|
||||
<Button
|
||||
onClick={toggleShowAll}
|
||||
variant="border"
|
||||
className="m-2"
|
||||
size="xs"
|
||||
>
|
||||
<Button onClick={toggleShowAll} variant="border" className="m-2" size="xs">
|
||||
Show {pairs.length - MAX_INITIAL_PAIRS} More
|
||||
</Button>
|
||||
)}
|
||||
@@ -289,13 +291,16 @@ type PairEditorRowProps = {
|
||||
} & Pick<
|
||||
PairEditorProps,
|
||||
| 'allowFileValues'
|
||||
| 'allowMultilineValues'
|
||||
| 'forceUpdateKey'
|
||||
| 'nameAutocomplete'
|
||||
| 'nameAutocompleteVariables'
|
||||
| 'namePlaceholder'
|
||||
| 'nameValidate'
|
||||
| 'nameAutocompleteFunctions'
|
||||
| 'stateKey'
|
||||
| 'valueAutocomplete'
|
||||
| 'valueAutocompleteFunctions'
|
||||
| 'valueAutocompleteVariables'
|
||||
| 'valuePlaceholder'
|
||||
| 'valueType'
|
||||
@@ -304,6 +309,7 @@ type PairEditorRowProps = {
|
||||
|
||||
function PairEditorRow({
|
||||
allowFileValues,
|
||||
allowMultilineValues,
|
||||
className,
|
||||
forceFocusNamePairId,
|
||||
forceFocusValuePairId,
|
||||
@@ -311,9 +317,10 @@ function PairEditorRow({
|
||||
index,
|
||||
isLast,
|
||||
nameAutocomplete,
|
||||
nameAutocompleteVariables,
|
||||
namePlaceholder,
|
||||
nameValidate,
|
||||
nameAutocompleteFunctions,
|
||||
nameAutocompleteVariables,
|
||||
onChange,
|
||||
onDelete,
|
||||
onEnd,
|
||||
@@ -322,6 +329,7 @@ function PairEditorRow({
|
||||
pair,
|
||||
stateKey,
|
||||
valueAutocomplete,
|
||||
valueAutocompleteFunctions,
|
||||
valueAutocompleteVariables,
|
||||
valuePlaceholder,
|
||||
valueType,
|
||||
@@ -330,7 +338,6 @@ function PairEditorRow({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const nameInputRef = useRef<EditorView>(null);
|
||||
const valueInputRef = useRef<EditorView>(null);
|
||||
const valueLanguage = languageFromContentType(pair.contentType ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceFocusNamePairId === pair.id) {
|
||||
@@ -347,17 +354,6 @@ function PairEditorRow({
|
||||
const handleFocus = useCallback(() => onFocus?.(pair), [onFocus, pair]);
|
||||
const handleDelete = useCallback(() => onDelete?.(pair, false), [onDelete, pair]);
|
||||
|
||||
const deleteItems = useMemo(
|
||||
(): DropdownItem[] => [
|
||||
{
|
||||
label: 'Delete',
|
||||
onSelect: handleDelete,
|
||||
color: 'danger',
|
||||
},
|
||||
],
|
||||
[handleDelete],
|
||||
);
|
||||
|
||||
const handleChangeEnabled = useMemo(
|
||||
() => (enabled: boolean) => onChange({ ...pair, enabled }),
|
||||
[onChange, pair],
|
||||
@@ -396,11 +392,27 @@ function PairEditorRow({
|
||||
hide={hide}
|
||||
onChange={handleChangeValueText}
|
||||
defaultValue={pair.value}
|
||||
language={valueLanguage}
|
||||
contentType={pair.contentType ?? null}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[handleChangeValueText, pair.name, pair.value, valueLanguage],
|
||||
[handleChangeValueText, pair.contentType, pair.name, pair.value],
|
||||
);
|
||||
|
||||
const defaultItems = useMemo(
|
||||
(): DropdownItem[] => [
|
||||
{
|
||||
label: 'Edit Multi-line',
|
||||
onSelect: handleEditMultiLineValue,
|
||||
hidden: !allowMultilineValues,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onSelect: handleDelete,
|
||||
color: 'danger',
|
||||
},
|
||||
],
|
||||
[allowMultilineValues, handleDelete, handleEditMultiLineValue],
|
||||
);
|
||||
|
||||
const [, connectDrop] = useDrop<Pair>(
|
||||
@@ -484,7 +496,6 @@ function PairEditorRow({
|
||||
<Input
|
||||
ref={nameInputRef}
|
||||
hideLabel
|
||||
useTemplating
|
||||
stateKey={`name.${pair.id}.${stateKey}`}
|
||||
wrapLines={false}
|
||||
readOnly={pair.readOnlyName}
|
||||
@@ -501,6 +512,7 @@ function PairEditorRow({
|
||||
placeholder={namePlaceholder ?? 'name'}
|
||||
autocomplete={nameAutocomplete}
|
||||
autocompleteVariables={nameAutocompleteVariables}
|
||||
autocompleteFunctions={nameAutocompleteFunctions}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
|
||||
@@ -524,20 +536,19 @@ function PairEditorRow({
|
||||
size="sm"
|
||||
onClick={handleEditMultiLineValue}
|
||||
title={pair.value}
|
||||
className="text-xs font-mono"
|
||||
>
|
||||
Edit {formatSize(pair.value.length)}
|
||||
{pair.value.split('\n').join(' ')}
|
||||
</Button>
|
||||
) : (
|
||||
<Input
|
||||
ref={valueInputRef}
|
||||
hideLabel
|
||||
useTemplating
|
||||
stateKey={`value.${pair.id}.${stateKey}`}
|
||||
wrapLines={false}
|
||||
size="sm"
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
validate={valueValidate}
|
||||
language={valueLanguage}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
defaultValue={pair.value}
|
||||
label="Value"
|
||||
@@ -547,6 +558,7 @@ function PairEditorRow({
|
||||
type={isLast ? 'text' : valueType}
|
||||
placeholder={valuePlaceholder ?? 'value'}
|
||||
autocomplete={valueAutocomplete?.(pair.name)}
|
||||
autocompleteFunctions={valueAutocompleteFunctions}
|
||||
autocompleteVariables={valueAutocompleteVariables}
|
||||
/>
|
||||
)}
|
||||
@@ -562,7 +574,7 @@ function PairEditorRow({
|
||||
editMultiLine={handleEditMultiLineValue}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown items={deleteItems}>
|
||||
<Dropdown items={defaultItems}>
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
@@ -641,6 +653,7 @@ function FileActionsDropdown({
|
||||
onSelect: onDelete,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
color: 'danger',
|
||||
},
|
||||
],
|
||||
[editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
|
||||
@@ -673,16 +686,17 @@ function isPairEmpty(pair: Pair): boolean {
|
||||
|
||||
function MultilineEditDialog({
|
||||
defaultValue,
|
||||
language,
|
||||
contentType,
|
||||
onChange,
|
||||
hide,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
language: EditorProps['language'];
|
||||
contentType: string | null;
|
||||
onChange: (value: string) => void;
|
||||
hide: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState<string>(defaultValue);
|
||||
const language = languageFromContentType(contentType, value);
|
||||
return (
|
||||
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<Editor
|
||||
@@ -691,6 +705,8 @@ function MultilineEditDialog({
|
||||
language={language}
|
||||
onChange={setValue}
|
||||
stateKey={null}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
|
||||
@@ -57,7 +57,7 @@ export function TableHeaderCell({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left w-0')}>
|
||||
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left w-0 text-text-subtle')}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ export function HistoryDialog({ log }: Props) {
|
||||
{log.map((l, i) => (
|
||||
<TableRow key={i}>
|
||||
<TruncatedWideTableCell>{l.message || <em className="text-text-subtle">No message</em>}</TruncatedWideTableCell>
|
||||
<TableCell>{l.author.name ?? 'Unknown'}</TableCell>
|
||||
<TableCell><span title={`Email: ${l.author.email}`}>{l.author.name || 'Unknown'}</span></TableCell>
|
||||
<TableCell className="text-text-subtle">
|
||||
<span title={l.when}>{formatDistanceToNowStrict(l.when)} ago</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -11,9 +11,13 @@ const templateFunctionsAtom = atom<TemplateFunction[]>([]);
|
||||
|
||||
export function useTemplateFunctionCompletionOptions(
|
||||
onClick: (fn: TemplateFunction, ragTag: string, pos: number) => void,
|
||||
enabled: boolean,
|
||||
) {
|
||||
const templateFunctions = useAtomValue(templateFunctionsAtom);
|
||||
return useMemo<TwigCompletionOption[]>(() => {
|
||||
if (!enabled) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
templateFunctions.map((fn) => {
|
||||
const NUM_ARGS = 2;
|
||||
@@ -35,7 +39,7 @@ export function useTemplateFunctionCompletionOptions(
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}, [onClick, templateFunctions]);
|
||||
}, [enabled, onClick, templateFunctions]);
|
||||
}
|
||||
|
||||
export function useSubscribeTemplateFunctions() {
|
||||
|
||||
@@ -11,7 +11,13 @@ export function languageFromContentType(
|
||||
} else if (justContentType.includes('xml')) {
|
||||
return 'xml';
|
||||
} else if (justContentType.includes('html')) {
|
||||
return detectFromContent(content, 'html');
|
||||
const detected = detectFromContent(content, 'html');
|
||||
if (detected === 'xml') {
|
||||
// If it's detected as XML, but is already HTML, don't change it
|
||||
return 'html';
|
||||
} else {
|
||||
return detected;
|
||||
}
|
||||
} else if (justContentType.includes('javascript')) {
|
||||
return 'javascript';
|
||||
}
|
||||
@@ -26,7 +32,6 @@ function detectFromContent(
|
||||
if (content == null) return 'text';
|
||||
|
||||
const firstBytes = content.slice(0, 20).trim();
|
||||
console.log("FIRST BYTES", firstBytes);
|
||||
|
||||
if (firstBytes.startsWith('{') || firstBytes.startsWith('[')) {
|
||||
return 'json';
|
||||
|
||||
@@ -62,11 +62,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Style the scrollbars */
|
||||
* {
|
||||
/* Style the scrollbars
|
||||
* Mac doesn't like this (especially in CodeMirror) so we only do it on non-macos platforms. On Mac,
|
||||
* styling the scrollbar seems to cause them to not show up at all most of the time
|
||||
*/
|
||||
html:not([data-platform="macos"]) * {
|
||||
::-webkit-scrollbar-corner,
|
||||
::-webkit-scrollbar {
|
||||
@apply w-1.5 h-1.5;
|
||||
@apply w-[10px] h-[10px];
|
||||
}
|
||||
|
||||
.scrollbar-track,
|
||||
@@ -78,7 +81,7 @@
|
||||
&:hover {
|
||||
&.scrollbar-thumb,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-surface-highlight hover:bg-surface-highlight rounded-full;
|
||||
@apply bg-text-subtlest hover:bg-text-subtle rounded-[2px];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-router": "^1.111.3",
|
||||
"@tanstack/react-virtual": "^3.13.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||
"@tauri-apps/plugin-log": "^2.0.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.2",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@tauri-apps/api": "^2.3.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-log": "^2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
|
||||
Reference in New Issue
Block a user