Compare commits

...

6 Commits

Author SHA1 Message Date
Gregory Schier
e044dcae3e Add back Rose Pine themes 2025-07-25 09:39:37 -07:00
Gregory Schier
b5b7b1638d Use physical key codes for zoom hotkeys 2025-07-24 09:15:57 -07:00
Gregory Schier
9d6ac8a107 Add template.desktop to make icon work until we can upgrade Tauri CLI 2025-07-24 08:44:55 -07:00
Gregory Schier
6440df492e Generate icons 2025-07-24 08:07:16 -07:00
Gregory Schier
2cdd97cabb Fix header spacing for window controls
https://feedback.yaak.app/p/app-bar-icons-not-aligned-correctly-when-fullscreen
2025-07-24 07:57:38 -07:00
Gregory Schier
20681e5be3 Scoped OAuth 2 tokens 2025-07-23 22:03:03 -07:00
26 changed files with 381 additions and 98 deletions

View File

@@ -57,6 +57,9 @@
"migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"icons": "run-p icons:*",
"icons:dev": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release",
"icons:release": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",

View File

@@ -1,19 +0,0 @@
import type { Context } from '@yaakapp/api';
import type { AccessToken } from './store';
import { getToken } from './store';
export async function getAccessTokenIfNotExpired(
ctx: Context,
contextId: string,
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
if (token == null || isTokenExpired(token)) {
return null;
}
return token;
}
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}

View File

@@ -1,12 +1,12 @@
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { isTokenExpired } from './getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse } from './store';
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
import { deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './util';
export async function getOrRefreshAccessToken(
ctx: Context,
contextId: string,
tokenArgs: TokenStoreArgs,
{
scope,
accessTokenUrl,
@@ -23,7 +23,7 @@ export async function getOrRefreshAccessToken(
forceRefresh?: boolean;
},
): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
const token = await getToken(ctx, tokenArgs);
if (token == null) {
return null;
}
@@ -75,7 +75,7 @@ export async function getOrRefreshAccessToken(
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
// and returning null;
console.log('[oauth2] Unauthorized refresh_token request');
await deleteToken(ctx, contextId);
await deleteToken(ctx, tokenArgs);
return null;
}
@@ -108,5 +108,5 @@ export async function getOrRefreshAccessToken(
refresh_token: response.refresh_token ?? token.response.refresh_token,
};
return storeToken(ctx, contextId, newResponse);
return storeToken(ctx, tokenArgs, newResponse);
}

View File

@@ -2,7 +2,7 @@ import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken } from '../store';
import type { AccessToken, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store';
export const PKCE_SHA256 = 'S256';
@@ -41,7 +41,14 @@ export async function getAuthorizationCode(
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: authorizationUrlRaw,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl,
scope,
clientId,
@@ -128,7 +135,7 @@ export async function getAuthorizationCode(
],
});
return storeToken(ctx, contextId, response, tokenName);
return storeToken(ctx, tokenArgs, response, tokenName);
}
export function genPkceCodeVerifier() {

View File

@@ -1,7 +1,8 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import type { TokenStoreArgs } from '../store';
import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getClientCredentials(
ctx: Context,
@@ -22,7 +23,13 @@ export async function getClientCredentials(
credentialsInBody: boolean;
},
) {
const token = await getToken(ctx, contextId);
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getToken(ctx, tokenArgs);
if (token && !isTokenExpired(token)) {
return token;
}
@@ -38,5 +45,5 @@ export async function getClientCredentials(
params: [],
});
return storeToken(ctx, contextId, response);
return storeToken(ctx, tokenArgs, response);
}

View File

