diff --git a/plugins/action-copy-curl/package.json b/plugins/action-copy-curl/package.json index a93cf38a..090e1dc7 100644 --- a/plugins/action-copy-curl/package.json +++ b/plugins/action-copy-curl/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/action-copy-grpcurl/package.json b/plugins/action-copy-grpcurl/package.json index 0de0d2aa..e8a3bedc 100644 --- a/plugins/action-copy-grpcurl/package.json +++ b/plugins/action-copy-grpcurl/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/auth-apikey/package.json b/plugins/auth-apikey/package.json index 7e154198..0b6047d0 100644 --- a/plugins/auth-apikey/package.json +++ b/plugins/auth-apikey/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/auth-basic/package.json b/plugins/auth-basic/package.json index 8dd9bce8..155bdb10 100644 --- a/plugins/auth-basic/package.json +++ b/plugins/auth-basic/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/auth-bearer/package.json b/plugins/auth-bearer/package.json index 8076ff95..e10f87ee 100644 --- a/plugins/auth-bearer/package.json +++ b/plugins/auth-bearer/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/auth-jwt/package.json b/plugins/auth-jwt/package.json index 67f255d3..703132b8 100644 --- a/plugins/auth-jwt/package.json +++ b/plugins/auth-jwt/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "jsonwebtoken": "^9.0.2" diff --git a/plugins/auth-oauth2/package.json b/plugins/auth-oauth2/package.json index f9092104..43641dd6 100644 --- a/plugins/auth-oauth2/package.json +++ b/plugins/auth-oauth2/package.json @@ -12,6 +12,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/filter-jsonpath/package.json b/plugins/filter-jsonpath/package.json index 591400a1..ad8b5217 100644 --- a/plugins/filter-jsonpath/package.json +++ b/plugins/filter-jsonpath/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "jsonpath-plus": "^10.3.0" diff --git a/plugins/filter-xpath/package.json b/plugins/filter-xpath/package.json index d0366235..f559eec2 100644 --- a/plugins/filter-xpath/package.json +++ b/plugins/filter-xpath/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "@xmldom/xmldom": "^0.9.8", diff --git a/plugins/importer-curl/package.json b/plugins/importer-curl/package.json index 0a126ac0..027fd34d 100644 --- a/plugins/importer-curl/package.json +++ b/plugins/importer-curl/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "shell-quote": "^1.8.1" diff --git a/plugins/importer-insomnia/package.json b/plugins/importer-insomnia/package.json index 1a328bfa..8315a1a9 100644 --- a/plugins/importer-insomnia/package.json +++ b/plugins/importer-insomnia/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "yaml": "^2.4.2" diff --git a/plugins/importer-openapi/package.json b/plugins/importer-openapi/package.json index 906031f4..21a08e2f 100644 --- a/plugins/importer-openapi/package.json +++ b/plugins/importer-openapi/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "openapi-to-postmanv2": "^5.0.0", diff --git a/plugins/importer-postman/package.json b/plugins/importer-postman/package.json index e1f0c0e5..ca8a2bcb 100644 --- a/plugins/importer-postman/package.json +++ b/plugins/importer-postman/package.json @@ -8,6 +8,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/importer-yaak/package.json b/plugins/importer-yaak/package.json index a894ca5b..45c1d0a4 100644 --- a/plugins/importer-yaak/package.json +++ b/plugins/importer-yaak/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-cookie/package.json b/plugins/template-function-cookie/package.json index 6b68c4ba..3b176368 100644 --- a/plugins/template-function-cookie/package.json +++ b/plugins/template-function-cookie/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-encode/package.json b/plugins/template-function-encode/package.json index 749a507c..f60e1d9c 100644 --- a/plugins/template-function-encode/package.json +++ b/plugins/template-function-encode/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-fs/package.json b/plugins/template-function-fs/package.json index cb1781c3..82a89ab8 100644 --- a/plugins/template-function-fs/package.json +++ b/plugins/template-function-fs/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-hash/package.json b/plugins/template-function-hash/package.json index fe7e00e7..6a93f0c7 100755 --- a/plugins/template-function-hash/package.json +++ b/plugins/template-function-hash/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-json/package.json b/plugins/template-function-json/package.json index 4ca9f9a0..e417cb4f 100755 --- a/plugins/template-function-json/package.json +++ b/plugins/template-function-json/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "jsonpath-plus": "^10.3.0" diff --git a/plugins/template-function-prompt/package.json b/plugins/template-function-prompt/package.json index 8f8a177a..ce04a42d 100644 --- a/plugins/template-function-prompt/package.json +++ b/plugins/template-function-prompt/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-regex/package.json b/plugins/template-function-regex/package.json index 1050db21..bd8797f8 100644 --- a/plugins/template-function-regex/package.json +++ b/plugins/template-function-regex/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-request/package.json b/plugins/template-function-request/package.json index 9bc71759..e11e9c13 100755 --- a/plugins/template-function-request/package.json +++ b/plugins/template-function-request/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/plugins/template-function-response/package.json b/plugins/template-function-response/package.json index fa97b8a9..46c0f3de 100644 --- a/plugins/template-function-response/package.json +++ b/plugins/template-function-response/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "jsonpath-plus": "^10.3.0", diff --git a/plugins/template-function-timestamp/package.json b/plugins/template-function-timestamp/package.json index 5d2952dd..d79ff99f 100755 --- a/plugins/template-function-timestamp/package.json +++ b/plugins/template-function-timestamp/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "date-fns": "^4.1.0" diff --git a/plugins/template-function-uuid/package.json b/plugins/template-function-uuid/package.json index 24974675..5a490255 100644 --- a/plugins/template-function-uuid/package.json +++ b/plugins/template-function-uuid/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "uuid": "^11.1.0" diff --git a/plugins/template-function-xml/package.json b/plugins/template-function-xml/package.json index b077b669..f2b9b238 100755 --- a/plugins/template-function-xml/package.json +++ b/plugins/template-function-xml/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "@xmldom/xmldom": "^0.9.8", diff --git a/plugins/themes-yaak/package.json b/plugins/themes-yaak/package.json index 758df6cc..9eece24d 100644 --- a/plugins/themes-yaak/package.json +++ b/plugins/themes-yaak/package.json @@ -7,6 +7,6 @@ "scripts": { "build": "yaakcli build", "dev": "yaakcli dev", - "lint": "eslint . --ext .ts,.tsx" + "lint":"tsc --noEmit && eslint . --ext .ts,.tsx" } } diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 1c792f39..4cd1e5bb 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -43,14 +43,14 @@ pub async fn send_http_request( ) -> Result { let app_handle = window.app_handle().clone(); let plugin_manager = app_handle.state::(); - let (settings, workspace) = { - let db = window.db(); - let settings = db.get_settings(); - let workspace = db.get_workspace(&unrendered_request.workspace_id)?; - (settings, workspace) - }; - let base_environment = - app_handle.db().get_base_environment(&unrendered_request.workspace_id)?; + let settings = window.db().get_settings(); + let workspace = window.db().get_workspace(&unrendered_request.workspace_id)?; + let environment_id = environment.map(|e| e.id); + let environment_chain = window.db().resolve_environments( + &unrendered_request.workspace_id, + unrendered_request.folder_id.as_deref(), + environment_id.as_deref(), + )?; let response_id = og_response.id.clone(); let response = Arc::new(Mutex::new(og_response.clone())); @@ -76,20 +76,17 @@ pub async fn send_http_request( RenderPurpose::Send, ); - let request = - match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb) - .await - { - Ok(r) => r, - Err(e) => { - return Ok(response_err( - &app_handle, - &*response.lock().await, - e.to_string(), - &update_source, - )); - } - }; + let request = match render_http_request(&resolved_request, environment_chain, &cb).await { + Ok(r) => r, + Err(e) => { + return Ok(response_err( + &app_handle, + &*response.lock().await, + e.to_string(), + &update_source, + )); + } + }; let mut url_string = request.url.clone(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 238cd269..9e25941e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,8 +30,9 @@ use yaak_common::window::WorkspaceWindowTrait; use yaak_grpc::manager::{DynamicMessage, GrpcHandle}; use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message}; use yaak_models::models::{ - CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, - GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace, WorkspaceMeta, + AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, + GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace, + WorkspaceMeta, }; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; @@ -110,15 +111,11 @@ async fn cmd_render_template( workspace_id: &str, environment_id: Option<&str>, ) -> YaakResult { - let environment = match environment_id { - Some(id) => app_handle.db().get_environment(id).ok(), - None => None, - }; - let base_environment = app_handle.db().get_base_environment(&workspace_id)?; + let environment_chain = + app_handle.db().resolve_environments(workspace_id, None, environment_id)?; let result = render_template( template, - &base_environment, - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -147,21 +144,19 @@ async fn cmd_grpc_reflect( app_handle: AppHandle, grpc_handle: State<'_, Mutex>, ) -> YaakResult> { - let environment = match environment_id { - Some(id) => app_handle.db().get_environment(id).ok(), - None => None, - }; let unrendered_request = app_handle.db().get_grpc_request(request_id)?; let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?; - let base_environment = - app_handle.db().get_base_environment(&unrendered_request.workspace_id)?; + let environment_chain = app_handle.db().resolve_environments( + &unrendered_request.workspace_id, + unrendered_request.folder_id.as_deref(), + environment_id, + )?; let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?; let req = render_grpc_request( &resolved_request, - &base_environment, - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -196,20 +191,18 @@ async fn cmd_grpc_go( window: WebviewWindow, grpc_handle: State<'_, Mutex>, ) -> YaakResult { - let environment = match environment_id { - Some(id) => app_handle.db().get_environment(id).ok(), - None => None, - }; let unrendered_request = app_handle.db().get_grpc_request(request_id)?; let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?; - let base_environment = - app_handle.db().get_base_environment(&unrendered_request.workspace_id)?; + let environment_chain = app_handle.db().resolve_environments( + &unrendered_request.workspace_id, + unrendered_request.folder_id.as_deref(), + environment_id, + )?; let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?; let request = render_grpc_request( &resolved_request, - &base_environment, - environment.as_ref(), + environment_chain.clone(), &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -300,9 +293,8 @@ async fn cmd_grpc_go( let cb = { let cancelled_rx = cancelled_rx.clone(); let app_handle = app_handle.clone(); + let environment_chain = environment_chain.clone(); let window = window.clone(); - let base_environment = base_environment.clone(); - let environment = environment.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); @@ -327,12 +319,12 @@ async fn cmd_grpc_go( let app_handle = app_handle.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); + let environment_chain = environment_chain.clone(); let msg = block_in_place(|| { tauri::async_runtime::block_on(async { render_template( msg.as_str(), - &base_environment, - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -396,12 +388,12 @@ async fn cmd_grpc_go( let window = window.clone(); let app_handle = app_handle.clone(); let base_event = base_msg.clone(); + let environment_chain = environment_chain.clone(); let req = request.clone(); let msg = if req.message.is_empty() { "{}".to_string() } else { req.message }; let msg = render_template( msg.as_str(), - &base_environment.clone(), - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -833,30 +825,25 @@ async fn cmd_get_http_authentication_config( plugin_manager: State<'_, PluginManager>, auth_name: &str, values: HashMap, - request_id: &str, + request: AnyModel, environment_id: Option<&str>, - workspace_id: &str, ) -> YaakResult { - let base_environment = window.db().get_base_environment(&workspace_id)?; - let environment = match environment_id { - Some(id) => match window.db().get_environment(id) { - Ok(env) => Some(env), - Err(e) => { - warn!("Failed to find environment by id {id} {}", e); - None - } + let (workspace_id, folder_id) = match request.clone() { + AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::Folder(m) => (m.workspace_id, m.folder_id), + AnyModel::Workspace(m) => (m.id, None), + m => { + return Err(GenericError(format!("Unsupported model to call auth config {m:?}"))); }, - None => None, }; + + let environment_chain = + window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; + Ok(plugin_manager - .get_http_authentication_config( - &window, - &base_environment, - environment.as_ref(), - auth_name, - values, - request_id, - ) + .get_http_authentication_config(&window, environment_chain, auth_name, values, request.id()) .await?) } @@ -907,30 +894,29 @@ async fn cmd_call_http_authentication_action( auth_name: &str, action_index: i32, values: HashMap, - model_id: &str, - workspace_id: &str, + model: AnyModel, environment_id: Option<&str>, ) -> YaakResult<()> { - let base_environment = window.db().get_base_environment(&workspace_id)?; - let environment = match environment_id { - Some(id) => match window.db().get_environment(id) { - Ok(env) => Some(env), - Err(e) => { - warn!("Failed to find environment by id {id} {}", e); - None - } - }, - None => None, + let (workspace_id, folder_id) = match model.clone() { + AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::Folder(m) => (m.workspace_id, m.folder_id), + AnyModel::Workspace(m) => (m.id, None), + m => { + return Err(GenericError(format!("Unsupported model to call auth {m:?}"))); + } }; + let environment_chain = + window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; Ok(plugin_manager .call_http_authentication_action( &window, - &base_environment, - environment.as_ref(), + environment_chain, auth_name, action_index, values, - model_id, + &model.id(), ) .await?) } diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index a2840048..6646787f 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -74,20 +74,15 @@ pub(crate) async fn handle_plugin_event( let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); - let environment = environment_from_window(&window); - let base_environment = app_handle + let environment_id = environment_from_window(&window).map(|e| e.id); + let environment_chain = window .db() - .get_base_environment(&workspace.id) - .expect("Failed to get base environment"); + .resolve_environments(&workspace.id, None, environment_id.as_deref()) + .expect("Failed to resolve environments"); let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); - let grpc_request = render_grpc_request( - &req.grpc_request, - &base_environment, - environment.as_ref(), - &cb, - ) - .await - .expect("Failed to render grpc request"); + let grpc_request = render_grpc_request(&req.grpc_request, environment_chain, &cb) + .await + .expect("Failed to render grpc request"); Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse { grpc_request, })) @@ -98,20 +93,15 @@ pub(crate) async fn handle_plugin_event( let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); - let environment = environment_from_window(&window); - let base_environment = app_handle + let environment_id = environment_from_window(&window).map(|e| e.id); + let environment_chain = window .db() - .get_base_environment(&workspace.id) - .expect("Failed to get base environment"); + .resolve_environments(&workspace.id, None, environment_id.as_deref()) + .expect("Failed to resolve environments"); let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); - let http_request = render_http_request( - &req.http_request, - &base_environment, - environment.as_ref(), - &cb, - ) - .await - .expect("Failed to render http request"); + let http_request = render_http_request(&req.http_request, environment_chain, &cb) + .await + .expect("Failed to render http request"); Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse { http_request, })) @@ -122,13 +112,13 @@ pub(crate) async fn handle_plugin_event( let workspace = workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); - let environment = environment_from_window(&window); - let base_environment = app_handle + let environment_id = environment_from_window(&window).map(|e| e.id); + let environment_chain = window .db() - .get_base_environment(&workspace.id) - .expect("Failed to get base environment"); + .resolve_environments(&workspace.id, None, environment_id.as_deref()) + .expect("Failed to resolve environments"); let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); - let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb) + let data = render_json_value(req.data, environment_chain, &cb) .await .expect("Failed to render template"); Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })) diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index 6efd6a0e..05e1f8f9 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -5,35 +5,32 @@ use yaak_models::models::{ Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, }; use yaak_models::render::make_vars_hashmap; -use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback}; +use yaak_templates::{TemplateCallback, parse_and_render, render_json_value_raw}; pub async fn render_template( template: &str, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, cb: &T, ) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); render(template, vars, cb).await } pub async fn render_json_value( value: Value, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, cb: &T, ) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); render_json_value_raw(value, vars, cb).await } pub async fn render_grpc_request( r: &GrpcRequest, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, cb: &T, ) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); let mut metadata = Vec::new(); for p in r.metadata.clone() { @@ -62,11 +59,10 @@ pub async fn render_grpc_request( pub async fn render_http_request( r: &HttpRequest, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, cb: &T, ) -> yaak_templates::error::Result { - let vars = &make_vars_hashmap(base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); let mut url_parameters = Vec::new(); for p in r.url_parameters.clone() { diff --git a/src-tauri/yaak-git/bindings/gen_models.ts b/src-tauri/yaak-git/bindings/gen_models.ts index b8341f92..1110f887 100644 --- a/src-tauri/yaak-git/bindings/gen_models.ts +++ b/src-tauri/yaak-git/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array, color: string | null, parentModel: string, parentId: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-git/src/util.rs b/src-tauri/yaak-git/src/util.rs index 46c06279..63554227 100644 --- a/src-tauri/yaak-git/src/util.rs +++ b/src-tauri/yaak-git/src/util.rs @@ -24,7 +24,7 @@ pub(crate) fn find_ssh_key() -> Option { None } -pub(crate) fn get_current_branch(repo: &Repository) -> Result> { +pub(crate) fn get_current_branch(repo: &Repository) -> Result>> { for b in repo.branches(None)? { let branch = b?.0; if branch.is_head() { @@ -101,7 +101,7 @@ pub(crate) fn get_default_remote_in_repo(repo: &Repository) -> Result { return Ok(DEFAULT_REMOTE_NAME.into()); } - // if only one remote exists pick that + // if only one remote exists, pick that if remotes.len() == 1 { let first_remote = remotes .iter() diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index accd0c2c..bf45715a 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs"; export type EncryptedKey = { encryptedKey: string, }; -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array, color: string | null, parentModel: string, parentId: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-models/migrations/20250918141129_request-folder-environments.sql b/src-tauri/yaak-models/migrations/20250918141129_request-folder-environments.sql new file mode 100644 index 00000000..727bdaf4 --- /dev/null +++ b/src-tauri/yaak-models/migrations/20250918141129_request-folder-environments.sql @@ -0,0 +1,62 @@ +-- Create temporary table for migration +CREATE TABLE environments__new +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at DATETIME, + workspace_id TEXT NOT NULL + REFERENCES workspaces ON DELETE CASCADE, + + name TEXT NOT NULL, + variables TEXT DEFAULT '[]' NOT NULL, + model TEXT DEFAULT 'environment', + public BOOLEAN DEFAULT FALSE, + color TEXT, + + -- NEW + parent_model TEXT DEFAULT 'workspace' NOT NULL, + parent_id TEXT +); + +-- Backfill the data from the old table +-- - base=1 -> (workspace, NULL) +-- - base=0 -> (environment, id_of_workspace_base) (fallback to workspace,NULL if none) +INSERT INTO environments__new +(id, created_at, updated_at, deleted_at, workspace_id, name, variables, model, public, color, parent_model, parent_id) +SELECT + e.id, + e.created_at, + e.updated_at, + e.deleted_at, + e.workspace_id, + e.name, + e.variables, + e.model, + e.public, + e.color, + CASE + WHEN e.base = 1 THEN 'workspace' + WHEN ( + SELECT COUNT(1) + FROM environments b + WHERE b.workspace_id = e.workspace_id AND b.base = 1 + ) > 0 THEN 'environment' + ELSE 'workspace' + END AS parent_model, + CASE + WHEN e.base = 1 THEN NULL + ELSE ( + SELECT b.id + FROM environments b + WHERE b.workspace_id = e.workspace_id AND b.base = 1 + ORDER BY b.created_at ASC, b.id ASC + LIMIT 1 + ) + END AS parent_id +FROM environments e; + +-- Move everything to the new table +DROP TABLE environments; +ALTER TABLE environments__new + RENAME TO environments; diff --git a/src-tauri/yaak-models/src/db_context.rs b/src-tauri/yaak-models/src/db_context.rs index 06e0485e..4b24ad0e 100644 --- a/src-tauri/yaak-models/src/db_context.rs +++ b/src-tauri/yaak-models/src/db_context.rs @@ -89,7 +89,7 @@ impl<'a> DbContext<'a> { col: impl IntoColumnRef, value: impl Into, limit: Option, - ) -> crate::error::Result> + ) -> Result> where M: Into + Clone + UpsertModelInfo, { diff --git a/src-tauri/yaak-models/src/error.rs b/src-tauri/yaak-models/src/error.rs index 466e75b6..515b2405 100644 --- a/src-tauri/yaak-models/src/error.rs +++ b/src-tauri/yaak-models/src/error.rs @@ -29,6 +29,9 @@ pub enum Error { #[error("Multiple base environments for {0}. Delete duplicates before continuing.")] MultipleBaseEnvironments(String), + + #[error("Multiple folder environments for {0}. Delete duplicates before continuing.")] + MultipleFolderEnvironments(String), #[error("unknown error")] Unknown, diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index 959f02b4..23961266 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -533,9 +533,10 @@ pub struct Environment { pub name: String, pub public: bool, - pub base: bool, pub variables: Vec, pub color: Option, + pub parent_model: String, + pub parent_id: Option, } impl UpsertModelInfo for Environment { @@ -568,7 +569,8 @@ impl UpsertModelInfo for Environment { (CreatedAt, upsert_date(source, self.created_at)), (UpdatedAt, upsert_date(source, self.updated_at)), (WorkspaceId, self.workspace_id.into()), - (Base, self.base.into()), + (ParentId, self.parent_id.into()), + (ParentModel, self.parent_model.into()), (Color, self.color.into()), (Name, self.name.trim().into()), (Public, self.public.into()), @@ -579,7 +581,8 @@ impl UpsertModelInfo for Environment { fn update_columns() -> Vec { vec![ EnvironmentIden::UpdatedAt, - EnvironmentIden::Base, + EnvironmentIden::ParentId, + EnvironmentIden::ParentModel, EnvironmentIden::Color, EnvironmentIden::Name, EnvironmentIden::Public, @@ -598,7 +601,8 @@ impl UpsertModelInfo for Environment { workspace_id: row.get("workspace_id")?, created_at: row.get("created_at")?, updated_at: row.get("updated_at")?, - base: row.get("base")?, + parent_id: row.get("parent_id")?, + parent_model: row.get("parent_model")?, color: row.get("color")?, name: row.get("name")?, public: row.get("public")?, @@ -2072,6 +2076,17 @@ macro_rules! define_any_model { )* } + impl AnyModel { + #[inline] + pub fn id(&self) -> &str { + match self { + $( + AnyModel::$type(inner) => &inner.id, + )* + } + } + } + $( impl From<$type> for AnyModel { fn from(value: $type) -> Self { diff --git a/src-tauri/yaak-models/src/queries/environments.rs b/src-tauri/yaak-models/src/queries/environments.rs index 3f1b3bd9..ae19ee8c 100644 --- a/src-tauri/yaak-models/src/queries/environments.rs +++ b/src-tauri/yaak-models/src/queries/environments.rs @@ -1,5 +1,7 @@ use crate::db_context::DbContext; -use crate::error::Error::{MissingBaseEnvironment, MultipleBaseEnvironments}; +use crate::error::Error::{ + MissingBaseEnvironment, MultipleBaseEnvironments, MultipleFolderEnvironments, +}; use crate::error::Result; use crate::models::{Environment, EnvironmentIden, EnvironmentVariable}; use crate::util::UpdateSource; @@ -10,21 +12,31 @@ impl<'a> DbContext<'a> { self.find_one(EnvironmentIden::Id, id) } + pub fn get_environment_by_folder_id(&self, folder_id: &str) -> Result> { + let environments: Vec = + self.find_many(EnvironmentIden::ParentId, folder_id, None)?; + if environments.len() > 1 { + return Err(MultipleFolderEnvironments(folder_id.to_string())); + } + + Ok(environments.get(0).cloned()) + } + pub fn get_base_environment(&self, workspace_id: &str) -> Result { let environments = self.list_environments_ensure_base(workspace_id)?; - let base_environments = - environments.into_iter().filter(|e| e.base).collect::>(); + let base_environments = environments + .into_iter() + .filter(|e| e.parent_id.is_none()) + .collect::>(); if base_environments.len() > 1 { return Err(MultipleBaseEnvironments(workspace_id.to_string())); } - let base_environment = base_environments.into_iter().find(|e| e.base).ok_or( + Ok(base_environments.first().cloned().ok_or( // Should never happen because one should be created above if it does not exist MissingBaseEnvironment(workspace_id.to_string()), - )?; - - Ok(base_environment) + )?) } /// Lists environments and will create a base environment if one doesn't exist @@ -32,13 +44,12 @@ impl<'a> DbContext<'a> { let mut environments = self.find_many::(EnvironmentIden::WorkspaceId, workspace_id, None)?; - let base_environment = environments.iter().find(|e| e.base); + let base_environment = environments.iter().find(|e| e.parent_id.is_none()); if let None = base_environment { let e = self.upsert_environment( &Environment { workspace_id: workspace_id.to_string(), - base: true, name: "Global Variables".to_string(), ..Default::default() }, @@ -98,4 +109,43 @@ impl<'a> DbContext<'a> { source, ) } + + pub fn resolve_environments( + &self, + workspace_id: &str, + folder_id: Option<&str>, + active_environment_id: Option<&str>, + ) -> Result> { + let mut environments = Vec::new(); + + if let Some(folder_id) = folder_id { + let folder = self.get_folder(folder_id)?; + + // Add current folder's environment + if let Some(e) = self.get_environment_by_folder_id(folder_id)? { + environments.push(e); + }; + + // Recurse up + let ancestors = self.resolve_environments( + workspace_id, + folder.folder_id.as_deref(), + active_environment_id, + )?; + environments.extend(ancestors); + } else { + // Add active and base environments + if let Some(id) = active_environment_id { + if let Ok(e) = self.get_environment(&id) { + // Add active sub environment + environments.push(e); + }; + }; + + // Add the base environment + environments.push(self.get_base_environment(workspace_id)?); + } + + Ok(environments) + } } diff --git a/src-tauri/yaak-models/src/queries/folders.rs b/src-tauri/yaak-models/src/queries/folders.rs index af29ea39..466ecbbc 100644 --- a/src-tauri/yaak-models/src/queries/folders.rs +++ b/src-tauri/yaak-models/src/queries/folders.rs @@ -1,10 +1,7 @@ use crate::connection_or_tx::ConnectionOrTx; use crate::db_context::DbContext; use crate::error::Result; -use crate::models::{ - Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader, - HttpRequestIden, WebsocketRequest, WebsocketRequestIden, -}; +use crate::models::{Environment, EnvironmentIden, Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestHeader, HttpRequestIden, WebsocketRequest, WebsocketRequestIden}; use crate::util::UpdateSource; use serde_json::Value; use std::collections::BTreeMap; @@ -37,6 +34,10 @@ impl<'a> DbContext<'a> { self.delete_websocket_request(&m, source)?; } + for e in self.find_many(EnvironmentIden::ParentId, fid, None)? { + self.delete_environment(&e, source)?; + } + // Recurse down into child folders for folder in self.find_many::(FolderIden::FolderId, fid, None)? { self.delete_folder(&folder, source)?; @@ -99,6 +100,17 @@ impl<'a> DbContext<'a> { )?; } + for m in self.find_many::(EnvironmentIden::ParentId, fid, None)? { + self.upsert_environment( + &Environment { + id: "".into(), + parent_id: Some(new_folder.id.clone()), + ..m + }, + source, + )?; + } + for m in self.find_many::(FolderIden::FolderId, fid, None)? { // Recurse down self.duplicate_folder( diff --git a/src-tauri/yaak-models/src/query_manager.rs b/src-tauri/yaak-models/src/query_manager.rs index 10d63933..4c4af904 100644 --- a/src-tauri/yaak-models/src/query_manager.rs +++ b/src-tauri/yaak-models/src/query_manager.rs @@ -64,7 +64,7 @@ impl QueryManager { } } - pub fn connect(&self) -> DbContext { + pub fn connect(&self) -> DbContext<'_> { let conn = self .pool .lock() diff --git a/src-tauri/yaak-models/src/render.rs b/src-tauri/yaak-models/src/render.rs index 4db65912..aa466aff 100644 --- a/src-tauri/yaak-models/src/render.rs +++ b/src-tauri/yaak-models/src/render.rs @@ -1,14 +1,10 @@ -use std::collections::HashMap; use crate::models::{Environment, EnvironmentVariable}; +use std::collections::HashMap; -pub fn make_vars_hashmap( - base_environment: &Environment, - environment: Option<&Environment>, -) -> HashMap { +pub fn make_vars_hashmap(environment_chain: Vec) -> HashMap { let mut variables = HashMap::new(); - variables = add_variable_to_map(variables, &base_environment.variables); - if let Some(e) = environment { + for e in environment_chain.iter().rev() { variables = add_variable_to_map(variables, &e.variables); } @@ -31,4 +27,3 @@ fn add_variable_to_map( map } - diff --git a/src-tauri/yaak-plugins/bindings/gen_models.ts b/src-tauri/yaak-plugins/bindings/gen_models.ts index 0880935d..1136b85a 100644 --- a/src-tauri/yaak-plugins/bindings/gen_models.ts +++ b/src-tauri/yaak-plugins/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array, color: string | null, parentModel: string, parentId: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 0b0c4feb..aa8610a4 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -584,8 +584,7 @@ impl PluginManager { pub async fn get_http_authentication_config( &self, window: &WebviewWindow, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, auth_name: &str, values: HashMap, request_id: &str, @@ -596,7 +595,7 @@ impl PluginManager { .find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None }) .ok_or(PluginNotFoundErr(auth_name.into()))?; - let vars = &make_vars_hashmap(&base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); let cb = PluginTemplateCallback::new( window.app_handle(), &PluginWindowContext::new(&window), @@ -629,14 +628,13 @@ impl PluginManager { pub async fn call_http_authentication_action( &self, window: &WebviewWindow, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, auth_name: &str, action_index: i32, values: HashMap, model_id: &str, ) -> Result<()> { - let vars = &make_vars_hashmap(&base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); let rendered_values = render_json_value_raw( json!(values), vars, diff --git a/src-tauri/yaak-plugins/src/nodejs.rs b/src-tauri/yaak-plugins/src/nodejs.rs index 0b55444d..0a048074 100644 --- a/src-tauri/yaak-plugins/src/nodejs.rs +++ b/src-tauri/yaak-plugins/src/nodejs.rs @@ -1,20 +1,12 @@ use crate::error::Result; use log::{info, warn}; -use serde; -use serde::Deserialize; use std::net::SocketAddr; use tauri::path::BaseDirectory; use tauri::{AppHandle, Manager, Runtime}; -use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::process::CommandEvent; +use tauri_plugin_shell::ShellExt; use tokio::sync::watch::Receiver; -#[derive(Deserialize, Default)] -#[serde(default, rename_all = "camelCase")] -struct PortFile { - port: i32, -} - pub async fn start_nodejs_plugin_runtime( app: &AppHandle, addr: SocketAddr, diff --git a/src-tauri/yaak-sync/bindings/gen_models.ts b/src-tauri/yaak-sync/bindings/gen_models.ts index 8e0417b5..cf43f482 100644 --- a/src-tauri/yaak-sync/bindings/gen_models.ts +++ b/src-tauri/yaak-sync/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array, color: string | null, parentModel: string, parentId: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-ws/src/commands.rs b/src-tauri/yaak-ws/src/commands.rs index c606287b..3851fbbf 100644 --- a/src-tauri/yaak-ws/src/commands.rs +++ b/src-tauri/yaak-ws/src/commands.rs @@ -109,24 +109,18 @@ pub(crate) async fn send( window: WebviewWindow, ws_manager: State<'_, Mutex>, ) -> Result { - let (connection, unrendered_request) = { - let db = app_handle.db(); - let connection = db.get_websocket_connection(connection_id)?; - let unrendered_request = db.get_websocket_request(&connection.request_id)?; - (connection, unrendered_request) - }; - let environment = match environment_id { - Some(id) => Some(app_handle.db().get_environment(id)?), - None => None, - }; - let base_environment = - app_handle.db().get_base_environment(&unrendered_request.workspace_id)?; + let connection = app_handle.db().get_websocket_connection(connection_id)?; + let unrendered_request = app_handle.db().get_websocket_request(&connection.request_id)?; + let environment_chain = app_handle.db().resolve_environments( + &unrendered_request.workspace_id, + unrendered_request.folder_id.as_deref(), + environment_id, + )?; let (resolved_request, _auth_context_id) = resolve_websocket_request(&window, &unrendered_request)?; let request = render_websocket_request( &resolved_request, - &base_environment, - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -192,19 +186,17 @@ pub(crate) async fn connect( ws_manager: State<'_, Mutex>, ) -> Result { let unrendered_request = app_handle.db().get_websocket_request(request_id)?; - let environment = match environment_id { - Some(id) => Some(app_handle.db().get_environment(id)?), - None => None, - }; - let base_environment = - app_handle.db().get_base_environment(&unrendered_request.workspace_id)?; + let environment_chain = app_handle.db().resolve_environments( + &unrendered_request.workspace_id, + unrendered_request.folder_id.as_deref(), + environment_id, + )?; let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?; let (resolved_request, auth_context_id) = resolve_websocket_request(&window, &unrendered_request)?; let request = render_websocket_request( &resolved_request, - &base_environment, - environment.as_ref(), + environment_chain, &PluginTemplateCallback::new( &app_handle, &PluginWindowContext::new(&window), @@ -305,7 +297,7 @@ pub(crate) async fn connect( // Add cookies to WS HTTP Upgrade if let Some(id) = cookie_jar_id { - let cookie_jar = app_handle.db().get_cookie_jar(id)?; + let cookie_jar = app_handle.db().get_cookie_jar(&id)?; let cookies = cookie_jar .cookies diff --git a/src-tauri/yaak-ws/src/render.rs b/src-tauri/yaak-ws/src/render.rs index 2d80a4f1..c7111277 100644 --- a/src-tauri/yaak-ws/src/render.rs +++ b/src-tauri/yaak-ws/src/render.rs @@ -6,11 +6,10 @@ use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback}; pub async fn render_websocket_request( r: &WebsocketRequest, - base_environment: &Environment, - environment: Option<&Environment>, + environment_chain: Vec, cb: &T, ) -> Result { - let vars = &make_vars_hashmap(base_environment, environment); + let vars = &make_vars_hashmap(environment_chain); let mut headers = Vec::new(); for p in r.headers.clone() { diff --git a/src-web/commands/createEnvironment.ts b/src-web/commands/createEnvironment.ts index 9a12a5d0..4ac57e20 100644 --- a/src-web/commands/createEnvironment.ts +++ b/src-web/commands/createEnvironment.ts @@ -37,7 +37,8 @@ export const createEnvironmentAndActivate = createFastMutation< name, variables: [], workspaceId, - base: false, + parentId: baseEnvironment.id, + parentModel: 'environment', }); }, onSuccess: async (environmentId) => { diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index 6bb9d126..f17df452 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -27,7 +27,8 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { createRequestAndNavigate } from '../lib/createRequestAndNavigate'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; -import { showDialog, toggleDialog } from '../lib/dialog'; +import { showDialog } from '../lib/dialog'; +import { editEnvironment } from '../lib/editEnvironment'; import { renameModelWithPrompt } from '../lib/renameModelWithPrompt'; import { resolvedModelNameWithFolders } from '../lib/resolvedModelName'; import { router } from '../lib/router'; @@ -40,7 +41,6 @@ import { HttpMethodTag } from './core/HttpMethodTag'; import { Icon } from './core/Icon'; import { PlainInput } from './core/PlainInput'; import { HStack } from './core/Stacks'; -import { EnvironmentEditDialog } from './EnvironmentEditDialog'; interface CommandPaletteGroup { key: string; @@ -125,15 +125,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { key: 'environment.edit', label: 'Edit Environment', action: 'environmentEditor.toggle', - onSelect: () => { - toggleDialog({ - id: 'environment-editor', - noPadding: true, - size: 'lg', - className: 'h-[80vh]', - render: () => , - }); - }, + onSelect: () => editEnvironment(activeEnvironment), }, { key: 'environment.create', diff --git a/src-web/components/ConfirmLargeResponse.tsx b/src-web/components/ConfirmLargeResponse.tsx index d5ff1ec4..04b4c9c7 100644 --- a/src-web/components/ConfirmLargeResponse.tsx +++ b/src-web/components/ConfirmLargeResponse.tsx @@ -51,7 +51,7 @@ export function ConfirmLargeResponse({ children, response }: Props) { color="secondary" variant="border" size="xs" - text={() => getResponseBodyText(response)} + text={() => getResponseBodyText({ responseId: response.id, filter: null })} /> )} diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index 200d6033..6beffff6 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -1,16 +1,15 @@ import classNames from 'classnames'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; -import { toggleDialog } from '../lib/dialog'; +import { editEnvironment } from '../lib/editEnvironment'; import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; -import { Button } from './core/Button'; import type { ButtonProps } from './core/Button'; +import { Button } from './core/Button'; import type { DropdownItem } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; -import { EnvironmentEditDialog } from './EnvironmentEditDialog'; type Props = { className?: string; @@ -23,16 +22,6 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo const { subEnvironments, baseEnvironment } = useEnvironmentsBreakdown(); const activeEnvironment = useActiveEnvironment(); - const showEnvironmentDialog = useCallback(() => { - toggleDialog({ - id: 'environment-editor', - noPadding: true, - size: 'lg', - className: 'h-[80vh]', - render: () => , - }); - }, [activeEnvironment]); - const items: DropdownItem[] = useMemo( () => [ ...subEnvironments.map( @@ -55,14 +44,13 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo ? [{ type: 'separator', label: 'Environments' }] : []) as DropdownItem[]), { - key: 'edit', label: 'Manage Environments', hotKeyAction: 'environmentEditor.toggle', leftSlot: , - onSelect: showEnvironmentDialog, + onSelect: () => editEnvironment(activeEnvironment), }, ], - [activeEnvironment?.id, subEnvironments, showEnvironmentDialog], + [subEnvironments, activeEnvironment], ); const hasBaseVars = @@ -79,7 +67,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo )} // If no environments, the button simply opens the dialog. // NOTE: We don't create a new button because we want to reuse the hotkey from the menu items - onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined} + onClick={subEnvironments.length === 0 ? () => editEnvironment(null) : undefined} {...buttonProps} > diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index b885cc84..8ba940f4 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -1,42 +1,28 @@ import type { Environment } from '@yaakapp-internal/models'; import { duplicateModel, patchModel } from '@yaakapp-internal/models'; -import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; import type { ReactNode } from 'react'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { createEnvironmentAndActivate } from '../commands/createEnvironment'; import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; -import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled'; -import { useKeyValue } from '../hooks/useKeyValue'; -import { useRandomKey } from '../hooks/useRandomKey'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; -import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption'; +import { isBaseEnvironment } from '../lib/model_util'; import { showPrompt } from '../lib/prompt'; import { resolvedModelName } from '../lib/resolvedModelName'; -import { - setupOrConfigureEncryption, - withEncryptionEnabled, -} from '../lib/setupOrConfigureEncryption'; import { showColorPicker } from '../lib/showColorPicker'; -import { BadgeButton } from './core/BadgeButton'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; -import { DismissibleBanner } from './core/DismissibleBanner'; import type { DropdownItem } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown'; -import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; -import { Heading } from './core/Heading'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { IconTooltip } from './core/IconTooltip'; import { InlineCode } from './core/InlineCode'; -import type { PairWithId } from './core/PairEditor'; -import { ensurePairId } from './core/PairEditor'; -import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; -import { VStack } from './core/Stacks'; import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; +import { EnvironmentEditor } from './EnvironmentEditor'; +import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; interface Props { initialEnvironment: Environment | null; @@ -97,13 +83,13 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { @@ -153,7 +139,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { ) : ( ) @@ -162,139 +148,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { ); }; -const EnvironmentEditor = function ({ - environment: selectedEnvironment, - className, -}: { - environment: Environment; - className?: string; -}) { - const workspaceId = selectedEnvironment.workspaceId; - const isEncryptionEnabled = useIsEncryptionEnabled(); - const valueVisibility = useKeyValue({ - namespace: 'global', - key: ['environmentValueVisibility', workspaceId], - fallback: false, - }); - const { allEnvironments } = useEnvironmentsBreakdown(); - const handleChange = useCallback( - (variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }), - [selectedEnvironment], - ); - const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey(); - - // Gather a list of env names from other environments to help the user get them aligned - const nameAutocomplete = useMemo(() => { - const options: GenericCompletionOption[] = []; - if (selectedEnvironment.base) { - return { options }; - } - - const allVariables = allEnvironments.flatMap((e) => e?.variables); - const allVariableNames = new Set(allVariables.map((v) => v?.name)); - for (const name of allVariableNames) { - const containingEnvs = allEnvironments.filter((e) => - e.variables.some((v) => v.name === name), - ); - const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id); - if (isAlreadyInActive) continue; - options.push({ - label: name, - type: 'constant', - detail: containingEnvs.map((e) => e.name).join(', '), - }); - } - return { options }; - }, [selectedEnvironment.base, selectedEnvironment.id, allEnvironments]); - - const validateName = useCallback((name: string) => { - // Empty just means the variable doesn't have a name yet and is unusable - if (name === '') return true; - return name.match(/^[a-z_][a-z0-9_-]*$/i) != null; - }, []); - - const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password'; - const promptToEncrypt = useMemo(() => { - if (!isEncryptionEnabled) { - return true; - } else { - return !selectedEnvironment.variables.every( - (v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure', - ); - } - }, [selectedEnvironment.variables, isEncryptionEnabled]); - - const encryptEnvironment = (environment: Environment) => { - withEncryptionEnabled(async () => { - const encryptedVariables: PairWithId[] = []; - for (const variable of environment.variables) { - const value = variable.value ? await convertTemplateToSecure(variable.value) : ''; - encryptedVariables.push(ensurePairId({ ...variable, value })); - } - await handleChange(encryptedVariables); - regenerateForceUpdateKey(); - }); - }; - - return ( - - - -
{selectedEnvironment?.name}
- {isEncryptionEnabled ? ( - promptToEncrypt ? ( - encryptEnvironment(selectedEnvironment)}> - Encrypt All Variables - - ) : ( - - Encryption Settings - - ) - ) : ( - <> - valueVisibility.set((v) => !v)}> - {valueVisibility.value ? 'Hide Values' : 'Show Values'} - - - )} -
- {selectedEnvironment.public && promptToEncrypt && ( - - This environment is sharable. Ensure variable values are encrypted to avoid accidental - leaking of secrets during directory sync or data export. - - )} -
- -
-
- ); -}; - -function SidebarButton({ +function EnvironmentDialogSidebarButton({ children, className, active, @@ -359,7 +213,7 @@ function SidebarButton({ { label: 'Rename', leftSlot: , - hidden: environment.base, + hidden: isBaseEnvironment(environment), onSelect: async () => { const name = await showPrompt({ id: 'rename-environment', @@ -392,23 +246,13 @@ function SidebarButton({ { label: environment.color ? 'Change Color' : 'Assign Color', leftSlot: , - hidden: environment.base, + hidden: isBaseEnvironment(environment), onSelect: async () => showColorPicker(environment), }, { label: `Make ${environment.public ? 'Private' : 'Sharable'}`, leftSlot: , - rightSlot: ( - - Sharable environments will be included in Directory Sync or data export. It is - recommended to encrypt all variable values within sharable environments to - prevent accidentally leaking secrets. - - } - /> - ), + rightSlot: , onSelect: async () => { await patchModel(environment, { public: !environment.public }); }, diff --git a/src-web/components/EnvironmentEditor.tsx b/src-web/components/EnvironmentEditor.tsx new file mode 100644 index 00000000..86d11272 --- /dev/null +++ b/src-web/components/EnvironmentEditor.tsx @@ -0,0 +1,169 @@ +import type { Environment } from '@yaakapp-internal/models'; +import { patchModel } from '@yaakapp-internal/models'; +import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; +import React, { useCallback, useMemo } from 'react'; +import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; +import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled'; +import { useKeyValue } from '../hooks/useKeyValue'; +import { useRandomKey } from '../hooks/useRandomKey'; +import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption'; +import { isBaseEnvironment } from '../lib/model_util'; +import { + setupOrConfigureEncryption, + withEncryptionEnabled, +} from '../lib/setupOrConfigureEncryption'; +import { BadgeButton } from './core/BadgeButton'; +import { DismissibleBanner } from './core/DismissibleBanner'; +import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; +import { Heading } from './core/Heading'; +import type { PairWithId } from './core/PairEditor'; +import { ensurePairId } from './core/PairEditor'; +import { PairOrBulkEditor } from './core/PairOrBulkEditor'; +import { VStack } from './core/Stacks'; +import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; +import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; + +export function EnvironmentEditor({ + environment: selectedEnvironment, + hideName, + className, +}: { + environment: Environment; + hideName?: boolean; + className?: string; +}) { + const workspaceId = selectedEnvironment.workspaceId; + const isEncryptionEnabled = useIsEncryptionEnabled(); + const valueVisibility = useKeyValue({ + namespace: 'global', + key: ['environmentValueVisibility', workspaceId], + fallback: false, + }); + const { allEnvironments } = useEnvironmentsBreakdown(); + const handleChange = useCallback( + (variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }), + [selectedEnvironment], + ); + const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey(); + + // Gather a list of env names from other environments to help the user get them aligned + const nameAutocomplete = useMemo(() => { + const options: GenericCompletionOption[] = []; + if (isBaseEnvironment(selectedEnvironment)) { + return { options }; + } + + const allVariables = allEnvironments.flatMap((e) => e?.variables); + const allVariableNames = new Set(allVariables.map((v) => v?.name)); + for (const name of allVariableNames) { + const containingEnvs = allEnvironments.filter((e) => + e.variables.some((v) => v.name === name), + ); + const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id); + if (isAlreadyInActive) continue; + options.push({ + label: name, + type: 'constant', + detail: containingEnvs.map((e) => e.name).join(', '), + }); + } + return { options }; + }, [selectedEnvironment, allEnvironments]); + + const validateName = useCallback((name: string) => { + // Empty just means the variable doesn't have a name yet and is unusable + if (name === '') return true; + return name.match(/^[a-z_][a-z0-9_-]*$/i) != null; + }, []); + + const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password'; + const allVariableAreEncrypted = useMemo( + () => + selectedEnvironment.variables.every( + (v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure', + ), + [selectedEnvironment.variables], + ); + + const encryptEnvironment = (environment: Environment) => { + withEncryptionEnabled(async () => { + const encryptedVariables: PairWithId[] = []; + for (const variable of environment.variables) { + const value = variable.value ? await convertTemplateToSecure(variable.value) : ''; + encryptedVariables.push(ensurePairId({ ...variable, value })); + } + await handleChange(encryptedVariables); + regenerateForceUpdateKey(); + }); + }; + + return ( + + + + {!hideName &&
{selectedEnvironment?.name}
} + {isEncryptionEnabled ? ( + !allVariableAreEncrypted ? ( + encryptEnvironment(selectedEnvironment)}> + Encrypt All Variables + + ) : ( + + Encryption Settings + + ) + ) : ( + valueVisibility.set((v) => !v)}> + {valueVisibility.value ? 'Hide Values' : 'Show Values'} + + )} + } + onClick={async () => { + await patchModel(selectedEnvironment, { public: !selectedEnvironment.public }); + }} + > + {selectedEnvironment.public ? 'Sharable' : 'Private'} + +
+ {selectedEnvironment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && ( + encryptEnvironment(selectedEnvironment), + color: 'primary', + }, + ]} + > + This sharable environment contains plain-text secrets + + )} +
+ +
+
+ ); +} diff --git a/src-web/components/EnvironmentSharableTooltip.tsx b/src-web/components/EnvironmentSharableTooltip.tsx new file mode 100644 index 00000000..5ea93be8 --- /dev/null +++ b/src-web/components/EnvironmentSharableTooltip.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { IconTooltip } from './core/IconTooltip'; + +export function EnvironmentSharableTooltip() { + return ( + + ); +} diff --git a/src-web/components/FolderSettingsDialog.tsx b/src-web/components/FolderSettingsDialog.tsx index 7031e47a..cbfc93a1 100644 --- a/src-web/components/FolderSettingsDialog.tsx +++ b/src-web/components/FolderSettingsDialog.tsx @@ -1,13 +1,19 @@ -import { foldersAtom, patchModel } from '@yaakapp-internal/models'; +import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; -import { useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useAuthTab } from '../hooks/useAuthTab'; +import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useHeadersTab } from '../hooks/useHeadersTab'; import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; +import { Button } from './core/Button'; +import { CountBadge } from './core/CountBadge'; import { Input } from './core/Input'; +import { Link } from './core/Link'; import { VStack } from './core/Stacks'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; +import { EmptyStateText } from './EmptyStateText'; +import { EnvironmentEditor } from './EnvironmentEditor'; import { HeadersEditor } from './HeadersEditor'; import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { MarkdownEditor } from './MarkdownEditor'; @@ -19,9 +25,10 @@ interface Props { const TAB_AUTH = 'auth'; const TAB_HEADERS = 'headers'; +const TAB_VARIABLES = 'variables'; const TAB_GENERAL = 'general'; -export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL; +export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL | typeof TAB_VARIABLES; export function FolderSettingsDialog({ folderId, tab }: Props) { const folders = useAtomValue(foldersAtom); @@ -30,6 +37,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) { const authTab = useAuthTab(TAB_AUTH, folder); const headersTab = useHeadersTab(TAB_HEADERS, folder); const inheritedHeaders = useInheritedHeaders(folder); + const environments = useEnvironmentsBreakdown(); + const folderEnvironment = environments.allEnvironments.find( + (e) => e.parentModel === 'folder' && e.parentId === folderId, + ); + const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length; const tabs = useMemo(() => { if (folder == null) return []; @@ -39,10 +51,15 @@ export function FolderSettingsDialog({ folderId, tab }: Props) { value: TAB_GENERAL, label: 'General', }, - ...authTab, ...headersTab, + ...authTab, + { + value: TAB_VARIABLES, + label: 'Variables', + rightSlot: numVars > 0 ? : null, + }, ]; - }, [authTab, folder, headersTab]); + }, [authTab, folder, headersTab, numVars]); if (folder == null) return null; @@ -85,6 +102,38 @@ export function FolderSettingsDialog({ folderId, tab }: Props) { stateKey={`headers.${folder.id}`} /> + + {folderEnvironment == null ? ( + + +

+ Override{' '} + + Variables + {' '} + for requests within this folder. +

+ +
+
+ ) : ( + + )} +
); } diff --git a/src-web/components/HttpAuthenticationEditor.tsx b/src-web/components/HttpAuthenticationEditor.tsx index b42fb50d..9e4d2c6b 100644 --- a/src-web/components/HttpAuthenticationEditor.tsx +++ b/src-web/components/HttpAuthenticationEditor.tsx @@ -32,7 +32,7 @@ export function HttpAuthenticationEditor({ model }: Props) { const authConfig = useHttpAuthenticationConfig( model.authenticationType, model.authentication, - model.id, + model, ); const handleChange = useCallback( diff --git a/src-web/components/WorkspaceSettingsDialog.tsx b/src-web/components/WorkspaceSettingsDialog.tsx index 6e96dc82..fc2ba75a 100644 --- a/src-web/components/WorkspaceSettingsDialog.tsx +++ b/src-web/components/WorkspaceSettingsDialog.tsx @@ -75,8 +75,8 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) { value: TAB_DATA, label: 'Directory Sync', }, - ...authTab, ...headersTab, + ...authTab, ]} > diff --git a/src-web/components/core/DismissibleBanner.tsx b/src-web/components/core/DismissibleBanner.tsx index 333b4bf0..e2dcf9e2 100644 --- a/src-web/components/core/DismissibleBanner.tsx +++ b/src-web/components/core/DismissibleBanner.tsx @@ -1,15 +1,18 @@ +import type { Color } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; import { useKeyValue } from '../../hooks/useKeyValue'; import type { BannerProps } from './Banner'; import { Banner } from './Banner'; import { Button } from './Button'; +import { HStack } from './Stacks'; export function DismissibleBanner({ children, className, id, + actions, ...props -}: BannerProps & { id: string }) { +}: BannerProps & { id: string; actions?: { label: string; onClick: () => void; color?: Color }[] }) { const { set: setDismissed, value: dismissed } = useKeyValue({ namespace: 'global', key: ['dismiss-banner', id], @@ -19,17 +22,34 @@ export function DismissibleBanner({ if (dismissed) return null; return ( - + {children} - + + {actions?.map((a, i) => ( + + ))} + + ); } diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index 1557c33c..90299981 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -519,7 +519,7 @@ const Menu = forwardRef {showTriangle && ( diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 7429a76e..b57a5d2e 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -7,7 +7,7 @@ import { emacs } from '@replit/codemirror-emacs'; import { vim } from '@replit/codemirror-vim'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models'; +import type { EditorKeymap } from '@yaakapp-internal/models'; import { settingsAtom } from '@yaakapp-internal/models'; import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins'; import { parseTemplate } from '@yaakapp-internal/templates'; @@ -28,10 +28,12 @@ import { useRef, } from 'react'; import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment'; +import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables'; import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables'; import { useRequestEditor } from '../../../hooks/useRequestEditor'; import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions'; import { showDialog } from '../../../lib/dialog'; +import { editEnvironment } from '../../../lib/editEnvironment'; import { tryFormatJson, tryFormatXml } from '../../../lib/formatters'; import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption'; import { TemplateFunctionDialog } from '../../TemplateFunctionDialog'; @@ -96,7 +98,7 @@ export interface EditorProps { const stateFields = { history: historyField, folds: foldState }; -const emptyVariables: EnvironmentVariable[] = []; +const emptyVariables: WrappedEnvironmentVariable[] = []; const emptyExtension: Extension = []; export const Editor = forwardRef(function Editor( @@ -306,24 +308,9 @@ export const Editor = forwardRef(function E ); const onClickVariable = useCallback( - async (_v: EnvironmentVariable, tagValue: string, startPos: number) => { - const initialTokens = parseTemplate(tagValue); - showDialog({ - size: 'dynamic', - id: 'template-variable', - title: 'Change Variable', - render: ({ hide }) => ( - { - cm.current?.view.dispatch({ - changes: [{ from: startPos, to: startPos + tagValue.length, insert }], - }); - }} - /> - ), - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => { + editEnvironment(v.environment); }, [], ); diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index df29933a..ded2f41e 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -35,10 +35,10 @@ import { rectangularSelection, } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; -import type { EnvironmentVariable } from '@yaakapp-internal/models'; import { graphql } from 'cm6-graphql'; import type { GraphQLSchema } from 'graphql'; import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId'; +import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables'; import { jotaiStore } from '../../../lib/jotai'; import { renderMarkdown } from '../../../lib/markdown'; import { pluralizeCount } from '../../../lib/pluralize'; @@ -110,8 +110,8 @@ export function getLanguageExtension({ graphQLSchema, }: { useTemplating: boolean; - environmentVariables: EnvironmentVariable[]; - onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void; + environmentVariables: WrappedEnvironmentVariable[]; + onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void; onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void; onClickPathParameter: (name: string) => void; completionOptions: TwigCompletionOption[]; diff --git a/src-web/components/core/Editor/twig/extension.ts b/src-web/components/core/Editor/twig/extension.ts index 8e743deb..45aeaf43 100644 --- a/src-web/components/core/Editor/twig/extension.ts +++ b/src-web/components/core/Editor/twig/extension.ts @@ -2,7 +2,7 @@ import type { LanguageSupport } from '@codemirror/language'; import { LRLanguage } from '@codemirror/language'; import type { Extension } from '@codemirror/state'; import { parseMixed } from '@lezer/common'; -import type { EnvironmentVariable } from '@yaakapp-internal/models'; +import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables'; import type { GenericCompletionConfig } from '../genericCompletion'; import { genericCompletion } from '../genericCompletion'; import { textLanguage } from '../text/extension'; @@ -21,10 +21,10 @@ export function twig({ extraExtensions, }: { base: LanguageSupport; - environmentVariables: EnvironmentVariable[]; + environmentVariables: WrappedEnvironmentVariable[]; completionOptions: TwigCompletionOption[]; autocomplete?: GenericCompletionConfig; - onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void; + onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void; onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void; onClickPathParameter: (name: string) => void; extraExtensions: Extension[]; @@ -33,9 +33,11 @@ export function twig({ const variableOptions: TwigCompletionOption[] = environmentVariables.map((v) => ({ - ...v, + name: v.variable.name, + value: v.variable.value, type: 'variable', - label: v.name, + label: v.variable.name, + description: `Inherited from ${v.source}`, onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos), })) ?? []; diff --git a/src-web/components/core/Editor/twig/templateTags.ts b/src-web/components/core/Editor/twig/templateTags.ts index 79d37dcf..c51291de 100644 --- a/src-web/components/core/Editor/twig/templateTags.ts +++ b/src-web/components/core/Editor/twig/templateTags.ts @@ -94,13 +94,16 @@ function templateTags( (o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)), ); if (option == null) { + const from = node.from; // Cache here so the reference doesn't change option = { invalid: true, type: 'variable', name: inner, value: null, label: inner, - onClick: () => onClickMissingVariable(name, rawTag, node.from), + onClick: () => { + onClickMissingVariable(name, rawTag, from); + }, }; } diff --git a/src-web/components/responseViewers/CsvViewer.tsx b/src-web/components/responseViewers/CsvViewer.tsx index cdbf2ba7..ca70b147 100644 --- a/src-web/components/responseViewers/CsvViewer.tsx +++ b/src-web/components/responseViewers/CsvViewer.tsx @@ -10,7 +10,7 @@ interface Props { } export function CsvViewer({ response, className }: Props) { - const body = useResponseBodyText(response); + const body = useResponseBodyText({ response, filter: null }); const parsed = useMemo(() => { if (body.data == null) return null; diff --git a/src-web/components/responseViewers/HTMLOrTextViewer.tsx b/src-web/components/responseViewers/HTMLOrTextViewer.tsx index 6b4768aa..58dd12bb 100644 --- a/src-web/components/responseViewers/HTMLOrTextViewer.tsx +++ b/src-web/components/responseViewers/HTMLOrTextViewer.tsx @@ -13,7 +13,7 @@ interface Props { } export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) { - const rawTextBody = useResponseBodyText({ responseId: response.id, filter: null }); + const rawTextBody = useResponseBodyText({ response, filter: null }); const contentType = getContentTypeFromHeaders(response.headers); const language = languageFromContentType(contentType, rawTextBody.data ?? ''); @@ -32,7 +32,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop text={rawTextBody.data} pretty={pretty} className={textViewerClassName} - responseId={response.id} + response={response} requestId={response.requestId} /> ); diff --git a/src-web/components/responseViewers/JsonViewer.tsx b/src-web/components/responseViewers/JsonViewer.tsx index 72c7e030..c81b1385 100644 --- a/src-web/components/responseViewers/JsonViewer.tsx +++ b/src-web/components/responseViewers/JsonViewer.tsx @@ -9,7 +9,7 @@ interface Props { } export function JsonViewer({ response, className }: Props) { - const rawBody = useResponseBodyText(response); + const rawBody = useResponseBodyText({ response, filter: null }); if (rawBody.isLoading || rawBody.data == null) return null; diff --git a/src-web/components/responseViewers/SvgViewer.tsx b/src-web/components/responseViewers/SvgViewer.tsx index fa599ba0..ee8e9b13 100644 --- a/src-web/components/responseViewers/SvgViewer.tsx +++ b/src-web/components/responseViewers/SvgViewer.tsx @@ -7,7 +7,7 @@ interface Props { } export function SvgViewer({ response }: Props) { - const rawTextBody = useResponseBodyText(response); + const rawTextBody = useResponseBodyText({ response, filter: null }); const [src, setSrc] = useState(null); useEffect(() => { diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index cf6c5264..d3ee59cb 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -1,3 +1,4 @@ +import type { HttpResponse } from '@yaakapp-internal/models'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; @@ -18,13 +19,13 @@ interface Props { className?: string; text: string; language: EditorProps['language']; - responseId: string; + response: HttpResponse; requestId: string; } const useFilterText = createGlobalState>({}); -export function TextViewer({ language, text, responseId, requestId, pretty, className }: Props) { +export function TextViewer({ language, text, response, requestId, pretty, className }: Props) { const [filterTextMap, setFilterTextMap] = useFilterText(); const filterText = filterTextMap[requestId] ?? null; const debouncedFilterText = useDebouncedValue(filterText); @@ -36,7 +37,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas ); const isSearching = filterText != null; - const filteredResponse = useResponseBodyText({ responseId, filter: debouncedFilterText ?? null }); + const filteredResponse = useResponseBodyText({ response, filter: debouncedFilterText ?? null }); const toggleSearch = useCallback(() => { if (isSearching) { @@ -69,7 +70,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas defaultValue={filterText} onKeyDown={(e) => e.key === 'Escape' && toggleSearch()} onChange={setFilterText} - stateKey={`filter.${responseId}`} + stateKey={`filter.${response.id}`} /> , ); @@ -96,7 +97,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas isSearching, language, requestId, - responseId, + response, setFilterText, toggleSearch, ]); diff --git a/src-web/components/responseViewers/WebPageViewer.tsx b/src-web/components/responseViewers/WebPageViewer.tsx index 5d0669d3..2b31cae9 100644 --- a/src-web/components/responseViewers/WebPageViewer.tsx +++ b/src-web/components/responseViewers/WebPageViewer.tsx @@ -8,7 +8,7 @@ interface Props { export function WebPageViewer({ response }: Props) { const { url } = response; - const body = useResponseBodyText(response).data ?? ''; + const body = useResponseBodyText({ response, filter: null }).data ?? ''; const contentForIframe: string | undefined = useMemo(() => { if (body.includes('')) { diff --git a/src-web/components/sidebar/SidebarItemContextMenu.tsx b/src-web/components/sidebar/SidebarItemContextMenu.tsx index 9a2498c0..20944d0f 100644 --- a/src-web/components/sidebar/SidebarItemContextMenu.tsx +++ b/src-web/components/sidebar/SidebarItemContextMenu.tsx @@ -37,11 +37,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) { const items = useMemo((): DropdownItem[] => { if (child.model === 'folder') { return [ - { - label: 'Send All', - leftSlot: , - onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)), - }, { label: 'Settings', leftSlot: , @@ -52,6 +47,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) { leftSlot: , onSelect: () => duplicateModelById(child.model, child.id), }, + { + label: 'Send All', + leftSlot: , + onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)), + }, { label: 'Delete', color: 'danger', diff --git a/src-web/hooks/useActiveEnvironmentVariables.ts b/src-web/hooks/useActiveEnvironmentVariables.ts index a13ca905..05d5e7aa 100644 --- a/src-web/hooks/useActiveEnvironmentVariables.ts +++ b/src-web/hooks/useActiveEnvironmentVariables.ts @@ -4,5 +4,5 @@ import { useEnvironmentVariables } from './useEnvironmentVariables'; export function useActiveEnvironmentVariables() { const activeEnvironment = useAtomValue(activeEnvironmentAtom); - return useEnvironmentVariables(activeEnvironment?.id ?? null); + return useEnvironmentVariables(activeEnvironment?.id ?? null).map((v) => v.variable); } diff --git a/src-web/hooks/useCopyHttpResponse.ts b/src-web/hooks/useCopyHttpResponse.ts index 39098176..60be4f8e 100644 --- a/src-web/hooks/useCopyHttpResponse.ts +++ b/src-web/hooks/useCopyHttpResponse.ts @@ -7,7 +7,7 @@ export function useCopyHttpResponse(response: HttpResponse) { return useFastMutation({ mutationKey: ['copy_http_response', response.id], async mutationFn() { - const body = await getResponseBodyText(response); + const body = await getResponseBodyText({ responseId: response.id, filter: null }); copyToClipboard(body); }, }); diff --git a/src-web/hooks/useEnvironmentValueVisibility.ts b/src-web/hooks/useEnvironmentValueVisibility.ts new file mode 100644 index 00000000..f4285b9c --- /dev/null +++ b/src-web/hooks/useEnvironmentValueVisibility.ts @@ -0,0 +1,10 @@ +import type { Environment } from '@yaakapp-internal/models'; +import { useKeyValue } from './useKeyValue'; + +export function useEnvironmentValueVisibility(environment: Environment) { + return useKeyValue({ + namespace: 'global', + key: ['environmentValueVisibility', environment.workspaceId], + fallback: false, + }); +} diff --git a/src-web/hooks/useEnvironmentVariables.ts b/src-web/hooks/useEnvironmentVariables.ts index 424176eb..5da32137 100644 --- a/src-web/hooks/useEnvironmentVariables.ts +++ b/src-web/hooks/useEnvironmentVariables.ts @@ -1,25 +1,52 @@ -import type { EnvironmentVariable } from '@yaakapp-internal/models'; -import { environmentsAtom } from '@yaakapp-internal/models'; -import { useAtomValue } from 'jotai'; +import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models'; +import { foldersAtom } from '@yaakapp-internal/models'; import { useMemo } from 'react'; +import { jotaiStore } from '../lib/jotai'; +import { useActiveRequest } from './useActiveRequest'; import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown'; +import { useParentFolders } from './useParentFolders'; export function useEnvironmentVariables(environmentId: string | null) { - const { baseEnvironment } = useEnvironmentsBreakdown(); - const activeEnvironment = - useAtomValue(environmentsAtom).find((e) => e.id === environmentId) ?? null; + const { baseEnvironment, folderEnvironments, subEnvironments } = useEnvironmentsBreakdown(); + const activeEnvironment = subEnvironments.find((e) => e.id === environmentId) ?? null; + const activeRequest = useActiveRequest(); + const parentFolders = useParentFolders(activeRequest); + return useMemo(() => { - const varMap: Record = {}; + const varMap: Record = {}; + const folderVariables = parentFolders.flatMap((f) => + wrapVariables(folderEnvironments.find((fe) => fe.parentId === f.id) ?? null), + ); + const allVariables = [ - ...(baseEnvironment?.variables ?? []), - ...(activeEnvironment?.variables ?? []), + ...folderVariables, + ...wrapVariables(activeEnvironment), + ...wrapVariables(baseEnvironment), ]; for (const v of allVariables) { - if (!v.enabled || !v.name) continue; - varMap[v.name] = v; + if (!v.variable.enabled || !v.variable.name || v.variable.name in varMap) { + continue; + } + varMap[v.variable.name] = v; } return Object.values(varMap); - }, [activeEnvironment, baseEnvironment]); + }, [activeEnvironment, baseEnvironment, folderEnvironments, parentFolders]); +} + +export interface WrappedEnvironmentVariable { + variable: EnvironmentVariable; + environment: Environment; + source: string; +} + +function wrapVariables(e: Environment | null): WrappedEnvironmentVariable[] { + if (e == null) return []; + const folders = jotaiStore.get(foldersAtom); + return e.variables.map((v) => { + const folder = e.parentModel === 'folder' ? folders.find((f) => f.id === e.parentId) : null; + const source = folder?.name ?? e.name; + return { variable: v, environment: e, source }; + }); } diff --git a/src-web/hooks/useEnvironmentsBreakdown.ts b/src-web/hooks/useEnvironmentsBreakdown.ts index dea73621..ce73d3ca 100644 --- a/src-web/hooks/useEnvironmentsBreakdown.ts +++ b/src-web/hooks/useEnvironmentsBreakdown.ts @@ -5,12 +5,15 @@ import { useMemo } from 'react'; export function useEnvironmentsBreakdown() { const allEnvironments = useAtomValue(environmentsAtom); return useMemo(() => { - const baseEnvironments = allEnvironments.filter((e) => e.base) ?? []; - const subEnvironments = allEnvironments.filter((e) => !e.base) ?? []; + const baseEnvironments = allEnvironments.filter((e) => e.parentId == null) ?? []; + const subEnvironments = + allEnvironments.filter((e) => e.parentModel === 'environment' && e.parentId != null) ?? []; + const folderEnvironments = + allEnvironments.filter((e) => e.parentModel === 'folder' && e.parentId != null) ?? []; const baseEnvironment = baseEnvironments[0] ?? null; const otherBaseEnvironments = baseEnvironments.filter((e) => e.id !== baseEnvironment?.id) ?? []; - return { allEnvironments, baseEnvironment, subEnvironments, otherBaseEnvironments }; + return { allEnvironments, baseEnvironment, subEnvironments, folderEnvironments, otherBaseEnvironments }; }, [allEnvironments]); } diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 148fc869..f99f98de 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -40,7 +40,7 @@ const hotkeys: Record = { 'request_switcher.prev': ['Control+Tab'], 'request_switcher.toggle': ['CmdCtrl+p'], 'settings.show': ['CmdCtrl+,'], - 'sidebar.delete_selected_item': ['Backspace'], + 'sidebar.delete_selected_item': ['Delete'], 'sidebar.focus': ['CmdCtrl+b'], 'url_bar.focus': ['CmdCtrl+l'], 'workspace_settings.show': ['CmdCtrl+;'], @@ -98,7 +98,7 @@ export function useHotKey( // Don't add key if not holding modifier const isValidKeymapKey = - e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace'; + e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete'; if (!isValidKeymapKey) { return; } diff --git a/src-web/hooks/useHttpAuthenticationConfig.ts b/src-web/hooks/useHttpAuthenticationConfig.ts index 4b89fbb0..a18852c3 100644 --- a/src-web/hooks/useHttpAuthenticationConfig.ts +++ b/src-web/hooks/useHttpAuthenticationConfig.ts @@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace'; export function useHttpAuthenticationConfig( authName: string | null, values: Record, - requestId: string, + request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, ) { const workspaceId = useAtomValue(activeWorkspaceIdAtom); const environmentId = useAtomValue(activeEnvironmentIdAtom); @@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig( return useQuery({ queryKey: [ 'http_authentication_config', - requestId, + request, authName, values, responseKey, @@ -53,8 +53,7 @@ export function useHttpAuthenticationConfig( { authName, values, - requestId, - workspaceId, + request, environmentId, }, ); @@ -63,17 +62,16 @@ export function useHttpAuthenticationConfig( ...config, actions: config.actions?.map((a, i) => ({ ...a, - call: async ({ - id: modelId, - }: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace) => { + call: async ( + model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, + ) => { await invokeCmd('cmd_call_http_authentication_action', { pluginRefId: config.pluginRefId, actionIndex: i, authName, values, - modelId, + model, environmentId, - workspaceId, }); // Ensure the config is refreshed after the action is done diff --git a/src-web/hooks/useIntrospectGraphQL.ts b/src-web/hooks/useIntrospectGraphQL.ts index 0d427543..e5da81e7 100644 --- a/src-web/hooks/useIntrospectGraphQL.ts +++ b/src-web/hooks/useIntrospectGraphQL.ts @@ -66,7 +66,7 @@ export function useIntrospectGraphQL( return setError(response.error); } - const bodyText = await getResponseBodyText(response); + const bodyText = await getResponseBodyText({ responseId: response.id, filter: null }); if (response.status < 200 || response.status >= 300) { return setError( `Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`, diff --git a/src-web/hooks/useIsEncryptionEnabled.ts b/src-web/hooks/useIsEncryptionEnabled.ts index 6e15851e..b4e10a68 100644 --- a/src-web/hooks/useIsEncryptionEnabled.ts +++ b/src-web/hooks/useIsEncryptionEnabled.ts @@ -1,7 +1,7 @@ -import {useAtomValue} from "jotai/index"; -import {activeWorkspaceMetaAtom} from "./useActiveWorkspace"; +import { useAtomValue } from 'jotai'; +import { activeWorkspaceMetaAtom } from './useActiveWorkspace'; export function useIsEncryptionEnabled() { - const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom); - return workspaceMeta?.encryptionKey != null; + const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom); + return workspaceMeta?.encryptionKey != null; } diff --git a/src-web/hooks/useParentFolders.ts b/src-web/hooks/useParentFolders.ts new file mode 100644 index 00000000..4a7dc07c --- /dev/null +++ b/src-web/hooks/useParentFolders.ts @@ -0,0 +1,24 @@ +import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; +import { foldersAtom } from '@yaakapp-internal/models'; +import { useAtomValue } from 'jotai'; +import { useMemo } from 'react'; + +export function useParentFolders(m: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null) { + const folders = useAtomValue(foldersAtom); + + return useMemo(() => getParentFolders(folders, m), [folders, m]); +} + +function getParentFolders( + folders: Folder[], + currentModel: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null, +): Folder[] { + if (currentModel == null) return []; + + const folder = currentModel.folderId ? folders.find((f) => f.id === currentModel.folderId) : null; + if (folder == null) { + return []; + } + + return [folder, ...getParentFolders(folders, folder)]; +} diff --git a/src-web/hooks/useResponseBodyText.ts b/src-web/hooks/useResponseBodyText.ts index 58db5b59..1fe1ba20 100644 --- a/src-web/hooks/useResponseBodyText.ts +++ b/src-web/hooks/useResponseBodyText.ts @@ -1,15 +1,22 @@ import { useQuery } from '@tanstack/react-query'; +import type { HttpResponse } from '@yaakapp-internal/models'; import { getResponseBodyText } from '../lib/responseBody'; export function useResponseBodyText({ - responseId, + response, filter, }: { - responseId: string; + response: HttpResponse; filter: string | null; }) { return useQuery({ - queryKey: ['response_body_text', responseId, filter ?? ''], - queryFn: () => getResponseBodyText({ responseId, filter }), + queryKey: [ + 'response_body_text', + response.id, + response.updatedAt, + response.contentLength, + filter ?? '', + ], + queryFn: () => getResponseBodyText({ responseId: response.id, filter }), }); } diff --git a/src-web/lib/editEnvironment.tsx b/src-web/lib/editEnvironment.tsx new file mode 100644 index 00000000..4be717cb --- /dev/null +++ b/src-web/lib/editEnvironment.tsx @@ -0,0 +1,18 @@ +import type { Environment } from '@yaakapp-internal/models'; +import { openFolderSettings } from '../commands/openFolderSettings'; +import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog'; +import { toggleDialog } from './dialog'; + +export function editEnvironment(environment: Environment | null) { + if (environment?.parentModel === 'folder' && environment.parentId != null) { + openFolderSettings(environment.parentId, 'variables'); + } else { + toggleDialog({ + id: 'environment-editor', + noPadding: true, + size: 'lg', + className: 'h-[80vh]', + render: () => , + }); + } +} diff --git a/src-web/lib/model_util.ts b/src-web/lib/model_util.ts index b0cd59d8..66b9c662 100644 --- a/src-web/lib/model_util.ts +++ b/src-web/lib/model_util.ts @@ -1,4 +1,4 @@ -import type { AnyModel, Cookie, HttpResponseHeader } from '@yaakapp-internal/models'; +import type { AnyModel, Cookie, Environment, HttpResponseHeader } from '@yaakapp-internal/models'; import { getMimeTypeFromContentType } from './contentType'; export const BODY_TYPE_NONE = null; @@ -47,3 +47,7 @@ export function getCharsetFromContentType(headers: HttpResponseHeader[]): string const mimeType = getMimeTypeFromContentType(contentType); return mimeType.parameters.get('charset') ?? null; } + +export function isBaseEnvironment(environment: Environment): boolean { + return environment.parentId == null; +} diff --git a/src-web/package.json b/src-web/package.json index dd5142f5..ec9a91eb 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev --force", "build": "vite build", - "lint": "eslint . --ext .ts,.tsx" + "lint": "tsc --noEmit && eslint . --ext .ts,.tsx" }, "dependencies": { "@codemirror/commands": "^6.8.1",