mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 14:06:49 +01:00
Compare commits
8 Commits
v2025.2.0-
...
v2025.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
041298b3f8 | ||
|
|
b400940f0e | ||
|
|
2e144f064d | ||
|
|
d8b1cadae6 | ||
|
|
c2f9760d08 | ||
|
|
a4c600cb48 | ||
|
|
bc3a5e3e58 | ||
|
|
4c3a02ac53 |
@@ -8,6 +8,7 @@ use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
use yaak_license::{LicenseCheckStatus, check_license};
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
@@ -70,6 +71,13 @@ impl YaakNotifier {
|
||||
|
||||
self.last_check = SystemTime::now();
|
||||
|
||||
let license_check = match check_license(window).await? {
|
||||
LicenseCheckStatus::PersonalUse { .. } => "personal".to_string(),
|
||||
LicenseCheckStatus::CommercialUse => "commercial".to_string(),
|
||||
LicenseCheckStatus::InvalidLicense => "invalid_license".to_string(),
|
||||
LicenseCheckStatus::Trialing { .. } => "trialing".to_string(),
|
||||
};
|
||||
let settings = window.db().get_settings();
|
||||
let num_launches = get_num_launches(app_handle).await;
|
||||
let info = app_handle.package_info().clone();
|
||||
let req = reqwest::Client::default()
|
||||
@@ -77,6 +85,8 @@ impl YaakNotifier {
|
||||
.query(&[
|
||||
("version", info.version.to_string().as_str()),
|
||||
("launches", num_launches.to_string().as_str()),
|
||||
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()),
|
||||
("license", &license_check),
|
||||
("platform", get_os()),
|
||||
]);
|
||||
let resp = req.send().await?;
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
use crate::error::Result;
|
||||
use crate::{
|
||||
activate_license, check_license, deactivate_license, ActivateLicenseRequestPayload,
|
||||
CheckActivationRequestPayload, DeactivateLicenseRequestPayload, LicenseCheckStatus,
|
||||
};
|
||||
use crate::{LicenseCheckStatus, activate_license, check_license, deactivate_license};
|
||||
use log::{debug, info};
|
||||
use std::string::ToString;
|
||||
use tauri::{command, Manager, Runtime, WebviewWindow};
|
||||
use tauri::{Runtime, WebviewWindow, command};
|
||||
|
||||
#[command]
|
||||
pub async fn check<R: Runtime>(window: WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||
debug!("Checking license");
|
||||
check_license(
|
||||
&window,
|
||||
CheckActivationRequestPayload {
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
check_license(&window).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -> Result<()> {
|
||||
info!("Activating license {}", license_key);
|
||||
activate_license(
|
||||
&window,
|
||||
ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
activate_license(&window, license_key).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn deactivate<R: Runtime>(window: WebviewWindow<R>) -> Result<()> {
|
||||
info!("Deactivating activation");
|
||||
deactivate_license(
|
||||
&window,
|
||||
DeactivateLicenseRequestPayload {
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
deactivate_license(&window).await
|
||||
}
|
||||
|
||||
@@ -16,3 +16,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.invoke_handler(generate_handler![check, activate, deactivate])
|
||||
.build()
|
||||
}
|
||||
|
||||
pub(crate) fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
use std::time::Duration;
|
||||
use tauri::{is_dev, AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||
use ts_rs::TS;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -63,10 +63,15 @@ pub struct APIErrorResponsePayload {
|
||||
|
||||
pub async fn activate_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
p: ActivateLicenseRequestPayload,
|
||||
license_key: &str,
|
||||
) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(build_url("/licenses/activate")).json(&p).send().await?;
|
||||
let payload = ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
};
|
||||
let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
@@ -95,16 +100,17 @@ pub async fn activate_license<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn deactivate_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
p: DeactivateLicenseRequestPayload,
|
||||
) -> Result<()> {
|
||||
pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<()> {
|
||||
let app_handle = window.app_handle();
|
||||
let activation_id = get_activation_id(app_handle).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let path = format!("/licenses/activations/{}/deactivate", activation_id);
|
||||
let response = client.post(build_url(&path)).json(&p).send().await?;
|
||||
let payload = DeactivateLicenseRequestPayload {
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
};
|
||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
@@ -141,10 +147,11 @@ pub enum LicenseCheckStatus {
|
||||
Trialing { end: NaiveDateTime },
|
||||
}
|
||||
|
||||
pub async fn check_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
payload: CheckActivationRequestPayload,
|
||||
) -> Result<LicenseCheckStatus> {
|
||||
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||
let payload = CheckActivationRequestPayload {
|
||||
app_platform: crate::get_os().to_string(),
|
||||
app_version: window.package_info().version.to_string(),
|
||||
};
|
||||
let activation_id = get_activation_id(window.app_handle()).await;
|
||||
let settings = window.db().get_settings();
|
||||
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
|
||||
@@ -197,9 +204,5 @@ fn build_url(path: &str) -> String {
|
||||
}
|
||||
|
||||
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
|
||||
app_handle.db().get_key_value_string(
|
||||
KV_ACTIVATION_ID_KEY,
|
||||
KV_NAMESPACE,
|
||||
"",
|
||||
)
|
||||
app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function SelectFile({
|
||||
<>
|
||||
{filePath && (
|
||||
<IconButton
|
||||
size={size}
|
||||
size={size === 'auto' ? 'md' : size}
|
||||
variant="border"
|
||||
icon="x"
|
||||
title={'Unset ' + itemLabel}
|
||||
|
||||
@@ -12,7 +12,7 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onC
|
||||
color?: Color | 'custom' | 'default';
|
||||
variant?: 'border' | 'solid';
|
||||
isLoading?: boolean;
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
|
||||
justify?: 'start' | 'center';
|
||||
type?: 'button' | 'submit';
|
||||
forDropdown?: boolean;
|
||||
@@ -114,7 +114,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingIcon size={size} className="mr-1" />
|
||||
<LoadingIcon size={size === 'auto' ? 'md' : size} className="mr-1" />
|
||||
) : leftSlot ? (
|
||||
<div className="mr-2">{leftSlot}</div>
|
||||
) : null}
|
||||
@@ -128,7 +128,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{children}
|
||||
</div>
|
||||
{rightSlot && <div className="ml-1">{rightSlot}</div>}
|
||||
{forDropdown && <Icon icon="chevron_down" size={size} className="ml-1 -mr-1" />}
|
||||
{forDropdown && (
|
||||
<Icon icon="chevron_down" size={size === 'auto' ? 'md' : size} className="ml-1 -mr-1" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ function mixLanguage(base: LanguageSupport): LRLanguage {
|
||||
|
||||
return {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'PlainText',
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
|
||||
export const highlight = styleTags({
|
||||
"${[": t.bracket,
|
||||
"]}": t.bracket,
|
||||
"(": t.bracket,
|
||||
")": t.bracket,
|
||||
"=": t.bracket,
|
||||
",": t.bracket,
|
||||
Tag: t.keyword,
|
||||
Identifier: t.variableName,
|
||||
ChainedIdentifier: t.variableName,
|
||||
Boolean: t.bool,
|
||||
TagOpen: t.bracket,
|
||||
TagClose: t.bracket,
|
||||
TagContent: t.keyword,
|
||||
});
|
||||
|
||||
@@ -80,6 +80,10 @@ function templateTags(
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
|
||||
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
|
||||
|
||||
if (inner.includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
if (name === 'Response') {
|
||||
|
||||
@@ -1,70 +1,17 @@
|
||||
@precedence {
|
||||
top,
|
||||
mid,
|
||||
lesser
|
||||
@top Template { (Tag | Text)* }
|
||||
|
||||
@local tokens {
|
||||
TagClose { "]}" }
|
||||
@else TagContent
|
||||
}
|
||||
|
||||
@top Template { (Tag | PlainText)* }
|
||||
|
||||
@skip { space }
|
||||
|
||||
Tag { "${[" expression* "]}" }
|
||||
|
||||
commaSep<content> { "" | content ("," content?)* }
|
||||
|
||||
expression {
|
||||
Function |
|
||||
Assignment |
|
||||
Identifier |
|
||||
ChainedIdentifier |
|
||||
basicType |
|
||||
hashStructure
|
||||
@skip { } {
|
||||
TagOpen { "${[" }
|
||||
Tag { TagOpen (TagContent)+ TagClose }
|
||||
}
|
||||
|
||||
basicType {
|
||||
String |
|
||||
Boolean |
|
||||
Number
|
||||
}
|
||||
|
||||
functionParam {
|
||||
Identifier |
|
||||
Assignment |
|
||||
basicType
|
||||
}
|
||||
|
||||
FunctionParamList {
|
||||
"(" commaSep<functionParam> ")"
|
||||
}
|
||||
|
||||
Assignment { Identifier "=" expression }
|
||||
|
||||
Function { (Identifier | ChainedIdentifier) !top FunctionParamList}
|
||||
|
||||
@tokens {
|
||||
PlainText { ![$] PlainText? | "$" (@eof | ![{[] PlainText?) }
|
||||
|
||||
Identifier { $[a-zA-Z_\-0-9]+ }
|
||||
ChainedIdentifier { Identifier ("." Identifier)+ }
|
||||
|
||||
hashStructure { "{" | "}" | ":" | ","}
|
||||
|
||||
Boolean { "true" | "false" }
|
||||
String { ("'" (![\\'] | "\\" _)* "'"?) | ("\"" (![\\"] | "\\" _)* "\""?) }
|
||||
Number { '-'? int frac? }
|
||||
int { '0' | $[1-9] @digit* }
|
||||
frac { '.' @digit+ }
|
||||
|
||||
@precedence { PlainText, Boolean, Number, ChainedIdentifier, Identifier, space, String }
|
||||
|
||||
space { $[ \t\n\r]+ }
|
||||
|
||||
"${[" "]}"
|
||||
"(" ")"
|
||||
"="
|
||||
","
|
||||
Text { ![$] Text? | "$" (@eof | ![{] Text?) }
|
||||
}
|
||||
|
||||
@external propSource highlight from "./highlight"
|
||||
|
||||
@detectDelim
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
Template = 1,
|
||||
Tag = 4,
|
||||
Function = 5,
|
||||
Identifier = 6,
|
||||
ChainedIdentifier = 7,
|
||||
FunctionParamList = 10,
|
||||
Assignment = 11,
|
||||
String = 13,
|
||||
Boolean = 14,
|
||||
Number = 15,
|
||||
PlainText = 17
|
||||
Tag = 2,
|
||||
TagOpen = 3,
|
||||
TagContent = 4,
|
||||
TagClose = 5,
|
||||
Text = 6
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {LRParser, LocalTokenGroup} from "@lezer/lr"
|
||||
import {highlight} from "./highlight"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "$zQVQPOOOsQQO'#C`OOQO'#Cn'#CnQVQPOOO!fQQO'#CtOOQO'#Cw'#CwOzQQO'#CtOOQO'#Ct'#CtOOQO'#Co'#CoO!mQQO,58zOOQO,58z,58zOOQO-E6l-E6lO_QQO,59RO!tQQO'#CfOOQO,58{,58{OOQO-E6m-E6mOOQO1G.f1G.fOOQO1G.m1G.mO#VQSO'#CvOOQO'#Cv'#CvO#bQSO'#CuO#jQQO,59QO#oQSO'#CpO$TQSO,59aOOQO1G.l1G.lO$]QSO'#CtO$kQSO'#CtOOQO,59[,59[OOQO-E6n-E6nO$vQQO,59R",
|
||||
stateData: "%e~OgOS~ORPOaQO~OUSOVUO]TO^TO_TOlVO~OQYO~P_OX]OQhXUhXVhX]hX^hX_hXlhX~O[[O~PzOQ`O~P_OUbO]TO^TO_TOWiP~O[mOWjX`jX~O`fOWiX~OWhO~OUbO]TO^TO_TOWdX`dX~O`fOWia~OX]O[mOWhX`hX~OX]OWhX`hX~OUiOVjO]TO^TO_TOlVO~Oa^_VUg]V~",
|
||||
goto: "!|lPPPPmqPPPPw}PPPPPP!X!_!ePPP!k!s!v}TQORXVPX[mX^SUijWVPX[mTc]fQRORZRQXPR_XQgdRlgSWPXTa[mRe]Qd]Rkf",
|
||||
nodeNames: "⚠ Template ]} ${[ Tag Function Identifier ChainedIdentifier ) ( FunctionParamList Assignment = String Boolean Number , PlainText",
|
||||
maxTerm: 28,
|
||||
nodeProps: [
|
||||
["openedBy", 2,"${[",8,"("],
|
||||
["closedBy", 3,"]}",9,")"]
|
||||
],
|
||||
states: "!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
||||
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
|
||||
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
|
||||
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
|
||||
maxTerm: 10,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 3,
|
||||
tokenData: "Fe~RzOX#uXY%OYZ%OZ]#u]^%O^p#upq%Oqr#urs%{st#utu+Ouw#uwx+vxy0^yz0tz|#u|}1[}!O1t!O!Q#u!Q!R6h!R![:S![!];_!]!_#u!_!`;u!`!c#u!c!}3Q!}#P#u#P#Q<]#Q#R#u#R#S3Q#S#T#u#T#Y3Q#Y#Z=_#Z#h3Q#h#iCu#i#o3Q#o#p;_#p#q#u#q#r;_#r;'S#u;'S;=`$s<%lO#uP#zTaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uP$^VO!}#u#O#o#u#p;'S#u;'S;=`$s<%l~#u~O#u~~$yP$vP;=`<%l#uP%OOaP~%V[aPg~OX#uXY%OYZ%OZ]#u]^%O^p#upq%Oqt#utu$Zu;'S#u;'S;=`$s<%lO#u~&SXaP]~Or%{rs&ost%{tu'Vu#O%{#O#P)r#P;'S%{;'S;=`*x<%lO%{~&vTaP]~Ot#utu$Zu;'S#u;'S;=`$s<%lO#u~'[[]~Or%{rs&os!}%{!}#O(Q#O#P)r#P#o%{#o#p(Q#p;'S%{;'S;=`*x<%l~%{~O%{~~$y~(VV]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l<%lO(Q~(qO]~~(tRO;'S(Q;'S;=`(};=`O(Q~)SW]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l;=`<%l(Q<%lO(Q~)oP;=`<%l(Q~)wUaPOt%{tu'Vu;'S%{;'S;=`*Z;=`<%l(Q<%lO%{~*`W]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l;=`<%l%{<%lO(Q~*{P;=`<%l%{~+RWO!}#u#O#o#u#o#p+k#p;'S#u;'S;=`$s<%l~#u~O#u~~$y~+nP!}#O+q~+vOR~~+}XaP]~Ot+vtu,juw+vwx&ox#O+v#O#P/Q#P;'S+v;'S;=`0W<%lO+v~,o[]~Ow+vwx&ox!}+v!}#O-e#O#P/Q#P#o+v#o#p-e#p;'S+v;'S;=`0W<%l~+v~O+v~~$y~-jV]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z<%lO-e~.SRO;'S-e;'S;=`.];=`O-e~.bW]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z;=`<%l-e<%lO-e~.}P;=`<%l-e~/VUaPOt+vtu,ju;'S+v;'S;=`/i;=`<%l-e<%lO+v~/nW]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z;=`<%l+v<%lO-e~0ZP;=`<%l+vV0eTXUaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV0{TWUaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV1eT`SlQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#u~1{aaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q!R6h!R![:S![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~3X`aPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~4`_aPOt#utu$Zu}#u}!O5_!O!Q#u!Q![5_![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~5f`aPV~Ot#utu$Zu}#u}!O5_!O!P4Z!P!Q#u!Q![5_![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~6q`aP_~U~Ot#utu$Zu}#u}!O3Q!O!P7s!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~7x_aPOt#utu$Zu}#u}!O5_!O!Q#u!Q![8w![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~9Q`aP_~V~Ot#utu$Zu}#u}!O5_!O!P4Z!P!Q#u!Q![8w![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~:]`aP_~U~Ot#utu$Zu}#u}!O3Q!O!P7s!P!Q#u!Q![:S![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#uR;fTlQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV;|T[UaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uR<bVaPOt#utu$Zu#q#u#q#r<w#r;'S#u;'S;=`$s<%lO#uR=OTQQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#u~=faaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#U>k#U#o3Q#o;'S#u;'S;=`$s<%lO#u~>rbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#`3Q#`#a?z#a#o3Q#o;'S#u;'S;=`$s<%lO#u~@RbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#g3Q#g#hAZ#h#o3Q#o;'S#u;'S;=`$s<%lO#u~AbbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#X3Q#X#YBj#Y#o3Q#o;'S#u;'S;=`$s<%lO#u~Bs`aP^~U~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~C|baPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#f3Q#f#gEU#g#o3Q#o;'S#u;'S;=`$s<%lO#u~E]baPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#i3Q#i#jAZ#j#o3Q#o;'S#u;'S;=`$s<%lO#u",
|
||||
tokenizers: [0, 1, 2],
|
||||
repeatNodeCount: 2,
|
||||
tokenData: "#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
|
||||
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
|
||||
topRules: {"Template":[0,1]},
|
||||
tokenPrec: 196
|
||||
tokenPrec: 0
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createFastMutation } from '../../hooks/useFastMutation';
|
||||
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { copyToClipboard } from '../../lib/copy';
|
||||
@@ -20,7 +21,10 @@ import {
|
||||
convertTemplateToSecure,
|
||||
} from '../../lib/encryption';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { withEncryptionEnabled } from '../../lib/setupOrConfigureEncryption';
|
||||
import {
|
||||
setupOrConfigureEncryption,
|
||||
withEncryptionEnabled,
|
||||
} from '../../lib/setupOrConfigureEncryption';
|
||||
import { Button } from './Button';
|
||||
import type { DropdownItem } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
@@ -29,6 +33,7 @@ import { Editor } from './Editor/Editor';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { IconTooltip } from './IconTooltip';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
@@ -67,7 +72,7 @@ export type InputProps = Pick<
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
rightSlot?: ReactNode;
|
||||
size?: 'xs' | 'sm' | 'md' | 'auto';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
|
||||
stateKey: EditorProps['stateKey'];
|
||||
tint?: Color;
|
||||
type?: 'text' | 'password';
|
||||
@@ -231,6 +236,7 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
size === '2xs' && 'min-h-2xs',
|
||||
)}
|
||||
>
|
||||
{tint != null && (
|
||||
@@ -332,7 +338,10 @@ function EncryptionInput({
|
||||
value: string | null;
|
||||
security: ReturnType<typeof analyzeTemplate> | null;
|
||||
obscured: boolean;
|
||||
}>({ fieldType: 'encrypted', value: null, security: null, obscured: true }, [ogForceUpdateKey]);
|
||||
error: string | null;
|
||||
}>({ fieldType: 'encrypted', value: null, security: null, obscured: true, error: null }, [
|
||||
ogForceUpdateKey,
|
||||
]);
|
||||
|
||||
const forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`;
|
||||
|
||||
@@ -345,25 +354,48 @@ function EncryptionInput({
|
||||
const security = analyzeTemplate(defaultValue ?? '');
|
||||
if (analyzeTemplate(defaultValue ?? '') === 'global_secured') {
|
||||
// Lazily update value to decrypted representation
|
||||
convertTemplateToInsecure(defaultValue ?? '').then((value) => {
|
||||
setState({ fieldType: 'encrypted', security, value, obscured: true });
|
||||
templateToInsecure.mutate(defaultValue ?? '', {
|
||||
onSuccess: (value) => {
|
||||
setState({ fieldType: 'encrypted', security, value, obscured: true, error: null });
|
||||
},
|
||||
onError: (value) => {
|
||||
setState({
|
||||
fieldType: 'encrypted',
|
||||
security,
|
||||
value: null,
|
||||
error: String(value),
|
||||
obscured: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else if (isEncryptionEnabled && !defaultValue) {
|
||||
// Default to encrypted field for new encrypted inputs
|
||||
setState({ fieldType: 'encrypted', security, value: '', obscured: true });
|
||||
setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null });
|
||||
} else if (isEncryptionEnabled) {
|
||||
// Don't obscure plain text when encryption is enabled
|
||||
setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: false });
|
||||
setState({
|
||||
fieldType: 'text',
|
||||
security,
|
||||
value: defaultValue ?? '',
|
||||
obscured: false,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
// Don't obscure plain text when encryption is disabled
|
||||
setState({ fieldType: 'text', security, value: defaultValue ?? '', obscured: true });
|
||||
setState({
|
||||
fieldType: 'text',
|
||||
security,
|
||||
value: defaultValue ?? '',
|
||||
obscured: true,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}, [defaultValue, isEncryptionEnabled, setState, state.value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string, fieldType: PasswordFieldType) => {
|
||||
if (fieldType === 'encrypted') {
|
||||
convertTemplateToSecure(value).then((value) => onChange?.(value));
|
||||
templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) });
|
||||
} else {
|
||||
onChange?.(value);
|
||||
}
|
||||
@@ -372,7 +404,7 @@ function EncryptionInput({
|
||||
const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value);
|
||||
// Reset obscured value when the field type is being changed
|
||||
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text';
|
||||
return { fieldType, value, security, obscured };
|
||||
return { fieldType, value, security, obscured, error: s.error };
|
||||
});
|
||||
},
|
||||
[onChange, setState],
|
||||
@@ -477,6 +509,23 @@ function EncryptionInput({
|
||||
|
||||
const type = state.obscured ? 'password' : 'text';
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<Button
|
||||
variant="border"
|
||||
color="danger"
|
||||
size={props.size}
|
||||
className="text-sm"
|
||||
rightSlot={<IconTooltip content={state.error} icon="alert_triangle" />}
|
||||
onClick={() => {
|
||||
setupOrConfigureEncryption();
|
||||
}}
|
||||
>
|
||||
{state.error.replace(/^Render Error: /i, '')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseInput
|
||||
disableObscureToggle
|
||||
@@ -488,8 +537,20 @@ function EncryptionInput({
|
||||
tint={tint}
|
||||
type={type}
|
||||
rightSlot={rightSlot}
|
||||
disabled={state.error != null}
|
||||
className="pr-1.5" // To account for encryption dropdown
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const templateToSecure = createFastMutation({
|
||||
mutationKey: ['template-to-secure'],
|
||||
mutationFn: convertTemplateToSecure,
|
||||
});
|
||||
|
||||
const templateToInsecure = createFastMutation({
|
||||
mutationKey: ['template-to-insecure'],
|
||||
mutationFn: convertTemplateToInsecure,
|
||||
disableToastError: true,
|
||||
});
|
||||
|
||||
@@ -116,6 +116,7 @@ export function PlainInput({
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
size === '2xs' && 'min-h-2xs',
|
||||
)}
|
||||
>
|
||||
{tint != null && (
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
deleteModelById,
|
||||
duplicateModelById,
|
||||
getModel,
|
||||
workspacesAtom,
|
||||
} from '@yaakapp-internal/models';
|
||||
import { duplicateModelById, getModel, workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
@@ -134,7 +129,9 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
hotKeyAction: 'sidebar.delete_selected_item',
|
||||
hotKeyLabelOnly: true,
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: async () => deleteModelById(child.model, child.id),
|
||||
onSelect: async () => {
|
||||
await deleteModelWithConfirm(getModel(child.model, child.id));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function createFastMutation<TData = unknown, TError = unknown, TVariables
|
||||
try {
|
||||
const data = await mutationFn(variables);
|
||||
onSuccess?.(data);
|
||||
onSettled?.();
|
||||
return data;
|
||||
} catch (err: unknown) {
|
||||
const stringKey = mutationKey.join('.');
|
||||
@@ -44,11 +45,9 @@ export function createFastMutation<TData = unknown, TError = unknown, TVariables
|
||||
});
|
||||
}
|
||||
onError?.(e);
|
||||
} finally {
|
||||
onSettled?.();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const mutate = (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { HttpRequest} from '@yaakapp-internal/models';
|
||||
import { createWorkspaceModel, patchModelById } from '@yaakapp-internal/models';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { patchModelById } from '@yaakapp-internal/models';
|
||||
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { showToast } from '../lib/toast';
|
||||
@@ -26,7 +27,7 @@ export function useImportCurl() {
|
||||
let verb;
|
||||
if (overwriteRequestId == null) {
|
||||
verb = 'Created';
|
||||
await createWorkspaceModel(importedRequest);
|
||||
await createRequestAndNavigate(importedRequest);
|
||||
} else {
|
||||
verb = 'Updated';
|
||||
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
|
||||
|
||||
@@ -11,7 +11,7 @@ export function languageFromContentType(
|
||||
} else if (justContentType.includes('xml')) {
|
||||
return 'xml';
|
||||
} else if (justContentType.includes('html')) {
|
||||
const detected = detectFromContent(content, 'html');
|
||||
const detected = detectFromContent(content);
|
||||
if (detected === 'xml') {
|
||||
// If it's detected as XML, but is already HTML, don't change it
|
||||
return 'html';
|
||||
@@ -19,7 +19,8 @@ export function languageFromContentType(
|
||||
return detected;
|
||||
}
|
||||
} else if (justContentType.includes('javascript')) {
|
||||
return 'javascript';
|
||||
// Sometimes `application/javascript` returns JSON, so try detecting that
|
||||
return detectFromContent(content, 'javascript');
|
||||
}
|
||||
|
||||
return detectFromContent(content, 'text');
|
||||
@@ -27,7 +28,7 @@ export function languageFromContentType(
|
||||
|
||||
function detectFromContent(
|
||||
content: string | null,
|
||||
fallback: EditorProps['language'],
|
||||
fallback?: EditorProps['language'],
|
||||
): EditorProps['language'] {
|
||||
if (content == null) return 'text';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function deleteModelWithConfirm(model: AnyModel | null): Promise<bo
|
||||
}
|
||||
|
||||
const confirmed = await showConfirmDelete({
|
||||
id: 'delete-model-' + model.model,
|
||||
id: 'delete-model-' + model.id,
|
||||
title: 'Delete ' + modelTypeLabel(model),
|
||||
description: (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user