@@ -1,7 +1,7 @@
import type { Context } from '@yaakapp/api';
import { isTokenExpired } from '../getAccessTokenIfNotExpired';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import type { AccessToken, AccessTokenRawResponse } from '../store';
import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getImplicit(
ctx: Context,
@@ -26,7 +26,13 @@ export async function getImplicit(
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
const token = await getToken(ctx, contextId);
const tokenArgs = {
contextId,
clientId,
accessTokenUrl: null,
authorizationUrl: authorizationUrlRaw,
};
const token = await getToken(ctx, tokenArgs);
if (token != null && !isTokenExpired(token)) {
return token;
}
@@ -82,7 +88,7 @@ export async function getImplicit(
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try {
resolve(storeToken(ctx, contextId, response));
resolve(storeToken(ctx, tokenArgs, response));
} catch (err) {
reject(err);
}

View File

@@ -1,7 +1,7 @@
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken} from '../store';
import type { AccessToken, TokenStoreArgs } from '../store';
import { storeToken } from '../store';
export async function getPassword(
@@ -27,7 +27,13 @@ export async function getPassword(
credentialsInBody: boolean;
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl,
scope,
clientId,
@@ -52,5 +58,5 @@ export async function getPassword(
],
});
return storeToken(ctx, contextId, response);
return storeToken(ctx, tokenArgs, response);
}

View File

@@ -15,7 +15,7 @@ import {
import { getClientCredentials } from './grants/clientCredentials';
import { getImplicit } from './grants/implicit';
import { getPassword } from './grants/password';
import type { AccessToken } from './store';
import type { AccessToken, TokenStoreArgs } from './store';
import { deleteToken, getToken, resetDataDirKey } from './store';
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
@@ -83,8 +83,14 @@ export const plugin: PluginDefinition = {
actions: [
{
label: 'Copy Current Token',
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
async onSelect(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) {
await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
} else {
@@ -99,8 +105,14 @@ export const plugin: PluginDefinition = {
},
{
label: 'Delete Token',
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
async onSelect(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
if (await deleteToken(ctx, tokenArgs)) {
await ctx.toast.show({ message: 'Token deleted', color: 'success' });
} else {
await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
@@ -281,8 +293,14 @@ export const plugin: PluginDefinition = {
{
type: 'accordion',
label: 'Access Token Response',
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
async dynamic(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) {
return { hidden: true };
}
@@ -316,9 +334,10 @@ export const plugin: PluginDefinition = {
accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
authorizationUrl:
authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'),
redirectUri: stringArgOrNull(values, 'redirectUri'),

View File

@@ -1,8 +1,9 @@
import type { Context } from '@yaakapp/api';
import { createHash } from 'node:crypto';
export async function storeToken(
ctx: Context,
contextId: string,
args: TokenStoreArgs,
response: AccessTokenRawResponse,
tokenName: 'access_token' | 'id_token' = 'access_token',
) {
@@ -15,16 +16,16 @@ export async function storeToken(
response,
expiresAt,
};
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token);
await ctx.store.set<AccessToken>(tokenStoreKey(args), token);
return token;
}
export async function getToken(ctx: Context, contextId: string) {
return ctx.store.get<AccessToken>(tokenStoreKey(contextId));
export async function getToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.get<AccessToken>(tokenStoreKey(args));
}
export async function deleteToken(ctx: Context, contextId: string) {
return ctx.store.delete(tokenStoreKey(contextId));
export async function deleteToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.delete(tokenStoreKey(args));
}
export async function resetDataDirKey(ctx: Context, contextId: string) {
@@ -37,8 +38,25 @@ export async function getDataDirKey(ctx: Context, contextId: string) {
return `${contextId}::${key}`;
}
function tokenStoreKey(contextId: string) {
return ['token', contextId].join('::');
export interface TokenStoreArgs {
contextId: string;
clientId: string;
accessTokenUrl: string | null;
authorizationUrl: string | null;
}
/**
* Generate a store key to use based on some arguments. The arguments will be normalized a bit to
* account for slight variations (like domains with and without a protocol scheme).
*/
function tokenStoreKey(args: TokenStoreArgs) {
const hash = createHash('md5');
if (args.contextId) hash.update(args.contextId.trim());
if (args.clientId) hash.update(args.clientId.trim());
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
const key = hash.digest('hex');
return ['token', key].join('::');
}
function dataDirStoreKey(contextId: string) {

View File

@@ -0,0 +1,5 @@
import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}

View File

@@ -595,5 +595,112 @@ export const plugin: PluginDefinition = {
danger: 'hsl(343,81%,75%)',
},
},
{
id: 'rose-pine',
label: 'Rosé Pine',
dark: true,
base: {
surface: 'hsl(249,22%,12%)',
text: 'hsl(245,50%,91%)',
textSubtle: 'hsl(248,15%,61%)',
textSubtlest: 'hsl(249,12%,47%)',
primary: 'hsl(267,57%,78%)',
secondary: 'hsl(249,12%,47%)',
info: 'hsl(199,49%,60%)',
success: 'hsl(180,43%,73%)',
notice: 'hsl(35,88%,72%)',
warning: 'hsl(1,74%,79%)',
danger: 'hsl(343,76%,68%)',
},
components: {
responsePane: {
surface: 'hsl(247,23%,15%)',
},
sidebar: {
surface: 'hsl(247,23%,15%)',
},
menu: {
surface: 'hsl(248,21%,26%)',
textSubtle: 'hsl(248,15%,66%)',
textSubtlest: 'hsl(249,12%,52%)',
border: 'hsl(248,21%,35%)',
borderSubtle: 'hsl(248,21%,33%)',
},
},
},
{
id: 'rose-pine-moon',
label: 'Rosé Pine Moon',
dark: true,
base: {
surface: 'hsl(246,24%,17%)',
text: 'hsl(245,50%,91%)',
textSubtle: 'hsl(248,15%,61%)',
textSubtlest: 'hsl(249,12%,47%)',
primary: 'hsl(267,57%,78%)',
secondary: 'hsl(248,15%,61%)',
info: 'hsl(197,48%,60%)',
success: 'hsl(197,48%,60%)',
notice: 'hsl(35,88%,72%)',
warning: 'hsl(2,66%,75%)',
danger: 'hsl(343,76%,68%)',
},
components: {
responsePane: {
surface: 'hsl(247,24%,20%)',
},
sidebar: {
surface: 'hsl(247,24%,20%)',
},
menu: {
surface: 'hsl(248,21%,26%)',
textSubtle: 'hsl(248,15%,61%)',
textSubtlest: 'hsl(249,12%,55%)',
border: 'hsl(248,21%,35%)',
borderSubtle: 'hsl(248,21%,31%)',
},
},
},
{
id: 'rose-pine-dawn',
label: 'Rosé Pine Dawn',
dark: false,
base: {
surface: 'hsl(32,57%,95%)',
border: 'hsl(10,9%,86%)',
surfaceHighlight: 'hsl(25,35%,93%)',
text: 'hsl(248,19%,40%)',
textSubtle: 'hsl(248,12%,52%)',
textSubtlest: 'hsl(257,9%,61%)',
primary: 'hsl(271,27%,56%)',
secondary: 'hsl(249,12%,47%)',
info: 'hsl(197,52%,36%)',
success: 'hsl(188,31%,45%)',
notice: 'hsl(34,64%,49%)',
warning: 'hsl(2,47%,64%)',
danger: 'hsl(343,35%,55%)',
},
components: {
responsePane: {
border: 'hsl(20,12%,90%)',
},
sidebar: {
border: 'hsl(20,12%,90%)',
},
appHeader: {
border: 'hsl(20,12%,90%)',
},
input: {
border: 'hsl(10,9%,86%)',
},
dialog: {
border: 'hsl(20,12%,90%)',
},
menu: {
surface: 'hsl(28,40%,92%)',
border: 'hsl(10,9%,86%)',
},
},
},
],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -55,11 +55,6 @@ pub async fn send_http_request<R: Runtime>(
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let update_source = UpdateSource::from_window(window);
let (resolved_request, auth_context_id) = match resolve_http_request(window, unrendered_request)
@@ -75,6 +70,12 @@ pub async fn send_http_request<R: Runtime>(
}
};
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let request =
match render_http_request(&resolved_request, &base_environment, environment.as_ref(), &cb)
.await

View File

@@ -35,7 +35,13 @@ use yaak_models::models::{
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest};
use yaak_plugins::events::{
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -818,9 +824,29 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
environment_id: Option<&str>,
workspace_id: &str,
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
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,
};
Ok(plugin_manager
.get_http_authentication_config(&window, auth_name, values, request_id)
.get_http_authentication_config(
&window,
&base_environment,
environment.as_ref(),
auth_name,
values,
request_id,
)
.await?)
}
@@ -872,9 +898,30 @@ async fn cmd_call_http_authentication_action<R: Runtime>(
action_index: i32,
values: HashMap<String, JsonPrimitive>,
model_id: &str,
workspace_id: &str,
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,
};
Ok(plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, model_id)
.call_http_authentication_action(
&window,
&base_environment,
environment.as_ref(),
auth_name,
action_index,
values,
model_id,
)
.await?)
}
@@ -1238,7 +1285,10 @@ pub fn run() {
let _ = app_handle.emit(
"show_toast",
ShowToastRequest {
message: format!("Error handling deep link: {}", e.to_string()),
message: format!(
"Error handling deep link: {}",
e.to_string()
),
color: Some(Color::Danger),
icon: None,
},

View File

@@ -1,3 +1,13 @@
{
"productName": "yaak"
"productName": "yaak",
"bundle": {
"linux": {
"deb": {
"desktopTemplate": "./template.desktop"
},
"rpm": {
"desktopTemplate": "./template.desktop"
}
}
}
}

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Categories={{categories}}
Comment={{comment}}
Exec={{exec}}
Icon={{icon}}
Name={{name}}
StartupWMClass={{exec}}
Terminal=false
Type=Application

View File

@@ -18,7 +18,9 @@ use crate::native_template_functions::template_function_secure;
use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle;
use crate::server_ws::PluginRuntimeServerWebsocket;
use crate::template_callback::PluginTemplateCallback;
use log::{error, info, warn};
use serde_json::json;
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
@@ -30,10 +32,13 @@ use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::{Mutex, mpsc};
use tokio::time::{Instant, timeout};
use yaak_models::models::Environment;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::render::make_vars_hashmap;
use yaak_models::util::generate_id;
use yaak_templates::error::Error::RenderError;
use yaak_templates::error::Result as TemplateResult;
use yaak_templates::render_json_value_raw;
#[derive(Clone)]
pub struct PluginManager {
@@ -569,6 +574,8 @@ impl PluginManager {
pub async fn get_http_authentication_config<R: Runtime>(
&self,
window: &WebviewWindow<R>,
base_environment: &Environment,
environment: Option<&Environment>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
@@ -579,13 +586,23 @@ 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 cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(&window),
RenderPurpose::Preview,
);
let rendered_values = render_json_value_raw(json!(values), vars, &cb).await?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&PluginWindowContext::new(window),
&plugin,
&InternalEventPayload::GetHttpAuthenticationConfigRequest(
GetHttpAuthenticationConfigRequest { values, context_id },
GetHttpAuthenticationConfigRequest {
values: serde_json::from_value(rendered_values)?,
context_id,
},
),
)
.await?;
@@ -602,11 +619,24 @@ impl PluginManager {
pub async fn call_http_authentication_action<R: Runtime>(
&self,
window: &WebviewWindow<R>,
base_environment: &Environment,
environment: Option<&Environment>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
model_id: &str,
) -> Result<()> {
let vars = &make_vars_hashmap(&base_environment, environment);
let rendered_values = render_json_value_raw(
json!(values),
vars,
&PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(&window),
RenderPurpose::Preview,
),
)
.await?;
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
.iter()
@@ -621,7 +651,10 @@ impl PluginManager {
CallHttpAuthenticationActionRequest {
index: action_index,
plugin_ref_id: plugin.clone().ref_id,
args: CallHttpAuthenticationActionArgs { context_id, values },
args: CallHttpAuthenticationActionArgs {
context_id,
values: serde_json::from_value(rendered_values)?,
},
},
),
)

View File

@@ -182,23 +182,24 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
);
case 'accordion':
return (
<DetailsBanner
key={i}
summary={input.label}
className={classNames(disabled && 'opacity-disabled')}
>
<div className="mb-3 px-3">
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables}
/>
</div>
</DetailsBanner>
<div key={i}>
<DetailsBanner
summary={input.label}
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
>
<div className="mb-3 px-3">
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
autocompleteFunctions={autocompleteFunctions || false}
autocompleteVariables={autocompleteVariables}
/>
</div>
</DetailsBanner>
</div>
);
case 'banner':
return (
@@ -309,6 +310,7 @@ function EditorArg({
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
disabled={arg.disabled}
language={arg.language}
readOnly={arg.readOnly}
onChange={onChange}
heightMode="auto"
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
@@ -329,9 +331,9 @@ function EditorArg({
showDialog({
id: 'id',
size: 'full',
title: 'Edit Value',
title: arg.readOnly ? 'View Value' : 'Edit Value',
className: '!max-w-[50rem] !max-h-[60rem]',
description: (
description: arg.label && (
<Label
htmlFor={id}
required={!arg.optional}
@@ -355,6 +357,7 @@ function EditorArg({
}
disabled={arg.disabled}
language={arg.language}
readOnly={arg.readOnly}
onChange={onChange}
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
placeholder={arg.placeholder ?? undefined}

View File

@@ -1,9 +1,10 @@
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import React, { useMemo } from 'react';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
@@ -23,7 +24,7 @@ export function HeaderSize({
children,
}: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const stoplightsVisible = useStoplightsVisible();
const isFullscreen = useIsFullscreen();
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
@@ -31,20 +32,22 @@ export function HeaderSize({
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
// Add large padding for window controls
if (stoplightsVisible && !ignoreControlsSpacing) {
s.paddingLeft = 72 / settings.interfaceScale;
} else if (!stoplightsVisible && !ignoreControlsSpacing && !settings.hideWindowControls) {
if (type() === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
}
} else if (!ignoreControlsSpacing && !settings.hideWindowControls) {
s.paddingRight = WINDOW_CONTROLS_WIDTH;
}
return s;
}, [
ignoreControlsSpacing,
isFullscreen,
settings.hideWindowControls,
settings.interfaceScale,
size,
stoplightsVisible,
style,
]);

View File

@@ -26,8 +26,8 @@ export type HotkeyAction =
| 'workspace_settings.show';
const hotkeys: Record<HotkeyAction, string[]> = {
'app.zoom_in': ['CmdCtrl+='],
'app.zoom_out': ['CmdCtrl+-'],
'app.zoom_in': ['CmdCtrl+Equal'],
'app.zoom_out': ['CmdCtrl+Minus'],
'app.zoom_reset': ['CmdCtrl+0'],
'command_palette.toggle': ['CmdCtrl+k'],
'environmentEditor.toggle': ['CmdCtrl+Shift+E', 'CmdCtrl+Shift+e'],
@@ -67,6 +67,8 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'workspace_settings.show': 'Open Workspace Settings',
};
const layoutInsensitiveKeys = ['Equal', 'Minus', 'BracketLeft', 'BracketRight', 'Backquote'];
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
interface Options {
@@ -106,7 +108,8 @@ export function useHotKey(
return;
}
currentKeys.current.add(e.key);
const keyToAdd = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
currentKeys.current.add(keyToAdd);
const currentKeysWithModifiers = new Set(currentKeys.current);
if (e.altKey) currentKeysWithModifiers.add('Alt');
@@ -150,7 +153,9 @@ export function useHotKey(
if (options.enable === false) {
return;
}
currentKeys.current.delete(e.key);
const keyToRemove = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
currentKeys.current.delete(keyToRemove);
// Clear all keys if no longer holding modifier
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;

View File

@@ -12,12 +12,16 @@ import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
requestId: string,
) {
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const environmentId = useAtomValue(activeEnvironmentIdAtom);
const responses = useAtomValue(httpResponsesAtom);
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
@@ -38,6 +42,8 @@ export function useHttpAuthenticationConfig(
values,
responseKey,
forceRefreshCounter,
workspaceId,
environmentId,
],
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => {
@@ -48,6 +54,8 @@ export function useHttpAuthenticationConfig(
authName,
values,
requestId,
workspaceId,
environmentId,
},
);
@@ -64,6 +72,8 @@ export function useHttpAuthenticationConfig(
authName,
values,
modelId,
environmentId,
workspaceId,
});
// Ensure the config is refreshed after the action is done