mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
Template Tag Function Editor (#67)

This commit is contained in:
@@ -18,6 +18,7 @@ module.exports = {
|
|||||||
'plugin-runtime-types/**/*',
|
'plugin-runtime-types/**/*',
|
||||||
'src-tauri/**/*',
|
'src-tauri/**/*',
|
||||||
'plugins/**/*',
|
'plugins/**/*',
|
||||||
|
'tailwind.config.cjs',
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -38,6 +38,7 @@
|
|||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
|
"jotai": "^2.9.3",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.309.0",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -7423,6 +7424,26 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jotai": {
|
||||||
|
"version": "2.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.9.3.tgz",
|
||||||
|
"integrity": "sha512-IqMWKoXuEzWSShjd9UhalNsRGbdju5G2FrqNLQJT+Ih6p41VNYe2sav5hnwQx4HJr25jq9wRqvGSWGviGG6Gjw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=17.0.0",
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-base64": {
|
"node_modules/js-base64": {
|
||||||
"version": "3.7.7",
|
"version": "3.7.7",
|
||||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
|
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
|
||||||
|
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
|
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
||||||
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
|
|
||||||
"@yaakapp/api": "^0.1.6",
|
"@yaakapp/api": "^0.1.6",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
|
"jotai": "^2.9.3",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.309.0",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
|
|||||||
4
plugin-runtime-types/package-lock.json
generated
4
plugin-runtime-types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.1.0-beta.4",
|
"version": "0.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.1.0-beta.4",
|
"version": "0.1.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^22.0.0"
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { HttpRequestActionPlugin } from './httpRequestAction';
|
|||||||
import { ImporterPlugin } from './import';
|
import { ImporterPlugin } from './import';
|
||||||
import { ThemePlugin } from './theme';
|
import { ThemePlugin } from './theme';
|
||||||
|
|
||||||
export { YaakContext } from './context';
|
export type { YaakContext } from './context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The global structure of a Yaak plugin
|
* The global structure of a Yaak plugin
|
||||||
|
|||||||
19
src-tauri/Cargo.lock
generated
19
src-tauri/Cargo.lock
generated
@@ -4919,9 +4919,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.205"
|
version = "1.0.208"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
|
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@@ -4949,9 +4949,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.205"
|
version = "1.0.208"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
|
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4971,9 +4971,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.122"
|
version = "1.0.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.11",
|
"itoa 1.0.11",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -5844,9 +5844,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-clipboard-manager"
|
name = "tauri-plugin-clipboard-manager"
|
||||||
version = "2.0.0-rc.0"
|
version = "2.1.0-beta.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76a26868f7e05a09673e4172d23acb82cd48911cca092f0e8d06179a69e5024c"
|
checksum = "becbc5a692e842f8d6a7ab5e490c3c36d267b5c3d5bf4b6a0cdd039d7df25569"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"image 0.24.9",
|
"image 0.24.9",
|
||||||
@@ -7649,6 +7649,9 @@ name = "yaak_templates"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ts-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ serde_json = { version = "1.0.116", features = ["raw_value"] }
|
|||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
tauri = { workspace = true, features = ["unstable"] }
|
tauri = { workspace = true, features = ["unstable"] }
|
||||||
tauri-plugin-shell = { workspace = true }
|
tauri-plugin-shell = { workspace = true }
|
||||||
tauri-plugin-clipboard-manager = "2.0.0-rc.0"
|
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
|
||||||
tauri-plugin-dialog = "2.0.0-rc.0"
|
tauri-plugin-dialog = "2.0.0-rc.0"
|
||||||
tauri-plugin-fs = "2.0.0-rc.0"
|
tauri-plugin-fs = "2.0.0-rc.0"
|
||||||
tauri-plugin-log = { version = "2.0.0-rc.0", features = ["colored"] }
|
tauri-plugin-log = { version = "2.0.0-rc.0", features = ["colored"] }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ use crate::export_resources::{get_workspace_export_resources, WorkspaceExportRes
|
|||||||
use crate::grpc::metadata_to_map;
|
use crate::grpc::metadata_to_map;
|
||||||
use crate::http_request::send_http_request;
|
use crate::http_request::send_http_request;
|
||||||
use crate::notifications::YaakNotifier;
|
use crate::notifications::YaakNotifier;
|
||||||
use crate::render::{render_request, variables_from_environment};
|
use crate::render::{render_request, render_template, variables_from_environment};
|
||||||
use crate::updates::{UpdateMode, YaakUpdater};
|
use crate::updates::{UpdateMode, YaakUpdater};
|
||||||
use crate::window_menu::app_menu;
|
use crate::window_menu::app_menu;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
@@ -60,6 +60,7 @@ use yaak_plugin_runtime::events::{
|
|||||||
GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse,
|
GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse,
|
||||||
SendHttpRequestResponse,
|
SendHttpRequestResponse,
|
||||||
};
|
};
|
||||||
|
use yaak_templates::{parse_and_render, Parser, Tokens};
|
||||||
|
|
||||||
mod analytics;
|
mod analytics;
|
||||||
mod export_resources;
|
mod export_resources;
|
||||||
@@ -99,6 +100,38 @@ 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_template_tokens_to_string(tokens: Tokens) -> Result<String, String> {
|
||||||
|
Ok(tokens.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_render_template(
|
||||||
|
window: WebviewWindow,
|
||||||
|
template: &str,
|
||||||
|
workspace_id: &str,
|
||||||
|
environment_id: Option<&str>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let environment = match environment_id {
|
||||||
|
Some(id) => Some(
|
||||||
|
get_environment(&window, id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let workspace = get_workspace(&window, &workspace_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let rendered = render_template(template, &workspace, environment.as_ref());
|
||||||
|
Ok(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_dismiss_notification(
|
async fn cmd_dismiss_notification(
|
||||||
window: WebviewWindow,
|
window: WebviewWindow,
|
||||||
@@ -1641,6 +1674,9 @@ pub fn run() {
|
|||||||
cmd_delete_http_response,
|
cmd_delete_http_response,
|
||||||
cmd_delete_workspace,
|
cmd_delete_workspace,
|
||||||
cmd_dismiss_notification,
|
cmd_dismiss_notification,
|
||||||
|
cmd_parse_template,
|
||||||
|
cmd_template_tokens_to_string,
|
||||||
|
cmd_render_template,
|
||||||
cmd_duplicate_grpc_request,
|
cmd_duplicate_grpc_request,
|
||||||
cmd_duplicate_http_request,
|
cmd_duplicate_http_request,
|
||||||
cmd_export_data,
|
cmd_export_data,
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use serde_json::Value;
|
|
||||||
use crate::template_fns::timestamp;
|
use crate::template_fns::timestamp;
|
||||||
use yaak_templates::parse_and_render;
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
|
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
|
||||||
};
|
};
|
||||||
|
use yaak_templates::parse_and_render;
|
||||||
|
|
||||||
|
pub fn render_template(template: &str, w: &Workspace, e: Option<&Environment>) -> String {
|
||||||
|
let vars = &variables_from_environment(w, e);
|
||||||
|
render(template, vars)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
|
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
|
||||||
let r = r.clone();
|
let r = r.clone();
|
||||||
|
|||||||
@@ -5,3 +5,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
|
serde = { version = "1.0.208", features = ["derive"] }
|
||||||
|
serde_json = "1.0.125"
|
||||||
|
ts-rs = { version = "9.0.1" }
|
||||||
|
|||||||
6
src-tauri/yaak_templates/build.rs
Normal file
6
src-tauri/yaak_templates/build.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Tell ts-rs where to generate types to
|
||||||
|
println!("cargo:rustc-env=TS_RS_EXPORT_DIR=../../src-web/gen");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -2,6 +2,4 @@ pub mod parser;
|
|||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
|
||||||
pub use parser::*;
|
pub use parser::*;
|
||||||
pub use renderer::*;
|
pub use renderer::*;
|
||||||
|
|
||||||
pub fn template_foo() {}
|
|
||||||
@@ -1,23 +1,92 @@
|
|||||||
#[derive(Clone, PartialEq, Debug)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct Tokens {
|
||||||
|
pub tokens: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Tokens {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = self
|
||||||
|
.tokens
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("");
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct FnArg {
|
pub struct FnArg {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: Val,
|
pub value: Val,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
impl Display for FnArg {
|
||||||
pub enum Val {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Str(String),
|
let str = format!("{}={}", self.name, self.value);
|
||||||
Var(String),
|
write!(f, "{}", str)
|
||||||
Fn { name: String, args: Vec<FnArg> },
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum Val {
|
||||||
|
Str { text: String },
|
||||||
|
Var { name: String },
|
||||||
|
Fn { name: String, args: Vec<FnArg> },
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Val {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = match self {
|
||||||
|
Val::Str { text } => format!(r#""{}""#, text.to_string().replace(r#"""#, r#"\""#)),
|
||||||
|
Val::Var { name } => name.to_string(),
|
||||||
|
Val::Fn { name, args } => {
|
||||||
|
format!(
|
||||||
|
"{name}({})",
|
||||||
|
args.iter()
|
||||||
|
.filter_map(|a| match a.value.clone() {
|
||||||
|
Val::Null => None,
|
||||||
|
_ => Some(a.to_string()),
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Val::Null => "null".to_string(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
|
#[ts(export)]
|
||||||
pub enum Token {
|
pub enum Token {
|
||||||
Raw(String),
|
Raw { text: String },
|
||||||
Tag(Val),
|
Tag { val: Val },
|
||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Token {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = match self {
|
||||||
|
Token::Raw { text } => text.to_string(),
|
||||||
|
Token::Tag { val } => format!("${{[ {} ]}}", val.to_string()),
|
||||||
|
Token::Eof => "".to_string(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Template Syntax
|
// Template Syntax
|
||||||
//
|
//
|
||||||
// ${[ my_var ]}
|
// ${[ my_var ]}
|
||||||
@@ -42,7 +111,7 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(&mut self) -> Vec<Token> {
|
pub fn parse(&mut self) -> Tokens {
|
||||||
let start_pos = self.pos;
|
let start_pos = self.pos;
|
||||||
|
|
||||||
while self.pos < self.chars.len() {
|
while self.pos < self.chars.len() {
|
||||||
@@ -65,7 +134,9 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.push_token(Token::Eof);
|
self.push_token(Token::Eof);
|
||||||
self.tokens.clone()
|
Tokens {
|
||||||
|
tokens: self.tokens.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_tag(&mut self) -> Option<Token> {
|
fn parse_tag(&mut self) -> Option<Token> {
|
||||||
@@ -85,7 +156,7 @@ impl Parser {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Token::Tag(val))
|
Some(Token::Tag { val })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -103,9 +174,13 @@ impl Parser {
|
|||||||
if let Some((name, args)) = self.parse_fn() {
|
if let Some((name, args)) = self.parse_fn() {
|
||||||
Some(Val::Fn { name, args })
|
Some(Val::Fn { name, args })
|
||||||
} else if let Some(v) = self.parse_ident() {
|
} else if let Some(v) = self.parse_ident() {
|
||||||
Some(Val::Var(v))
|
if v == "null" {
|
||||||
|
Some(Val::Null)
|
||||||
|
} else {
|
||||||
|
Some(Val::Var { name: v })
|
||||||
|
}
|
||||||
} else if let Some(v) = self.parse_string() {
|
} else if let Some(v) = self.parse_string() {
|
||||||
Some(Val::Str(v))
|
Some(Val::Str { text: v })
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -145,7 +220,7 @@ impl Parser {
|
|||||||
// Fn closed immediately
|
// Fn closed immediately
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.match_str(")") {
|
if self.match_str(")") {
|
||||||
return Some(args)
|
return Some(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
while self.pos < self.chars.len() {
|
while self.pos < self.chars.len() {
|
||||||
@@ -183,7 +258,7 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(args);
|
Some(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ident(&mut self) -> Option<String> {
|
fn parse_ident(&mut self) -> Option<String> {
|
||||||
@@ -209,7 +284,7 @@ impl Parser {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(text);
|
Some(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_string(&mut self) -> Option<String> {
|
fn parse_string(&mut self) -> Option<String> {
|
||||||
@@ -246,7 +321,7 @@ impl Parser {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(text);
|
Some(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_whitespace(&mut self) {
|
fn skip_whitespace(&mut self) {
|
||||||
@@ -274,7 +349,9 @@ impl Parser {
|
|||||||
fn push_token(&mut self, token: Token) {
|
fn push_token(&mut self, token: Token) {
|
||||||
// Push any text we've accumulated
|
// Push any text we've accumulated
|
||||||
if !self.curr_text.is_empty() {
|
if !self.curr_text.is_empty() {
|
||||||
let text_token = Token::Raw(self.curr_text.clone());
|
let text_token = Token::Raw {
|
||||||
|
text: self.curr_text.clone(),
|
||||||
|
};
|
||||||
self.tokens.push(text_token);
|
self.tokens.push(text_token);
|
||||||
self.curr_text.clear();
|
self.curr_text.clear();
|
||||||
}
|
}
|
||||||
@@ -303,14 +380,20 @@ impl Parser {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::Val::Null;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn var_simple() {
|
fn var_simple() {
|
||||||
let mut p = Parser::new("${[ foo ]}");
|
let mut p = Parser::new("${[ foo ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![Token::Tag(Val::Var("foo".into())), Token::Eof]
|
vec![
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Var { name: "foo".into() }
|
||||||
|
},
|
||||||
|
Token::Eof
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,8 +401,13 @@ mod tests {
|
|||||||
fn var_multiple_names_invalid() {
|
fn var_multiple_names_invalid() {
|
||||||
let mut p = Parser::new("${[ foo bar ]}");
|
let mut p = Parser::new("${[ foo bar ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![Token::Raw("${[ foo bar ]}".into()), Token::Eof]
|
vec![
|
||||||
|
Token::Raw {
|
||||||
|
text: "${[ foo bar ]}".into()
|
||||||
|
},
|
||||||
|
Token::Eof
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +415,15 @@ mod tests {
|
|||||||
fn tag_string() {
|
fn tag_string() {
|
||||||
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
|
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof]
|
vec![
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Str {
|
||||||
|
text: r#"foo "bar" baz"#.into()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Token::Eof
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +431,17 @@ mod tests {
|
|||||||
fn var_surrounded() {
|
fn var_surrounded() {
|
||||||
let mut p = Parser::new("Hello ${[ foo ]}!");
|
let mut p = Parser::new("Hello ${[ foo ]}!");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Raw("Hello ".to_string()),
|
Token::Raw {
|
||||||
Token::Tag(Val::Var("foo".into())),
|
text: "Hello ".to_string()
|
||||||
Token::Raw("!".to_string()),
|
},
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Var { name: "foo".into() }
|
||||||
|
},
|
||||||
|
Token::Raw {
|
||||||
|
text: "!".to_string()
|
||||||
|
},
|
||||||
Token::Eof,
|
Token::Eof,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -350,12 +451,14 @@ mod tests {
|
|||||||
fn fn_simple() {
|
fn fn_simple() {
|
||||||
let mut p = Parser::new("${[ foo() ]}");
|
let mut p = Parser::new("${[ foo() ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "foo".into(),
|
val: Val::Fn {
|
||||||
args: Vec::new(),
|
name: "foo".into(),
|
||||||
}),
|
args: Vec::new(),
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -365,15 +468,17 @@ mod tests {
|
|||||||
fn fn_ident_arg() {
|
fn fn_ident_arg() {
|
||||||
let mut p = Parser::new("${[ foo(a=bar) ]}");
|
let mut p = Parser::new("${[ foo(a=bar) ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "foo".into(),
|
val: Val::Fn {
|
||||||
args: vec![FnArg {
|
name: "foo".into(),
|
||||||
name: "a".into(),
|
args: vec![FnArg {
|
||||||
value: Val::Var("bar".into())
|
name: "a".into(),
|
||||||
}],
|
value: Val::Var { name: "bar".into() }
|
||||||
}),
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -383,25 +488,27 @@ mod tests {
|
|||||||
fn fn_ident_args() {
|
fn fn_ident_args() {
|
||||||
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
|
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "foo".into(),
|
val: Val::Fn {
|
||||||
args: vec![
|
name: "foo".into(),
|
||||||
FnArg {
|
args: vec![
|
||||||
name: "a".into(),
|
FnArg {
|
||||||
value: Val::Var("bar".into())
|
name: "a".into(),
|
||||||
},
|
value: Val::Var { name: "bar".into() }
|
||||||
FnArg {
|
},
|
||||||
name: "b".into(),
|
FnArg {
|
||||||
value: Val::Var("baz".into())
|
name: "b".into(),
|
||||||
},
|
value: Val::Var { name: "baz".into() }
|
||||||
FnArg {
|
},
|
||||||
name: "c".into(),
|
FnArg {
|
||||||
value: Val::Var("qux".into())
|
name: "c".into(),
|
||||||
},
|
value: Val::Var { name: "qux".into() }
|
||||||
],
|
},
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -411,25 +518,29 @@ mod tests {
|
|||||||
fn fn_mixed_args() {
|
fn fn_mixed_args() {
|
||||||
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux ) ]}"#);
|
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux ) ]}"#);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "foo".into(),
|
val: Val::Fn {
|
||||||
args: vec![
|
name: "foo".into(),
|
||||||
FnArg {
|
args: vec![
|
||||||
name: "aaa".into(),
|
FnArg {
|
||||||
value: Val::Var("bar".into())
|
name: "aaa".into(),
|
||||||
},
|
value: Val::Var { name: "bar".into() }
|
||||||
FnArg {
|
},
|
||||||
name: "bb".into(),
|
FnArg {
|
||||||
value: Val::Str(r#"baz "hi""#.into())
|
name: "bb".into(),
|
||||||
},
|
value: Val::Str {
|
||||||
FnArg {
|
text: r#"baz "hi""#.into()
|
||||||
name: "c".into(),
|
}
|
||||||
value: Val::Var("qux".into())
|
},
|
||||||
},
|
FnArg {
|
||||||
],
|
name: "c".into(),
|
||||||
}),
|
value: Val::Var { name: "qux".into() }
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -439,18 +550,20 @@ mod tests {
|
|||||||
fn fn_nested() {
|
fn fn_nested() {
|
||||||
let mut p = Parser::new("${[ foo(b=bar()) ]}");
|
let mut p = Parser::new("${[ foo(b=bar()) ]}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "foo".into(),
|
val: Val::Fn {
|
||||||
args: vec![FnArg {
|
name: "foo".into(),
|
||||||
name: "b".into(),
|
args: vec![FnArg {
|
||||||
value: Val::Fn {
|
name: "b".into(),
|
||||||
name: "bar".into(),
|
value: Val::Fn {
|
||||||
args: vec![],
|
name: "bar".into(),
|
||||||
}
|
args: vec![],
|
||||||
}],
|
}
|
||||||
}),
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -460,35 +573,134 @@ mod tests {
|
|||||||
fn fn_nested_args() {
|
fn fn_nested_args() {
|
||||||
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b="i"), c="o") ]}"#);
|
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b="i"), c="o") ]}"#);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse(),
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
Token::Tag(Val::Fn {
|
Token::Tag {
|
||||||
name: "outer".into(),
|
val: Val::Fn {
|
||||||
args: vec![
|
name: "outer".into(),
|
||||||
FnArg {
|
args: vec![
|
||||||
name: "a".into(),
|
FnArg {
|
||||||
value: Val::Fn {
|
name: "a".into(),
|
||||||
name: "inner".into(),
|
value: Val::Fn {
|
||||||
args: vec![
|
name: "inner".into(),
|
||||||
FnArg {
|
args: vec![
|
||||||
name: "a".into(),
|
FnArg {
|
||||||
value: Val::Var("foo".into())
|
name: "a".into(),
|
||||||
},
|
value: Val::Var { name: "foo".into() }
|
||||||
FnArg {
|
},
|
||||||
name: "b".into(),
|
FnArg {
|
||||||
value: Val::Str("i".into()),
|
name: "b".into(),
|
||||||
},
|
value: Val::Str { text: "i".into() },
|
||||||
],
|
},
|
||||||
}
|
],
|
||||||
},
|
}
|
||||||
FnArg {
|
},
|
||||||
name: "c".into(),
|
FnArg {
|
||||||
value: Val::Str("o".into())
|
name: "c".into(),
|
||||||
},
|
value: Val::Str { text: "o".into() }
|
||||||
],
|
},
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
Token::Eof
|
Token::Eof
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_display_var() {
|
||||||
|
assert_eq!(
|
||||||
|
Val::Var {
|
||||||
|
name: "foo".to_string()
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
"foo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_display_str() {
|
||||||
|
assert_eq!(
|
||||||
|
Val::Str {
|
||||||
|
text: r#"Hello "You""#.to_string()
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#""Hello \"You\"""#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_null_fn_arg() {
|
||||||
|
assert_eq!(
|
||||||
|
Val::Fn {
|
||||||
|
name: "fn".to_string(),
|
||||||
|
args: vec![
|
||||||
|
FnArg {
|
||||||
|
name: "n".to_string(),
|
||||||
|
value: Null,
|
||||||
|
},
|
||||||
|
FnArg {
|
||||||
|
name: "a".to_string(),
|
||||||
|
value: Val::Str {
|
||||||
|
text: "aaa".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"fn(a="aaa")"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_display_fn() {
|
||||||
|
assert_eq!(
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Fn {
|
||||||
|
name: "foo".to_string(),
|
||||||
|
args: vec![
|
||||||
|
FnArg {
|
||||||
|
name: "arg".to_string(),
|
||||||
|
value: Val::Str {
|
||||||
|
text: "v".to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FnArg {
|
||||||
|
name: "arg2".to_string(),
|
||||||
|
value: Val::Var {
|
||||||
|
name: "my_var".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"${[ foo(arg="v", arg2=my_var) ]}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokens_display() {
|
||||||
|
assert_eq!(
|
||||||
|
Tokens {
|
||||||
|
tokens: vec![
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Var {
|
||||||
|
name: "my_var".to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Token::Raw {
|
||||||
|
text: " Some cool text ".to_string(),
|
||||||
|
},
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Str {
|
||||||
|
text: "Hello World".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"${[ my_var ]} Some cool text ${[ "Hello World" ]}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{FnArg, Parser, Token, Val};
|
use crate::{FnArg, Parser, Token, Tokens, Val};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -15,27 +15,27 @@ pub fn parse_and_render(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
tokens: Vec<Token>,
|
tokens: Tokens,
|
||||||
vars: &HashMap<String, String>,
|
vars: &HashMap<String, String>,
|
||||||
cb: Option<TemplateCallback>,
|
cb: Option<TemplateCallback>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut doc_str: Vec<String> = Vec::new();
|
let mut doc_str: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for t in tokens {
|
for t in tokens.tokens {
|
||||||
match t {
|
match t {
|
||||||
Token::Raw(s) => doc_str.push(s),
|
Token::Raw { text } => doc_str.push(text),
|
||||||
Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)),
|
Token::Tag { val } => doc_str.push(render_tag(val, &vars, cb)),
|
||||||
Token::Eof => {}
|
Token::Eof => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc_str.join("");
|
doc_str.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallback>) -> String {
|
fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallback>) -> String {
|
||||||
match val {
|
match val {
|
||||||
Val::Str(s) => s.into(),
|
Val::Str { text } => text.into(),
|
||||||
Val::Var(name) => match vars.get(name.as_str()) {
|
Val::Var { name } => match vars.get(name.as_str()) {
|
||||||
Some(v) => v.to_string(),
|
Some(v) => v.to_string(),
|
||||||
None => "".into(),
|
None => "".into(),
|
||||||
},
|
},
|
||||||
@@ -46,14 +46,14 @@ fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallb
|
|||||||
.map(|a| match a {
|
.map(|a| match a {
|
||||||
FnArg {
|
FnArg {
|
||||||
name,
|
name,
|
||||||
value: Val::Str(s),
|
value: Val::Str { text },
|
||||||
} => (name.to_string(), s.to_string()),
|
} => (name.to_string(), text.to_string()),
|
||||||
FnArg {
|
FnArg {
|
||||||
name,
|
name,
|
||||||
value: Val::Var(i),
|
value: Val::Var { name: var_name },
|
||||||
} => (
|
} => (
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
vars.get(i.as_str()).unwrap_or(&empty).to_string(),
|
vars.get(var_name.as_str()).unwrap_or(&empty).to_string(),
|
||||||
),
|
),
|
||||||
FnArg { name, value: val } => {
|
FnArg { name, value: val } => {
|
||||||
(name.to_string(), render_tag(val.clone(), vars, cb))
|
(name.to_string(), render_tag(val.clone(), vars, cb))
|
||||||
@@ -64,13 +64,17 @@ fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallb
|
|||||||
Some(cb) => match cb(name.as_str(), resolved_args.clone()) {
|
Some(cb) => match cb(name.as_str(), resolved_args.clone()) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to run template callback {}({:?}): {}", name, resolved_args, e);
|
warn!(
|
||||||
|
"Failed to run template callback {}({:?}): {}",
|
||||||
|
name, resolved_args, e
|
||||||
|
);
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => "".into(),
|
None => "".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Val::Null => "".into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +151,7 @@ mod tests {
|
|||||||
result.to_string()
|
result.to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_fn_err() {
|
fn render_fn_err() {
|
||||||
let vars = HashMap::new();
|
let vars = HashMap::new();
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
import type { Model } from '@yaakapp/api';
|
import type { Model } from '@yaakapp/api';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar';
|
import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar';
|
||||||
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
||||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||||
import { useCopy } from '../hooks/useCopy';
|
import { useCopy } from '../hooks/useCopy';
|
||||||
import { environmentsQueryKey } from '../hooks/useEnvironments';
|
import { environmentsAtom } from '../hooks/useEnvironments';
|
||||||
import { foldersQueryKey } from '../hooks/useFolders';
|
import { foldersQueryKey } from '../hooks/useFolders';
|
||||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||||
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
||||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
|
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||||
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
|
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
|
||||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||||
@@ -25,7 +26,7 @@ import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
|||||||
import { settingsQueryKey, useSettings } from '../hooks/useSettings';
|
import { settingsQueryKey, useSettings } from '../hooks/useSettings';
|
||||||
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
|
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
|
||||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
import { workspacesAtom } from '../hooks/useWorkspaces';
|
||||||
import { useZoom } from '../hooks/useZoom';
|
import { useZoom } from '../hooks/useZoom';
|
||||||
import { extractKeyValue } from '../lib/keyValueStore';
|
import { extractKeyValue } from '../lib/keyValueStore';
|
||||||
import { modelsEq } from '../lib/models';
|
import { modelsEq } from '../lib/models';
|
||||||
@@ -64,25 +65,22 @@ export function GlobalHooks() {
|
|||||||
windowLabel: string;
|
windowLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setWorkspaces = useSetAtom(workspacesAtom);
|
||||||
|
const setHttpRequests = useSetAtom(httpRequestsAtom);
|
||||||
|
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
|
||||||
|
const setEnvironments = useSetAtom(environmentsAtom);
|
||||||
|
|
||||||
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
|
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
|
||||||
const { model, windowLabel } = payload;
|
const { model, windowLabel } = payload;
|
||||||
const queryKey =
|
const queryKey =
|
||||||
model.model === 'http_request'
|
model.model === 'http_response'
|
||||||
? httpRequestsQueryKey(model)
|
|
||||||
: model.model === 'http_response'
|
|
||||||
? httpResponsesQueryKey(model)
|
? httpResponsesQueryKey(model)
|
||||||
: model.model === 'folder'
|
: model.model === 'folder'
|
||||||
? foldersQueryKey(model)
|
? foldersQueryKey(model)
|
||||||
: model.model === 'environment'
|
|
||||||
? environmentsQueryKey(model)
|
|
||||||
: model.model === 'grpc_connection'
|
: model.model === 'grpc_connection'
|
||||||
? grpcConnectionsQueryKey(model)
|
? grpcConnectionsQueryKey(model)
|
||||||
: model.model === 'grpc_event'
|
: model.model === 'grpc_event'
|
||||||
? grpcEventsQueryKey(model)
|
? grpcEventsQueryKey(model)
|
||||||
: model.model === 'grpc_request'
|
|
||||||
? grpcRequestsQueryKey(model)
|
|
||||||
: model.model === 'workspace'
|
|
||||||
? workspacesQueryKey(model)
|
|
||||||
: model.model === 'key_value'
|
: model.model === 'key_value'
|
||||||
? keyValueQueryKey(model)
|
? keyValueQueryKey(model)
|
||||||
: model.model === 'cookie_jar'
|
: model.model === 'cookie_jar'
|
||||||
@@ -91,11 +89,6 @@ export function GlobalHooks() {
|
|||||||
? settingsQueryKey()
|
? settingsQueryKey()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (queryKey === null) {
|
|
||||||
console.log('Unrecognized updated model:', model);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
|
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
|
||||||
wasUpdatedExternally(model.id);
|
wasUpdatedExternally(model.id);
|
||||||
}
|
}
|
||||||
@@ -106,21 +99,27 @@ export function GlobalHooks() {
|
|||||||
|
|
||||||
if (shouldIgnoreModel(model, windowLabel)) return;
|
if (shouldIgnoreModel(model, windowLabel)) return;
|
||||||
|
|
||||||
queryClient.setQueryData(queryKey, (current: unknown) => {
|
if (model.model === 'workspace') {
|
||||||
if (model.model === 'key_value') {
|
setWorkspaces(updateModelList(model, pushToFront));
|
||||||
// Special-case for KeyValue
|
} else if (model.model === 'http_request') {
|
||||||
return extractKeyValue(model);
|
setHttpRequests(updateModelList(model, pushToFront));
|
||||||
}
|
} else if (model.model === 'grpc_request') {
|
||||||
|
setGrpcRequests(updateModelList(model, pushToFront));
|
||||||
if (Array.isArray(current)) {
|
} else if (model.model === 'environment') {
|
||||||
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
|
setEnvironments(updateModelList(model, pushToFront));
|
||||||
if (index >= 0) {
|
} else if (queryKey != null) {
|
||||||
return [...current.slice(0, index), model, ...current.slice(index + 1)];
|
// TODO: Convert all models to use Jotai
|
||||||
} else {
|
queryClient.setQueryData(queryKey, (current: unknown) => {
|
||||||
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
|
if (model.model === 'key_value') {
|
||||||
|
// Special-case for KeyValue
|
||||||
|
return extractKeyValue(model);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
if (Array.isArray(current)) {
|
||||||
|
return updateModelList(model, pushToFront)(current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
|
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
|
||||||
@@ -128,17 +127,17 @@ export function GlobalHooks() {
|
|||||||
if (shouldIgnoreModel(model, windowLabel)) return;
|
if (shouldIgnoreModel(model, windowLabel)) return;
|
||||||
|
|
||||||
if (model.model === 'workspace') {
|
if (model.model === 'workspace') {
|
||||||
queryClient.setQueryData(workspacesQueryKey(), removeById(model));
|
setWorkspaces(removeById(model));
|
||||||
} else if (model.model === 'http_request') {
|
} else if (model.model === 'http_request') {
|
||||||
queryClient.setQueryData(httpRequestsQueryKey(model), removeById(model));
|
setHttpRequests(removeById(model));
|
||||||
} else if (model.model === 'http_response') {
|
} else if (model.model === 'http_response') {
|
||||||
queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model));
|
queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model));
|
||||||
} else if (model.model === 'folder') {
|
} else if (model.model === 'folder') {
|
||||||
queryClient.setQueryData(foldersQueryKey(model), removeById(model));
|
queryClient.setQueryData(foldersQueryKey(model), removeById(model));
|
||||||
} else if (model.model === 'environment') {
|
} else if (model.model === 'environment') {
|
||||||
queryClient.setQueryData(environmentsQueryKey(model), removeById(model));
|
setEnvironments(removeById(model));
|
||||||
} else if (model.model === 'grpc_request') {
|
} else if (model.model === 'grpc_request') {
|
||||||
queryClient.setQueryData(grpcRequestsQueryKey(model), removeById(model));
|
setGrpcRequests(removeById(model));
|
||||||
} else if (model.model === 'grpc_connection') {
|
} else if (model.model === 'grpc_connection') {
|
||||||
queryClient.setQueryData(grpcConnectionsQueryKey(model), removeById(model));
|
queryClient.setQueryData(grpcConnectionsQueryKey(model), removeById(model));
|
||||||
} else if (model.model === 'grpc_event') {
|
} else if (model.model === 'grpc_event') {
|
||||||
@@ -192,8 +191,19 @@ export function GlobalHooks() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateModelList<T extends Model>(model: T, pushToFront: boolean) {
|
||||||
|
return (current: T[]): T[] => {
|
||||||
|
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
|
||||||
|
if (index >= 0) {
|
||||||
|
return [...current.slice(0, index), model, ...current.slice(index + 1)];
|
||||||
|
} else {
|
||||||
|
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function removeById<T extends { id: string }>(model: T) {
|
function removeById<T extends { id: string }>(model: T) {
|
||||||
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id);
|
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldIgnoreModel = (payload: Model, windowLabel: string) => {
|
const shouldIgnoreModel = (payload: Model, windowLabel: string) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
|
|
||||||
export function IsDev({ children }: Props) {
|
export function IsDev({ children }: Props) {
|
||||||
const appInfo = useAppInfo();
|
const appInfo = useAppInfo();
|
||||||
if (!appInfo?.isDev) {
|
if (!appInfo.isDev) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import type { GrpcRequest, HttpRequest } from '@yaakapp/api';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
|
||||||
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
|
|
||||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
import type { GrpcRequest, HttpRequest } from '@yaakapp/api';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { Select } from './core/Select';
|
import { Select } from './core/Select';
|
||||||
@@ -22,7 +19,6 @@ interface Props {
|
|||||||
|
|
||||||
export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Props) {
|
export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Props) {
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -52,14 +48,8 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
|
|||||||
|
|
||||||
if (request.model === 'http_request') {
|
if (request.model === 'http_request') {
|
||||||
await updateHttpRequest.mutateAsync(args);
|
await updateHttpRequest.mutateAsync(args);
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: httpRequestsQueryKey({ workspaceId: activeWorkspaceId }),
|
|
||||||
});
|
|
||||||
} else if (request.model === 'grpc_request') {
|
} else if (request.model === 'grpc_request') {
|
||||||
await updateGrpcRequest.mutateAsync(args);
|
await updateGrpcRequest.mutateAsync(args);
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: grpcRequestsQueryKey({ workspaceId: activeWorkspaceId }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide after a moment, to give time for request to disappear
|
// Hide after a moment, to give time for request to disappear
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ export function SettingsGeneral() {
|
|||||||
|
|
||||||
<Heading size={2}>App Info</Heading>
|
<Heading size={2}>App Info</Heading>
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
<KeyValueRow label="Version" value={appInfo?.version} />
|
<KeyValueRow label="Version" value={appInfo.version} />
|
||||||
<KeyValueRow label="Data Directory" value={appInfo?.appDataDir} />
|
<KeyValueRow label="Data Directory" value={appInfo.appDataDir} />
|
||||||
<KeyValueRow label="Logs Directory" value={appInfo?.appLogDir} />
|
<KeyValueRow label="Logs Directory" value={appInfo.appLogDir} />
|
||||||
</KeyValueRows>
|
</KeyValueRows>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function SettingsDropdown() {
|
|||||||
leftSlot: <Icon icon="folderOutput" />,
|
leftSlot: <Icon icon="folderOutput" />,
|
||||||
onSelect: () => exportData.mutate(),
|
onSelect: () => exportData.mutate(),
|
||||||
},
|
},
|
||||||
{ type: 'separator', label: `Yaak v${appInfo?.version}` },
|
{ type: 'separator', label: `Yaak v${appInfo.version}` },
|
||||||
{
|
{
|
||||||
key: 'update-check',
|
key: 'update-check',
|
||||||
label: 'Check for Updates',
|
label: 'Check for Updates',
|
||||||
@@ -92,7 +92,7 @@ export function SettingsDropdown() {
|
|||||||
label: 'Changelog',
|
label: 'Changelog',
|
||||||
leftSlot: <Icon icon="cake" />,
|
leftSlot: <Icon icon="cake" />,
|
||||||
rightSlot: <Icon icon="externalLink" />,
|
rightSlot: <Icon icon="externalLink" />,
|
||||||
onSelect: () => open(`https://yaak.app/changelog/${appInfo?.version}`),
|
onSelect: () => open(`https://yaak.app/changelog/${appInfo.version}`),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
|
import type { Folder, GrpcRequest, HttpRequest, Model, Workspace } from '@yaakapp/api';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
|
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
@@ -576,7 +576,7 @@ type SidebarItemProps = {
|
|||||||
itemId: string;
|
itemId: string;
|
||||||
itemName: string;
|
itemName: string;
|
||||||
itemFallbackName: string;
|
itemFallbackName: string;
|
||||||
itemModel: string;
|
itemModel: Model['model'];
|
||||||
itemPrefix: ReactNode;
|
itemPrefix: ReactNode;
|
||||||
useProminentStyles?: boolean;
|
useProminentStyles?: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@@ -658,8 +658,10 @@ function SidebarItem({
|
|||||||
const sendRequest = useSendAnyHttpRequest();
|
const sendRequest = useSendAnyHttpRequest();
|
||||||
const moveToWorkspace = useMoveToWorkspace(itemId);
|
const moveToWorkspace = useMoveToWorkspace(itemId);
|
||||||
const sendManyRequests = useSendManyRequests();
|
const sendManyRequests = useSendManyRequests();
|
||||||
const latestHttpResponse = useLatestHttpResponse(itemId);
|
const latestHttpResponse = useLatestHttpResponse(itemModel === 'http_request' ? itemId : null);
|
||||||
const latestGrpcConnection = useLatestGrpcConnection(itemId);
|
const latestGrpcConnection = useLatestGrpcConnection(
|
||||||
|
itemModel === 'grpc_request' ? itemId : null,
|
||||||
|
);
|
||||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||||
|
|||||||
211
src-web/components/TemplateFunctionDialog.tsx
Normal file
211
src-web/components/TemplateFunctionDialog.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import type { FnArg } from '../gen/FnArg';
|
||||||
|
import type { Tokens } from '../gen/Tokens';
|
||||||
|
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||||
|
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||||
|
import type {
|
||||||
|
TemplateFunction,
|
||||||
|
TemplateFunctionArg,
|
||||||
|
TemplateFunctionHttpRequestArg,
|
||||||
|
TemplateFunctionSelectArg,
|
||||||
|
TemplateFunctionTextArg,
|
||||||
|
} from '../hooks/useTemplateFunctions';
|
||||||
|
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
||||||
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { PlainInput } from './core/PlainInput';
|
||||||
|
import { Select } from './core/Select';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
|
const NULL_ARG = '__NULL__';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
templateFunction: TemplateFunction;
|
||||||
|
initialTokens: Tokens;
|
||||||
|
hide: () => void;
|
||||||
|
onChange: (insert: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) {
|
||||||
|
const [argValues, setArgValues] = useState<Record<string, string>>(() => {
|
||||||
|
const initial: Record<string, string> = {};
|
||||||
|
const initialArgs =
|
||||||
|
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||||
|
? initialTokens.tokens[0]?.val.args
|
||||||
|
: [];
|
||||||
|
for (const arg of templateFunction.args) {
|
||||||
|
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||||
|
const initialArgValue =
|
||||||
|
initialArg?.value.type === 'str'
|
||||||
|
? initialArg?.value.text
|
||||||
|
: // TODO: Implement variable-based args
|
||||||
|
'__NULL__';
|
||||||
|
initial[arg.name] = initialArgValue ?? NULL_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setArgValue = useCallback((name: string, value: string) => {
|
||||||
|
setArgValues((v) => ({ ...v, [name]: value }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tokens: Tokens = useMemo(() => {
|
||||||
|
const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({
|
||||||
|
name,
|
||||||
|
value:
|
||||||
|
argValues[name] === NULL_ARG
|
||||||
|
? { type: 'null' }
|
||||||
|
: {
|
||||||
|
type: 'str',
|
||||||
|
text: argValues[name] ?? '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
type: 'tag',
|
||||||
|
val: {
|
||||||
|
type: 'fn',
|
||||||
|
name: templateFunction.name,
|
||||||
|
args: argTokens,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}, [argValues, templateFunction.name]);
|
||||||
|
|
||||||
|
const tagText = useTemplateTokensToString(tokens);
|
||||||
|
|
||||||
|
const handleDone = () => {
|
||||||
|
if (tagText.data) {
|
||||||
|
onChange(tagText.data);
|
||||||
|
}
|
||||||
|
hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rendered = useRenderTemplate(tagText.data ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack className="pb-3" space={4}>
|
||||||
|
<VStack space={2}>
|
||||||
|
{templateFunction.args.map((a: TemplateFunctionArg, i: number) => {
|
||||||
|
switch (a.type) {
|
||||||
|
case 'select':
|
||||||
|
return (
|
||||||
|
<SelectArg
|
||||||
|
key={i}
|
||||||
|
arg={a}
|
||||||
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
|
value={argValues[a.name] ?? '__ERROR__'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<TextArg
|
||||||
|
key={i}
|
||||||
|
arg={a}
|
||||||
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
|
value={argValues[a.name] ?? '__ERROR__'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'http_request':
|
||||||
|
return (
|
||||||
|
<HttpRequestArg
|
||||||
|
key={i}
|
||||||
|
arg={a}
|
||||||
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
|
value={argValues[a.name] ?? '__ERROR__'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
<InlineCode className="select-text cursor-text">{rendered.data}</InlineCode>
|
||||||
|
<Button color="primary" onClick={handleDone}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextArg({
|
||||||
|
arg,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
arg: TemplateFunctionTextArg;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
onChange(value === '' ? NULL_ARG : value);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainInput
|
||||||
|
name={arg.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
defaultValue={value === NULL_ARG ? '' : value}
|
||||||
|
label={arg.label ?? arg.name}
|
||||||
|
hideLabel={arg.label == null}
|
||||||
|
placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectArg({
|
||||||
|
arg,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
arg: TemplateFunctionSelectArg;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label={arg.label ?? arg.name}
|
||||||
|
name={arg.name}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
options={[
|
||||||
|
...arg.options.map((a) => ({
|
||||||
|
label: a.name + (arg.defaultValue === a.value ? ' (default)' : ''),
|
||||||
|
value: a.value === arg.defaultValue ? NULL_ARG : a.value,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HttpRequestArg({
|
||||||
|
arg,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
arg: TemplateFunctionHttpRequestArg;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
const httpRequests = useHttpRequests();
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label={arg.label ?? arg.name}
|
||||||
|
name={arg.name}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
options={[
|
||||||
|
...httpRequests.map((r) => ({
|
||||||
|
label: fallbackRequestName(r),
|
||||||
|
value: r.id,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
src-web/components/TemplateVariableDialog.tsx
Normal file
69
src-web/components/TemplateVariableDialog.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { EnvironmentVariable } from '@yaakapp/api';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import type { Tokens } from '../gen/Tokens';
|
||||||
|
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
|
||||||
|
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||||
|
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { Select } from './core/Select';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
definition: EnvironmentVariable;
|
||||||
|
initialTokens: Tokens;
|
||||||
|
hide: () => void;
|
||||||
|
onChange: (rawTag: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateVariableDialog({ hide, onChange, initialTokens }: Props) {
|
||||||
|
const variables = useActiveEnvironmentVariables();
|
||||||
|
const [selectedVariableName, setSelectedVariableName] = useState<string>(() => {
|
||||||
|
return initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'var'
|
||||||
|
? initialTokens.tokens[0]?.val.name
|
||||||
|
: ''; // Should never happen
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens: Tokens = useMemo(() => {
|
||||||
|
const selectedVariable = variables.find((v) => v.name === selectedVariableName);
|
||||||
|
return {
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
type: 'tag',
|
||||||
|
val: {
|
||||||
|
type: 'var',
|
||||||
|
name: selectedVariable?.name ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}, [selectedVariableName, variables]);
|
||||||
|
|
||||||
|
const tagText = useTemplateTokensToString(tokens);
|
||||||
|
const handleDone = useCallback(async () => {
|
||||||
|
if (tagText.data != null) {
|
||||||
|
onChange(tagText.data);
|
||||||
|
}
|
||||||
|
hide();
|
||||||
|
}, [hide, onChange, tagText.data]);
|
||||||
|
|
||||||
|
const rendered = useRenderTemplate(tagText.data ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack className="pb-3" space={4}>
|
||||||
|
<VStack space={2}>
|
||||||
|
<Select
|
||||||
|
name="variable"
|
||||||
|
label="Variable"
|
||||||
|
value={selectedVariableName}
|
||||||
|
options={variables.map((v) => ({ label: v.name, value: v.name }))}
|
||||||
|
onChange={setSelectedVariableName}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
<InlineCode className="select-text cursor-text">{rendered.data}</InlineCode>
|
||||||
|
<Button color="primary" onClick={handleDone}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-placeholder {
|
.cm-placeholder {
|
||||||
@apply text-text-subtlest;
|
@apply text-placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
@@ -60,12 +60,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.template-tag {
|
||||||
/* Colors */
|
/* Colors */
|
||||||
@apply bg-surface text-text-subtle border-border-subtle;
|
@apply bg-surface text-text-subtle border-border-subtle;
|
||||||
@apply hover:border-border-subtle hover:text-text hover:bg-surface-highlight;
|
@apply hover:border-border-subtle hover:text-text hover:bg-surface-highlight;
|
||||||
|
|
||||||
@apply border px-1 mx-[0.5px] rounded cursor-default dark:shadow;
|
@apply inline border px-1 mx-[0.5px] rounded cursor-default dark:shadow;
|
||||||
|
|
||||||
-webkit-text-security: none;
|
-webkit-text-security: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
||||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||||
|
import type { EnvironmentVariable } from '@yaakapp/api';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { MutableRefObject, ReactNode } from 'react';
|
import type { MutableRefObject, ReactNode } from 'react';
|
||||||
@@ -15,9 +16,13 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
|
||||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
import { parseTemplate } from '../../../hooks/useParseTemplate';
|
||||||
import { useSettings } from '../../../hooks/useSettings';
|
import { useSettings } from '../../../hooks/useSettings';
|
||||||
|
import { type TemplateFunction, useTemplateFunctions } from '../../../hooks/useTemplateFunctions';
|
||||||
|
import { useDialog } from '../../DialogContext';
|
||||||
|
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||||
|
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||||
import { IconButton } from '../IconButton';
|
import { IconButton } from '../IconButton';
|
||||||
import { HStack } from '../Stacks';
|
import { HStack } from '../Stacks';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
@@ -58,6 +63,8 @@ export interface EditorProps {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyVariables: EnvironmentVariable[] = [];
|
||||||
|
|
||||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||||
{
|
{
|
||||||
readOnly,
|
readOnly,
|
||||||
@@ -87,10 +94,9 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const s = useSettings();
|
const s = useSettings();
|
||||||
const [e] = useActiveEnvironment();
|
const templateFunctions = useTemplateFunctions();
|
||||||
const w = useActiveWorkspace();
|
const allEnvironmentVariables = useActiveEnvironmentVariables();
|
||||||
const environment = autocompleteVariables ? e : null;
|
const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables;
|
||||||
const workspace = autocompleteVariables ? w : null;
|
|
||||||
|
|
||||||
if (s && wrapLines === undefined) {
|
if (s && wrapLines === undefined) {
|
||||||
wrapLines = s.editorSoftWrap;
|
wrapLines = s.editorSoftWrap;
|
||||||
@@ -148,19 +154,78 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
cm.current?.view.dispatch({ effects: effect });
|
cm.current?.view.dispatch({ effects: effect });
|
||||||
}, [wrapLines]);
|
}, [wrapLines]);
|
||||||
|
|
||||||
|
const dialog = useDialog();
|
||||||
|
const onClickFunction = useCallback(
|
||||||
|
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
||||||
|
const initialTokens = await parseTemplate(tagValue);
|
||||||
|
dialog.show({
|
||||||
|
id: 'template-function',
|
||||||
|
size: 'sm',
|
||||||
|
title: 'Configure Function',
|
||||||
|
render: ({ hide }) => (
|
||||||
|
<TemplateFunctionDialog
|
||||||
|
templateFunction={fn}
|
||||||
|
hide={hide}
|
||||||
|
initialTokens={initialTokens}
|
||||||
|
onChange={(insert) => {
|
||||||
|
cm.current?.view.dispatch({
|
||||||
|
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dialog],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickVariable = useCallback(
|
||||||
|
async (v: EnvironmentVariable, tagValue: string, startPos: number) => {
|
||||||
|
const initialTokens = await parseTemplate(tagValue);
|
||||||
|
dialog.show({
|
||||||
|
size: 'dynamic',
|
||||||
|
id: 'template-variable',
|
||||||
|
title: 'Configure Variable',
|
||||||
|
render: ({ hide }) => (
|
||||||
|
<TemplateVariableDialog
|
||||||
|
definition={v}
|
||||||
|
hide={hide}
|
||||||
|
initialTokens={initialTokens}
|
||||||
|
onChange={(insert) => {
|
||||||
|
cm.current?.view.dispatch({
|
||||||
|
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dialog],
|
||||||
|
);
|
||||||
|
|
||||||
// Update language extension when contentType changes
|
// Update language extension when contentType changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cm.current === null) return;
|
if (cm.current === null) return;
|
||||||
const { view, languageCompartment } = cm.current;
|
const { view, languageCompartment } = cm.current;
|
||||||
const ext = getLanguageExtension({
|
const ext = getLanguageExtension({
|
||||||
contentType,
|
contentType,
|
||||||
environment,
|
environmentVariables,
|
||||||
workspace,
|
|
||||||
useTemplating,
|
useTemplating,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
|
templateFunctions,
|
||||||
|
onClickFunction,
|
||||||
|
onClickVariable,
|
||||||
});
|
});
|
||||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||||
}, [contentType, autocomplete, useTemplating, environment, workspace]);
|
}, [
|
||||||
|
contentType,
|
||||||
|
autocomplete,
|
||||||
|
useTemplating,
|
||||||
|
environmentVariables,
|
||||||
|
templateFunctions,
|
||||||
|
onClickFunction,
|
||||||
|
onClickVariable,
|
||||||
|
]);
|
||||||
|
|
||||||
// Initialize the editor when ref mounts
|
// Initialize the editor when ref mounts
|
||||||
const initEditorRef = useCallback(
|
const initEditorRef = useCallback(
|
||||||
@@ -178,8 +243,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
contentType,
|
contentType,
|
||||||
useTemplating,
|
useTemplating,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
environment,
|
environmentVariables,
|
||||||
workspace,
|
templateFunctions,
|
||||||
|
onClickVariable,
|
||||||
|
onClickFunction,
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ import {
|
|||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import type { EnvironmentVariable } from '@yaakapp/api';
|
||||||
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { Environment, Workspace } from '@yaakapp/api';
|
import type { TemplateFunction } from '../../../hooks/useTemplateFunctions';
|
||||||
import type { EditorProps } from './index';
|
import type { EditorProps } from './index';
|
||||||
import { pairs } from './pairs/extension';
|
import { pairs } from './pairs/extension';
|
||||||
import { text } from './text/extension';
|
import { text } from './text/extension';
|
||||||
@@ -78,13 +79,17 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
|
|||||||
export function getLanguageExtension({
|
export function getLanguageExtension({
|
||||||
contentType,
|
contentType,
|
||||||
useTemplating = false,
|
useTemplating = false,
|
||||||
environment,
|
environmentVariables,
|
||||||
workspace,
|
|
||||||
autocomplete,
|
autocomplete,
|
||||||
}: { environment: Environment | null; workspace: Workspace | null } & Pick<
|
templateFunctions,
|
||||||
EditorProps,
|
onClickVariable,
|
||||||
'contentType' | 'useTemplating' | 'autocomplete'
|
onClickFunction,
|
||||||
>) {
|
}: {
|
||||||
|
environmentVariables: EnvironmentVariable[];
|
||||||
|
templateFunctions: TemplateFunction[];
|
||||||
|
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||||
|
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
|
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
||||||
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
||||||
if (justContentType === 'application/graphql') {
|
if (justContentType === 'application/graphql') {
|
||||||
return graphql();
|
return graphql();
|
||||||
@@ -94,7 +99,14 @@ export function getLanguageExtension({
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
return twig(base, environment, workspace, autocomplete);
|
return twig({
|
||||||
|
base,
|
||||||
|
environmentVariables,
|
||||||
|
templateFunctions,
|
||||||
|
autocomplete,
|
||||||
|
onClickFunction,
|
||||||
|
onClickVariable,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseExtensions = [
|
export const baseExtensions = [
|
||||||
|
|||||||
@@ -75,21 +75,21 @@ const decorator = function () {
|
|||||||
|
|
||||||
return ViewPlugin.fromClass(
|
return ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
placeholders: DecorationSet;
|
decorations: DecorationSet;
|
||||||
|
|
||||||
constructor(view: EditorView) {
|
constructor(view: EditorView) {
|
||||||
this.placeholders = placeholderMatcher.createDeco(view);
|
this.decorations = placeholderMatcher.createDeco(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
this.decorations = placeholderMatcher.updateDeco(update, this.decorations);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
decorations: (instance) => instance.placeholders,
|
decorations: (instance) => instance.decorations,
|
||||||
provide: (plugin) =>
|
provide: (plugin) =>
|
||||||
EditorView.bidiIsolatedRanges.of((view) => {
|
EditorView.bidiIsolatedRanges.of((view) => {
|
||||||
return view.plugin(plugin)?.placeholders || Decoration.none;
|
return view.plugin(plugin)?.decorations || Decoration.none;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import type { CompletionContext } from '@codemirror/autocomplete';
|
|||||||
const openTag = '${[ ';
|
const openTag = '${[ ';
|
||||||
const closeTag = ' ]}';
|
const closeTag = ' ]}';
|
||||||
|
|
||||||
interface TwigCompletionOption {
|
export interface TwigCompletionOption {
|
||||||
name: string;
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: 'function' | 'variable' | 'unknown';
|
||||||
|
value: string | null;
|
||||||
|
onClick?: (rawTag: string, startPos: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TwigCompletionConfig {
|
export interface TwigCompletionConfig {
|
||||||
@@ -41,12 +45,16 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
|||||||
from: toMatch.from,
|
from: toMatch.from,
|
||||||
options: options
|
options: options
|
||||||
.filter((v) => v.name.trim())
|
.filter((v) => v.name.trim())
|
||||||
.map((v) => ({
|
.map((v) => {
|
||||||
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
|
const innerLabel = v.type === 'function' ? `${v.name}()` : v.name;
|
||||||
apply: `${openTag}${v.name}${closeTag}`,
|
const tagSyntax = openTag + innerLabel + closeTag;
|
||||||
type: 'variable',
|
return {
|
||||||
matchLen: matchLen,
|
label: innerLabel,
|
||||||
}))
|
apply: tagSyntax,
|
||||||
|
type: v.type === 'variable' ? 'variable' : 'function',
|
||||||
|
matchLen: matchLen,
|
||||||
|
};
|
||||||
|
})
|
||||||
// Filter out exact matches
|
// Filter out exact matches
|
||||||
.filter((o) => o.label !== toMatch.text),
|
.filter((o) => o.label !== toMatch.text),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,57 @@
|
|||||||
import type { LanguageSupport } from '@codemirror/language';
|
import type { LanguageSupport } from '@codemirror/language';
|
||||||
import { LRLanguage } from '@codemirror/language';
|
import { LRLanguage } from '@codemirror/language';
|
||||||
import { parseMixed } from '@lezer/common';
|
import { parseMixed } from '@lezer/common';
|
||||||
import type { Environment, Workspace } from '@yaakapp/api';
|
import type { EnvironmentVariable } from '@yaakapp/api';
|
||||||
|
import type { TemplateFunction } from '../../../../hooks/useTemplateFunctions';
|
||||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||||
import { genericCompletion } from '../genericCompletion';
|
import { genericCompletion } from '../genericCompletion';
|
||||||
import { textLanguageName } from '../text/extension';
|
import { textLanguageName } from '../text/extension';
|
||||||
|
import type { TwigCompletionOption } from './completion';
|
||||||
import { twigCompletion } from './completion';
|
import { twigCompletion } from './completion';
|
||||||
import { placeholders } from './placeholder';
|
import { templateTags } from './templateTags';
|
||||||
import { parser as twigParser } from './twig';
|
import { parser as twigParser } from './twig';
|
||||||
|
|
||||||
export function twig(
|
export function twig({
|
||||||
base: LanguageSupport,
|
base,
|
||||||
environment: Environment | null,
|
environmentVariables,
|
||||||
workspace: Workspace | null,
|
templateFunctions,
|
||||||
autocomplete?: GenericCompletionConfig,
|
autocomplete,
|
||||||
) {
|
onClickFunction,
|
||||||
|
onClickVariable,
|
||||||
|
}: {
|
||||||
|
base: LanguageSupport;
|
||||||
|
environmentVariables: EnvironmentVariable[];
|
||||||
|
templateFunctions: TemplateFunction[];
|
||||||
|
autocomplete?: GenericCompletionConfig;
|
||||||
|
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||||
|
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
|
}) {
|
||||||
const language = mixLanguage(base);
|
const language = mixLanguage(base);
|
||||||
const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])];
|
|
||||||
const variables = allVariables.filter((v) => v.enabled) ?? [];
|
const variableOptions: TwigCompletionOption[] =
|
||||||
const completions = twigCompletion({ options: variables });
|
environmentVariables.map((v) => ({
|
||||||
|
...v,
|
||||||
|
type: 'variable',
|
||||||
|
label: v.name,
|
||||||
|
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||||
|
})) ?? [];
|
||||||
|
const functionOptions: TwigCompletionOption[] =
|
||||||
|
templateFunctions.map((fn) => ({
|
||||||
|
name: fn.name,
|
||||||
|
type: 'function',
|
||||||
|
value: null,
|
||||||
|
label: fn.name + '(' + fn.args.length + ')',
|
||||||
|
onClick: (rawTag: string, startPos: number) => onClickFunction(fn, rawTag, startPos),
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const options = [...variableOptions, ...functionOptions];
|
||||||
|
|
||||||
|
const completions = twigCompletion({ options });
|
||||||
|
|
||||||
return [
|
return [
|
||||||
language,
|
language,
|
||||||
base.support,
|
base.support,
|
||||||
placeholders(variables),
|
templateTags(options),
|
||||||
language.data.of({ autocomplete: completions }),
|
language.data.of({ autocomplete: completions }),
|
||||||
base.language.data.of({ autocomplete: completions }),
|
base.language.data.of({ autocomplete: completions }),
|
||||||
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
|
||||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
||||||
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
|
||||||
|
|
||||||
class PlaceholderWidget extends WidgetType {
|
|
||||||
constructor(
|
|
||||||
readonly name: string,
|
|
||||||
readonly value: string,
|
|
||||||
readonly exists: boolean,
|
|
||||||
readonly type: 'function' | 'variable' = 'variable',
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
eq(other: PlaceholderWidget) {
|
|
||||||
return this.name == other.name && this.exists == other.exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
toDOM() {
|
|
||||||
const elt = document.createElement('span');
|
|
||||||
elt.className = `x-theme-placeholder placeholder ${
|
|
||||||
!this.exists
|
|
||||||
? 'x-theme-placeholder--danger'
|
|
||||||
: this.type === 'variable'
|
|
||||||
? 'x-theme-placeholder--primary'
|
|
||||||
: 'x-theme-placeholder--info'
|
|
||||||
}`;
|
|
||||||
elt.title = !this.exists ? 'Variable not found in active environment' : this.value ?? '';
|
|
||||||
elt.textContent = this.name;
|
|
||||||
return elt;
|
|
||||||
}
|
|
||||||
|
|
||||||
ignoreEvent() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const placeholders = function (variables: { name: string; value?: string }[]) {
|
|
||||||
const placeholderMatcher = new BetterMatchDecorator({
|
|
||||||
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
|
||||||
decoration(match, view, matchStartPos) {
|
|
||||||
const matchEndPos = matchStartPos + match[0].length - 1;
|
|
||||||
|
|
||||||
// Don't decorate if the cursor is inside the match
|
|
||||||
for (const r of view.state.selection.ranges) {
|
|
||||||
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
|
||||||
return Decoration.replace({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupMatch = match[1];
|
|
||||||
if (groupMatch == null) {
|
|
||||||
// Should never happen, but make TS happy
|
|
||||||
console.warn('Group match was empty', match);
|
|
||||||
return Decoration.replace({});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFunction = groupMatch.includes('(');
|
|
||||||
return Decoration.replace({
|
|
||||||
inclusive: true,
|
|
||||||
widget: new PlaceholderWidget(
|
|
||||||
groupMatch,
|
|
||||||
variables.find((v) => v.name === groupMatch)?.value ?? '',
|
|
||||||
isFunction ? true : variables.some((v) => v.name === groupMatch),
|
|
||||||
isFunction ? 'function' : 'variable',
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
placeholders: DecorationSet;
|
|
||||||
|
|
||||||
constructor(view: EditorView) {
|
|
||||||
this.placeholders = placeholderMatcher.createDeco(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decorations: (instance) => instance.placeholders,
|
|
||||||
provide: (plugin) =>
|
|
||||||
EditorView.atomicRanges.of((view) => {
|
|
||||||
return view.plugin(plugin)?.placeholders || Decoration.none;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
115
src-web/components/core/Editor/twig/templateTags.ts
Normal file
115
src-web/components/core/Editor/twig/templateTags.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
|
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
|
import { truncate } from '../../../../lib/truncate';
|
||||||
|
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
||||||
|
import type { TwigCompletionOption } from './completion';
|
||||||
|
|
||||||
|
const TAG_TRUNCATE_LEN = 30;
|
||||||
|
|
||||||
|
class TemplateTagWidget extends WidgetType {
|
||||||
|
readonly #clickListenerCallback: () => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly option: TwigCompletionOption,
|
||||||
|
readonly rawTag: string,
|
||||||
|
readonly startPos: number,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.#clickListenerCallback = () => {
|
||||||
|
this.option.onClick?.(this.rawTag, this.startPos);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(other: TemplateTagWidget) {
|
||||||
|
return (
|
||||||
|
this.option.name === other.option.name &&
|
||||||
|
this.option.type === other.option.type &&
|
||||||
|
this.option.value === other.option.value &&
|
||||||
|
this.rawTag === other.rawTag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const elt = document.createElement('span');
|
||||||
|
elt.className = `x-theme-templateTag template-tag ${
|
||||||
|
this.option.type === 'unknown'
|
||||||
|
? 'x-theme-templateTag--danger'
|
||||||
|
: this.option.type === 'variable'
|
||||||
|
? 'x-theme-templateTag--primary'
|
||||||
|
: 'x-theme-templateTag--info'
|
||||||
|
}`;
|
||||||
|
elt.title = this.option.type === 'unknown' ? '__NOT_FOUND__' : this.option.value ?? '';
|
||||||
|
elt.textContent = truncate(
|
||||||
|
this.rawTag.replace('${[', '').replace(']}', '').trim(),
|
||||||
|
TAG_TRUNCATE_LEN,
|
||||||
|
);
|
||||||
|
elt.addEventListener('click', this.#clickListenerCallback);
|
||||||
|
return elt;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(dom: HTMLElement) {
|
||||||
|
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||||
|
super.destroy(dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvent() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateTags(options: TwigCompletionOption[]) {
|
||||||
|
const templateTagMatcher = new BetterMatchDecorator({
|
||||||
|
regexp: /\$\{\[\s*([^\]]+)\s*]}/g,
|
||||||
|
decoration(match, view, matchStartPos) {
|
||||||
|
const matchEndPos = matchStartPos + match[0].length - 1;
|
||||||
|
|
||||||
|
// Don't decorate if the cursor is inside the match
|
||||||
|
for (const r of view.state.selection.ranges) {
|
||||||
|
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerTagMatch = match[1];
|
||||||
|
if (innerTagMatch == null) {
|
||||||
|
// Should never happen, but make TS happy
|
||||||
|
console.warn('Group match was empty', match);
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this hacky match with a proper template parser
|
||||||
|
const name = innerTagMatch.match(/\s*(\w+)[(\s]*/)?.[1] ?? innerTagMatch;
|
||||||
|
|
||||||
|
let option = options.find((v) => v.name === name);
|
||||||
|
if (option == null) {
|
||||||
|
option = { type: 'unknown', name: innerTagMatch, value: null, label: innerTagMatch };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decoration.replace({
|
||||||
|
inclusive: true,
|
||||||
|
widget: new TemplateTagWidget(option, match[0], matchStartPos),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations: DecorationSet;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = templateTagMatcher.createDeco(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
this.decorations = templateTagMatcher.updateDeco(update, this.decorations);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (instance) => instance.decorations,
|
||||||
|
provide: (plugin) =>
|
||||||
|
EditorView.atomicRanges.of((view) => {
|
||||||
|
return view.plugin(plugin)?.decorations || Decoration.none;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ export function Select<T extends string>({
|
|||||||
{options.map((o) => {
|
{options.map((o) => {
|
||||||
if (o.type === 'separator') return null;
|
if (o.type === 'separator') return null;
|
||||||
return (
|
return (
|
||||||
<option key={o.label} value={o.value}>
|
<option key={o.value} value={o.value}>
|
||||||
{o.label}
|
{o.label}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
|
|||||||
4
src-web/gen/FnArg.ts
Normal file
4
src-web/gen/FnArg.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { Val } from "./Val";
|
||||||
|
|
||||||
|
export type FnArg = { name: string, value: Val, };
|
||||||
4
src-web/gen/Token.ts
Normal file
4
src-web/gen/Token.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { Val } from "./Val";
|
||||||
|
|
||||||
|
export type Token = { "type": "raw", text: string, } | { "type": "tag", val: Val, } | { "type": "eof" };
|
||||||
4
src-web/gen/Tokens.ts
Normal file
4
src-web/gen/Tokens.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { Token } from "./Token";
|
||||||
|
|
||||||
|
export type Tokens = { tokens: Array<Token>, };
|
||||||
4
src-web/gen/Val.ts
Normal file
4
src-web/gen/Val.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { FnArg } from "./FnArg";
|
||||||
|
|
||||||
|
export type Val = { "type": "str", text: string, } | { "type": "var", name: string, } | { "type": "fn", name: string, args: Array<FnArg>, } | { "type": "null" };
|
||||||
@@ -14,7 +14,7 @@ export function useActiveEnvironment() {
|
|||||||
|
|
||||||
export const QUERY_ENVIRONMENT_ID = 'environment_id';
|
export const QUERY_ENVIRONMENT_ID = 'environment_id';
|
||||||
|
|
||||||
export function useActiveEnvironmentId() {
|
function useActiveEnvironmentId() {
|
||||||
// NOTE: This query param is accessed from Rust side, so do not change
|
// NOTE: This query param is accessed from Rust side, so do not change
|
||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
const id = params.get(QUERY_ENVIRONMENT_ID);
|
const id = params.get(QUERY_ENVIRONMENT_ID);
|
||||||
|
|||||||
24
src-web/hooks/useActiveEnvironmentVariables.ts
Normal file
24
src-web/hooks/useActiveEnvironmentVariables.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { EnvironmentVariable } from '@yaakapp/api';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||||
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
|
||||||
|
export function useActiveEnvironmentVariables() {
|
||||||
|
const workspace = useActiveWorkspace();
|
||||||
|
const [environment] = useActiveEnvironment();
|
||||||
|
|
||||||
|
const variables = useMemo(() => {
|
||||||
|
const varMap: Record<string, EnvironmentVariable> = {};
|
||||||
|
|
||||||
|
const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])];
|
||||||
|
|
||||||
|
for (const v of allVariables) {
|
||||||
|
if (!v.enabled || !v.name) continue;
|
||||||
|
varMap[v.name] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(varMap);
|
||||||
|
}, [workspace, environment]);
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import type { Workspace } from '@yaakapp/api';
|
import type { Workspace } from '@yaakapp/api';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { RouteParamsWorkspace } from './useAppRoutes';
|
import type { RouteParamsWorkspace } from './useAppRoutes';
|
||||||
import { useWorkspaces } from './useWorkspaces';
|
import { useWorkspaces } from './useWorkspaces';
|
||||||
@@ -7,6 +7,7 @@ import { useWorkspaces } from './useWorkspaces';
|
|||||||
export function useActiveWorkspace(): Workspace | null {
|
export function useActiveWorkspace(): Workspace | null {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => workspaces.find((w) => w.id === workspaceId) ?? null,
|
() => workspaces.find((w) => w.id === workspaceId) ?? null,
|
||||||
[workspaces, workspaceId],
|
[workspaces, workspaceId],
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
|
|
||||||
export interface AppInfo {
|
export interface AppInfo {
|
||||||
@@ -9,12 +8,8 @@ export interface AppInfo {
|
|||||||
appLogDir: string;
|
appLogDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
|
||||||
|
|
||||||
export function useAppInfo() {
|
export function useAppInfo() {
|
||||||
return useQuery({
|
return appInfo;
|
||||||
queryKey: ['appInfo'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const metadata = await invokeCmd('cmd_metadata');
|
|
||||||
return metadata as AppInfo;
|
|
||||||
},
|
|
||||||
}).data;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function useCheckForUpdates() {
|
|||||||
title: 'No Update Available',
|
title: 'No Update Available',
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
You are currently on the latest version <InlineCode>{appInfo?.version}</InlineCode>
|
You are currently on the latest version <InlineCode>{appInfo.version}</InlineCode>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import type { GrpcRequest } from '@yaakapp/api';
|
import type { GrpcRequest } from '@yaakapp/api';
|
||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import { setKeyValue } from '../lib/keyValueStore';
|
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { protoFilesArgs, useGrpcProtoFiles } from './useGrpcProtoFiles';
|
import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles';
|
||||||
|
|
||||||
export function useDuplicateGrpcRequest({
|
export function useDuplicateGrpcRequest({
|
||||||
id,
|
id,
|
||||||
@@ -18,7 +17,7 @@ export function useDuplicateGrpcRequest({
|
|||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const [activeEnvironment] = useActiveEnvironment();
|
const [activeEnvironment] = useActiveEnvironment();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const protoFiles = useGrpcProtoFiles(id);
|
|
||||||
return useMutation<GrpcRequest, string>({
|
return useMutation<GrpcRequest, string>({
|
||||||
mutationKey: ['duplicate_grpc_request', id],
|
mutationKey: ['duplicate_grpc_request', id],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -27,8 +26,11 @@ export function useDuplicateGrpcRequest({
|
|||||||
},
|
},
|
||||||
onSettled: () => trackEvent('grpc_request', 'duplicate'),
|
onSettled: () => trackEvent('grpc_request', 'duplicate'),
|
||||||
onSuccess: async (request) => {
|
onSuccess: async (request) => {
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
// Also copy proto files to new request
|
// Also copy proto files to new request
|
||||||
await setKeyValue({ ...protoFilesArgs(request.id), value: protoFiles.value ?? [] });
|
const protoFiles = await getGrpcProtoFiles(id);
|
||||||
|
await setGrpcProtoFiles(request.id, protoFiles);
|
||||||
|
|
||||||
if (navigateAfter && activeWorkspace !== null) {
|
if (navigateAfter && activeWorkspace !== null) {
|
||||||
routes.navigate('request', {
|
routes.navigate('request', {
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { Environment } from '@yaakapp/api';
|
import type { Environment } from '@yaakapp/api';
|
||||||
|
import { atom, useAtom } from 'jotai/index';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
|
||||||
export function environmentsQueryKey({ workspaceId }: { workspaceId: string }) {
|
export const environmentsAtom = atom<Environment[]>([]);
|
||||||
return ['environments', { workspaceId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEnvironments() {
|
export function useEnvironments() {
|
||||||
|
const [items, setItems] = useAtom(environmentsAtom);
|
||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
return (
|
|
||||||
useQuery({
|
// Fetch new requests when workspace changes
|
||||||
enabled: workspace != null,
|
useEffect(() => {
|
||||||
queryKey: environmentsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
|
if (workspace == null) return;
|
||||||
queryFn: async () => {
|
invokeCmd<Environment[]>('cmd_list_environments', { workspaceId: workspace.id }).then(setItems);
|
||||||
if (workspace == null) return [];
|
}, [setItems, workspace]);
|
||||||
return (await invokeCmd('cmd_list_environments', {
|
|
||||||
workspaceId: workspace.id,
|
return items;
|
||||||
})) as Environment[];
|
|
||||||
},
|
|
||||||
}).data ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function useGrpcConnections(requestId: string | null) {
|
|||||||
initialData: [],
|
initialData: [],
|
||||||
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
|
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (requestId == null) return [];
|
||||||
return (await invokeCmd('cmd_list_grpc_connections', {
|
return (await invokeCmd('cmd_list_grpc_connections', {
|
||||||
requestId,
|
requestId,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
export function protoFilesArgs(requestId: string | null) {
|
export function protoFilesArgs(requestId: string | null) {
|
||||||
@@ -10,3 +11,11 @@ export function protoFilesArgs(requestId: string | null) {
|
|||||||
export function useGrpcProtoFiles(activeRequestId: string | null) {
|
export function useGrpcProtoFiles(activeRequestId: string | null) {
|
||||||
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
|
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getGrpcProtoFiles(requestId: string) {
|
||||||
|
return getKeyValue<string[]>({ ...protoFilesArgs(requestId), fallback: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setGrpcProtoFiles(requestId: string, protoFiles: string[]) {
|
||||||
|
return setKeyValue<string[]>({ ...protoFilesArgs(requestId), value: protoFiles });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { GrpcRequest } from '@yaakapp/api';
|
import type { GrpcRequest } from '@yaakapp/api';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
|
||||||
export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
|
export const grpcRequestsAtom = atom<GrpcRequest[]>([]);
|
||||||
return ['grpc_requests', { workspaceId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGrpcRequests() {
|
export function useGrpcRequests() {
|
||||||
|
const [items, setItems] = useAtom(grpcRequestsAtom);
|
||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
return (
|
|
||||||
useQuery({
|
// Fetch new requests when workspace changes
|
||||||
enabled: workspace != null,
|
useEffect(() => {
|
||||||
queryKey: grpcRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
|
if (workspace == null) return;
|
||||||
queryFn: async () => {
|
invokeCmd<GrpcRequest[]>('cmd_list_grpc_requests', { workspaceId: workspace.id }).then(
|
||||||
if (workspace == null) return [];
|
setItems,
|
||||||
return (await invokeCmd('cmd_list_grpc_requests', {
|
);
|
||||||
workspaceId: workspace.id,
|
}, [setItems, workspace]);
|
||||||
})) as GrpcRequest[];
|
|
||||||
},
|
return items;
|
||||||
}).data ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { invokeCmd } from '../lib/tauri';
|
|||||||
export function useHttpRequestActions() {
|
export function useHttpRequestActions() {
|
||||||
const httpRequestActions = useQuery({
|
const httpRequestActions = useQuery({
|
||||||
queryKey: ['http_request_actions'],
|
queryKey: ['http_request_actions'],
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const responses = (await invokeCmd(
|
const responses = (await invokeCmd(
|
||||||
'cmd_http_request_actions',
|
'cmd_http_request_actions',
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { HttpRequest } from '@yaakapp/api';
|
import type { HttpRequest } from '@yaakapp/api';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
|
||||||
export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
|
export const httpRequestsAtom = atom<HttpRequest[]>([]);
|
||||||
return ['http_requests', { workspaceId }];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useHttpRequests() {
|
export function useHttpRequests() {
|
||||||
|
const [items, setItems] = useAtom(httpRequestsAtom);
|
||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
return (
|
|
||||||
useQuery({
|
useEffect(() => {
|
||||||
enabled: workspace != null,
|
if (workspace == null) return;
|
||||||
queryKey: httpRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
|
invokeCmd<HttpRequest[]>('cmd_list_http_requests', { workspaceId: workspace.id }).then(
|
||||||
queryFn: async () => {
|
setItems,
|
||||||
if (workspace == null) return [];
|
);
|
||||||
return (await invokeCmd('cmd_list_http_requests', {
|
}, [setItems, workspace]);
|
||||||
workspaceId: workspace.id,
|
|
||||||
})) as HttpRequest[];
|
return items;
|
||||||
},
|
|
||||||
}).data ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function useHttpResponses(requestId: string | null) {
|
|||||||
initialData: [],
|
initialData: [],
|
||||||
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
|
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (requestId == null) return [];
|
||||||
return (await invokeCmd('cmd_list_http_responses', {
|
return (await invokeCmd('cmd_list_http_responses', {
|
||||||
requestId,
|
requestId,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
|
|||||||
14
src-web/hooks/useParseTemplate.ts
Normal file
14
src-web/hooks/useParseTemplate.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { Tokens } from '../gen/Tokens';
|
||||||
|
import { invokeCmd } from '../lib/tauri';
|
||||||
|
|
||||||
|
export function useParseTemplate(template: string) {
|
||||||
|
return useQuery<Tokens>({
|
||||||
|
queryKey: ['parse_template', template],
|
||||||
|
queryFn: () => parseTemplate(template),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseTemplate(template: string): Promise<Tokens> {
|
||||||
|
return invokeCmd('cmd_parse_template', { template });
|
||||||
|
}
|
||||||
26
src-web/hooks/useRenderTemplate.ts
Normal file
26
src-web/hooks/useRenderTemplate.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { invokeCmd } from '../lib/tauri';
|
||||||
|
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||||
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
|
|
||||||
|
export function useRenderTemplate(template: string) {
|
||||||
|
const workspaceId = useActiveWorkspace()?.id ?? 'n/a';
|
||||||
|
const environmentId = useActiveEnvironment()[0]?.id ?? null;
|
||||||
|
return useQuery<string>({
|
||||||
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
|
queryKey: ['render_template', template],
|
||||||
|
queryFn: () => renderTemplate({ template, workspaceId, environmentId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderTemplate({
|
||||||
|
template,
|
||||||
|
workspaceId,
|
||||||
|
environmentId,
|
||||||
|
}: {
|
||||||
|
template: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environmentId: string | null;
|
||||||
|
}): Promise<string> {
|
||||||
|
return invokeCmd('cmd_render_template', { template, workspaceId, environmentId });
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export function useSyncWorkspaceRequestTitle() {
|
|||||||
newTitle += ` › ${fallbackRequestName(activeRequest)}`;
|
newTitle += ` › ${fallbackRequestName(activeRequest)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appInfo?.isDev) {
|
if (appInfo.isDev) {
|
||||||
newTitle = `[DEV] ${newTitle}`;
|
newTitle = `[DEV] ${newTitle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ export function useSyncWorkspaceRequestTitle() {
|
|||||||
} else {
|
} else {
|
||||||
emit('yaak_title_changed', newTitle).catch(console.error);
|
emit('yaak_title_changed', newTitle).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [activeEnvironment, activeRequest, activeWorkspace, appInfo?.isDev, osInfo.osType]);
|
}, [activeEnvironment, activeRequest, activeWorkspace, appInfo.isDev, osInfo.osType]);
|
||||||
}
|
}
|
||||||
|
|||||||
88
src-web/hooks/useTemplateFunctions.ts
Normal file
88
src-web/hooks/useTemplateFunctions.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type { HttpRequest } from '@yaakapp/api';
|
||||||
|
|
||||||
|
export interface TemplateFunctionArgBase {
|
||||||
|
name: string;
|
||||||
|
optional?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateFunctionSelectArg extends TemplateFunctionArgBase {
|
||||||
|
type: 'select';
|
||||||
|
defaultValue?: string;
|
||||||
|
options: readonly { name: string; value: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateFunctionTextArg extends TemplateFunctionArgBase {
|
||||||
|
type: 'text';
|
||||||
|
defaultValue?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateFunctionHttpRequestArg extends TemplateFunctionArgBase {
|
||||||
|
type: HttpRequest['model'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TemplateFunctionArg =
|
||||||
|
| TemplateFunctionSelectArg
|
||||||
|
| TemplateFunctionTextArg
|
||||||
|
| TemplateFunctionHttpRequestArg;
|
||||||
|
|
||||||
|
export interface TemplateFunction {
|
||||||
|
name: string;
|
||||||
|
args: TemplateFunctionArg[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTemplateFunctions() {
|
||||||
|
const fns: TemplateFunction[] = [
|
||||||
|
{
|
||||||
|
name: 'timestamp',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'from',
|
||||||
|
label: 'From',
|
||||||
|
placeholder: '2023-23-12T04:03:03',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: 'Format',
|
||||||
|
name: 'format',
|
||||||
|
options: [
|
||||||
|
{ name: 'RFC3339', value: 'rfc3339' },
|
||||||
|
{ name: 'Unix', value: 'unix' },
|
||||||
|
{ name: 'Unix (ms)', value: 'unix_millis' },
|
||||||
|
],
|
||||||
|
optional: true,
|
||||||
|
defaultValue: 'RFC3339',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'response',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
type: 'http_request',
|
||||||
|
name: 'request',
|
||||||
|
label: 'Request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'attribute',
|
||||||
|
label: 'Attribute',
|
||||||
|
options: [
|
||||||
|
{ name: 'Body', value: 'body' },
|
||||||
|
{ name: 'Header', value: 'header' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'filter',
|
||||||
|
label: 'Filter',
|
||||||
|
placeholder: 'JSONPath or XPath expression',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return fns;
|
||||||
|
}
|
||||||
15
src-web/hooks/useTemplateTokensToString.ts
Normal file
15
src-web/hooks/useTemplateTokensToString.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { Tokens } from '../gen/Tokens';
|
||||||
|
import { invokeCmd } from '../lib/tauri';
|
||||||
|
|
||||||
|
export function useTemplateTokensToString(tokens: Tokens) {
|
||||||
|
return useQuery<string>({
|
||||||
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
|
queryKey: ['template_tokens_to_string', tokens],
|
||||||
|
queryFn: () => templateTokensToString(tokens),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function templateTokensToString(tokens: Tokens): Promise<string> {
|
||||||
|
return invokeCmd('cmd_template_tokens_to_string', { tokens });
|
||||||
|
}
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { Workspace } from '@yaakapp/api';
|
import type { Workspace } from '@yaakapp/api';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
|
import { listWorkspaces } from '../lib/store';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/ban-types
|
const workspaces = await listWorkspaces();
|
||||||
export function workspacesQueryKey(_?: {}) {
|
export const workspacesAtom = atom<Workspace[]>(workspaces);
|
||||||
return ['workspaces'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkspaces() {
|
export function useWorkspaces() {
|
||||||
return (
|
return useAtomValue(workspacesAtom);
|
||||||
useQuery({
|
|
||||||
queryKey: workspacesQueryKey(),
|
|
||||||
queryFn: async () => {
|
|
||||||
const workspaces = await invokeCmd('cmd_list_workspaces');
|
|
||||||
return workspaces as Workspace[];
|
|
||||||
},
|
|
||||||
}).data ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export async function getWorkspace(id: string | null): Promise<Workspace | null>
|
|||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listWorkspaces(): Promise<Workspace[]> {
|
||||||
|
const workspaces: Workspace[] = (await invokeCmd('cmd_list_workspaces')) ?? [];
|
||||||
|
return workspaces;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
|
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
|
||||||
if (id === null) return null;
|
if (id === null) return null;
|
||||||
const cookieJar: CookieJar = (await invokeCmd('cmd_get_cookie_jar', { id })) ?? null;
|
const cookieJar: CookieJar = (await invokeCmd('cmd_get_cookie_jar', { id })) ?? null;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type TauriCmd =
|
|||||||
| 'cmd_check_for_updates'
|
| 'cmd_check_for_updates'
|
||||||
| 'cmd_create_cookie_jar'
|
| 'cmd_create_cookie_jar'
|
||||||
| 'cmd_create_environment'
|
| 'cmd_create_environment'
|
||||||
|
| 'cmd_template_tokens_to_string'
|
||||||
| 'cmd_create_folder'
|
| 'cmd_create_folder'
|
||||||
| 'cmd_create_grpc_request'
|
| 'cmd_create_grpc_request'
|
||||||
| 'cmd_create_http_request'
|
| 'cmd_create_http_request'
|
||||||
@@ -21,6 +22,7 @@ type TauriCmd =
|
|||||||
| 'cmd_delete_http_request'
|
| 'cmd_delete_http_request'
|
||||||
| 'cmd_delete_http_response'
|
| 'cmd_delete_http_response'
|
||||||
| 'cmd_delete_workspace'
|
| 'cmd_delete_workspace'
|
||||||
|
| 'cmd_dismiss_notification'
|
||||||
| 'cmd_duplicate_grpc_request'
|
| 'cmd_duplicate_grpc_request'
|
||||||
| 'cmd_duplicate_http_request'
|
| 'cmd_duplicate_http_request'
|
||||||
| 'cmd_export_data'
|
| 'cmd_export_data'
|
||||||
@@ -35,6 +37,7 @@ type TauriCmd =
|
|||||||
| 'cmd_get_workspace'
|
| 'cmd_get_workspace'
|
||||||
| 'cmd_grpc_go'
|
| 'cmd_grpc_go'
|
||||||
| 'cmd_grpc_reflect'
|
| 'cmd_grpc_reflect'
|
||||||
|
| 'cmd_http_request_actions'
|
||||||
| 'cmd_import_data'
|
| 'cmd_import_data'
|
||||||
| 'cmd_list_cookie_jars'
|
| 'cmd_list_cookie_jars'
|
||||||
| 'cmd_list_environments'
|
| 'cmd_list_environments'
|
||||||
@@ -48,7 +51,8 @@ type TauriCmd =
|
|||||||
| 'cmd_metadata'
|
| 'cmd_metadata'
|
||||||
| 'cmd_new_nested_window'
|
| 'cmd_new_nested_window'
|
||||||
| 'cmd_new_window'
|
| 'cmd_new_window'
|
||||||
| 'cmd_dismiss_notification'
|
| 'cmd_parse_template'
|
||||||
|
| 'cmd_render_template'
|
||||||
| 'cmd_save_response'
|
| 'cmd_save_response'
|
||||||
| 'cmd_send_ephemeral_request'
|
| 'cmd_send_ephemeral_request'
|
||||||
| 'cmd_send_http_request'
|
| 'cmd_send_http_request'
|
||||||
@@ -62,7 +66,6 @@ type TauriCmd =
|
|||||||
| 'cmd_update_http_request'
|
| 'cmd_update_http_request'
|
||||||
| 'cmd_update_settings'
|
| 'cmd_update_settings'
|
||||||
| 'cmd_update_workspace'
|
| 'cmd_update_workspace'
|
||||||
| 'cmd_http_request_actions'
|
|
||||||
| 'cmd_write_file_dev';
|
| 'cmd_write_file_dev';
|
||||||
|
|
||||||
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type YaakTheme = YaakColors & {
|
|||||||
appHeader: Partial<YaakColors>;
|
appHeader: Partial<YaakColors>;
|
||||||
button: Partial<YaakColors>;
|
button: Partial<YaakColors>;
|
||||||
banner: Partial<YaakColors>;
|
banner: Partial<YaakColors>;
|
||||||
placeholder: Partial<YaakColors>;
|
templateTag: Partial<YaakColors>;
|
||||||
urlBar: Partial<YaakColors>;
|
urlBar: Partial<YaakColors>;
|
||||||
editor: Partial<YaakColors>;
|
editor: Partial<YaakColors>;
|
||||||
input: Partial<YaakColors>;
|
input: Partial<YaakColors>;
|
||||||
@@ -87,7 +87,7 @@ function themeVariables(theme?: Partial<YaakColors>, base?: CSSVariables): CSSVa
|
|||||||
return vars;
|
return vars;
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeholderColorVariables(color: YaakColor): Partial<CSSVariables> {
|
function templateTagColorVariables(color: YaakColor): Partial<CSSVariables> {
|
||||||
return {
|
return {
|
||||||
text: color.lift(0.6),
|
text: color.lift(0.6),
|
||||||
textSubtle: color.lift(0.4),
|
textSubtle: color.lift(0.4),
|
||||||
@@ -201,14 +201,14 @@ function bannerCSS(color: YaakColorKey, colors?: Partial<YaakColors>): string |
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeholderCSS(color: YaakColorKey, colors?: Partial<YaakColors>): string | null {
|
function templateTagCSS(color: YaakColorKey, colors?: Partial<YaakColors>): string | null {
|
||||||
const yaakColor = colors?.[color];
|
const yaakColor = colors?.[color];
|
||||||
if (yaakColor == null) {
|
if (yaakColor == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
variablesToCSS(`.x-theme-placeholder--${color}`, placeholderColorVariables(yaakColor)),
|
variablesToCSS(`.x-theme-templateTag--${color}`, templateTagColorVariables(yaakColor)),
|
||||||
].join('\n\n');
|
].join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export function getThemeCSS(theme: YaakTheme): string {
|
|||||||
bannerCSS(key as YaakColorKey, theme.components?.banner ?? colors),
|
bannerCSS(key as YaakColorKey, theme.components?.banner ?? colors),
|
||||||
),
|
),
|
||||||
...Object.keys(colors ?? {}).map((key) =>
|
...Object.keys(colors ?? {}).map((key) =>
|
||||||
placeholderCSS(key as YaakColorKey, theme.components?.placeholder ?? colors),
|
templateTagCSS(key as YaakColorKey, theme.components?.templateTag ?? colors),
|
||||||
),
|
),
|
||||||
].join('\n\n');
|
].join('\n\n');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
4
src-web/lib/truncate.ts
Normal file
4
src-web/lib/truncate.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export function truncate(text: string, len: number): string {
|
||||||
|
if (text.length <= len) return text;
|
||||||
|
return text.slice(0, len) + '…';
|
||||||
|
}
|
||||||
@@ -26,6 +26,13 @@
|
|||||||
@apply select-none cursor-default;
|
@apply select-none cursor-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
&::placeholder {
|
||||||
|
@apply text-placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.select-all * {
|
.select-all * {
|
||||||
/*@apply select-all;*/
|
/*@apply select-all;*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
|
import { attachConsole } from '@tauri-apps/plugin-log';
|
||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { pdfjs } from 'react-pdf';
|
import { pdfjs } from 'react-pdf';
|
||||||
import { attachConsole } from '@tauri-apps/plugin-log';
|
|
||||||
import { App } from './components/App';
|
import { App } from './components/App';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
|
placeholder: 'var(--textSubtlest)',
|
||||||
// placeholder: 'var(--fg-subtler)', // Now a component?
|
|
||||||
shadow: 'var(--shadow)',
|
shadow: 'var(--shadow)',
|
||||||
backdrop: 'var(--backdrop)',
|
backdrop: 'var(--backdrop)',
|
||||||
selection: 'var(--selection)',
|
selection: 'var(--selection)',
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"src-web",
|
"src-web",
|
||||||
"src-web/modules.d.ts",
|
"src-web/modules.d.ts",
|
||||||
"vite.config.ts",
|
"vite.config.ts"
|
||||||
"tailwind.config.cjs"
|
|
||||||
],
|
],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user