Compare commits

...

8 Commits

Author SHA1 Message Date
Gregory Schier
1e18933178 Kill plugin manager before NSIS update on Windows 2024-08-28 09:14:39 -07:00
Gregory Schier
97a4770464 Remove tauri "unstable" feature to fix Codemirror selection 2024-08-28 06:48:02 -07:00
Gregory Schier
db02dbcaa4 Hotfix for window focusing 2024-08-27 16:56:04 -07:00
Gregory Schier
badcbc7aef Special case Response()->response() 2024-08-26 15:26:43 -07:00
Gregory Schier
4b91601b98 Clean up code 2024-08-26 15:10:29 -07:00
Gregory Schier
93e0202b86 Default template fn args 2024-08-26 13:10:22 -07:00
Gregory Schier
e75d6abe33 Option to disable telemetry 2024-08-26 12:06:56 -07:00
Gregory Schier
24a4e3494e Node syntaxTree to parse template tags 2024-08-26 11:30:10 -07:00
31 changed files with 197 additions and 109 deletions

8
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.12",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -2990,9 +2990,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.12.tgz",
"integrity": "sha512-qA+2BBZz1LGTi0wsOmlwaw6xJbE3elPIUMt/BkiRT+DqQC5spZtISsyoPXjtsM0xZc2orjoRJd0LesXH7xkD0g==",
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.13.tgz",
"integrity": "sha512-FSYPHZV0mP967w63VXi9zYP81hPo3vjSW3/UElJLuF/8ig6WmG4p1q2oYos4Ik267Z3qSQAGN5dPMfuk3DAnBA==",
"dependencies": {
"@types/node": "^22.0.0"
}

View File

@@ -42,7 +42,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.12",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.1.12",
"version": "0.1.13",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, telemetry: boolean, openWorkspaceNewWindow: boolean | null, };

View File

@@ -44,7 +44,7 @@ reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true, features = ["unstable"] }
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-dialog = "2.0.0-rc.0"

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN telemetry BOOLEAN DEFAULT TRUE;

View File

@@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{
generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string,
};
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
use crate::is_dev;
@@ -157,6 +155,7 @@ pub async fn track_event<R: Runtime>(
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
@@ -186,9 +185,15 @@ pub async fn track_event<R: Runtime>(
.get(format!("{base_url}/t/e"))
.query(&params);
let settings = get_or_create_settings(w).await;
if !settings.telemetry {
info!("Track event (disabled): {}", event);
return
}
// Disable analytics actual sending in dev
if is_dev() {
debug!("track: {} {}", event, attributes_json);
debug!("Track event: {} {}", event, attributes_json);
return;
}

View File

@@ -2062,20 +2062,28 @@ async fn handle_plugin_event<R: Runtime>(app_handle: &AppHandle<R>, event: &Inte
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {}", e)
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}
// app_handle.get_focused_window locks, so this one is a non-locking version, safe for use in async context
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
// TODO: Getting the focused window doesn't seem to work on Windows, so
// we'll need to pass the window label into plugin events instead.
if app_handle.webview_windows().len() == 1 {
debug!("Returning only webview window");
let w = app_handle
.webview_windows()
.iter()
.next()
.map(|w| w.1.clone());
return w;
}
app_handle
.windows()
.iter()
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())?
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned())
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager};
use yaak_plugin_runtime::events::RenderPurpose;
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg};
use yaak_plugin_runtime::manager::PluginManager;
use yaak_templates::TemplateCallback;
@@ -12,7 +12,10 @@ pub struct PluginTemplateCallback {
impl PluginTemplateCallback {
pub fn new(app_handle: AppHandle) -> PluginTemplateCallback {
PluginTemplateCallback { app_handle, purpose: RenderPurpose::Preview }
PluginTemplateCallback {
app_handle,
purpose: RenderPurpose::Preview,
}
}
pub fn for_send(&self) -> PluginTemplateCallback {
@@ -24,9 +27,41 @@ impl PluginTemplateCallback {
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
// 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
let fn_name = if fn_name == "Response" {
"response"
} else {
fn_name
};
let plugin_manager = self.app_handle.state::<PluginManager>();
let function = plugin_manager
.get_template_functions()
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for a_def in function.args {
let base = match a_def {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = plugin_manager
.call_template_function(fn_name, args, self.purpose.clone())
.call_template_function(fn_name, args_with_defaults, self.purpose.clone())
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())

View File

@@ -2,9 +2,11 @@ use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use log::info;
use tauri::AppHandle;
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_updater::UpdaterExt;
use tokio::task::block_in_place;
use yaak_plugin_runtime::manager::PluginManager;
use crate::is_dev;
@@ -49,6 +51,7 @@ impl YaakUpdater {
last_update_check: SystemTime::UNIX_EPOCH,
}
}
pub async fn force_check(
&mut self,
app_handle: &AppHandle,
@@ -58,8 +61,22 @@ impl YaakUpdater {
info!("Checking for updates mode={}", mode);
let h = app_handle.clone();
let update_check_result = app_handle
.updater_builder()
.on_before_exit(move || {
// Kill plugin manager before exit or NSIS installer will fail to replace sidecar
// while it's running.
// NOTE: This is only called on Windows
let h = h.clone();
block_in_place(|| {
tauri::async_runtime::block_on(async move {
info!("Shutting down plugin manager before update");
let plugin_manager = h.state::<PluginManager>();
plugin_manager.cleanup().await;
});
});
})
.header("X-Update-Mode", mode.to_string())?
.build()?
.check()

View File

@@ -23,6 +23,7 @@ pub struct Settings {
pub interface_scale: i32,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub telemetry: bool,
pub open_workspace_new_window: Option<bool>,
}
@@ -43,6 +44,7 @@ pub enum SettingsIden {
InterfaceScale,
EditorFontSize,
EditorSoftWrap,
Telemetry,
OpenWorkspaceNewWindow,
}
@@ -64,6 +66,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
interface_scale: r.get("interface_scale")?,
editor_font_size: r.get("editor_font_size")?,
editor_soft_wrap: r.get("editor_soft_wrap")?,
telemetry: r.get("telemetry")?,
open_workspace_new_window: r.get("open_workspace_new_window")?,
})
}

View File

@@ -799,6 +799,10 @@ pub async fn update_settings<R: Runtime>(
SettingsIden::EditorSoftWrap,
settings.editor_soft_wrap.into(),
),
(
SettingsIden::Telemetry,
settings.telemetry.into(),
),
(
SettingsIden::OpenWorkspaceNewWindow,
settings.open_workspace_new_window.into(),

View File

@@ -1,6 +1,6 @@
import type { Cookie } from '@yaakapp/api';
import { useCookieJars } from '../hooks/useCookieJars';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import type { Cookie } from '../lib/models/Cookie';
import { cookieDomain } from '../lib/models';
import { Banner } from './core/Banner';
import { IconButton } from './core/IconButton';

View File

@@ -77,6 +77,12 @@ export function SettingsGeneral() {
{ label: 'New Window', value: 'new' },
]}
/>
<Checkbox
checked={settings.telemetry}
title="Send Usage Statistics"
onChange={(telemetry) => updateSettings.mutate({ telemetry })}
/>
<Separator className="my-4" />
<Heading size={2}>

View File

@@ -68,7 +68,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),
'application/javascript': javascript(),
'text/html': xml(), // HTML as xml because HTML is oddly slow
'text/html': xml(), // HTML as XML because HTML is oddly slow
'application/xml': xml(),
'text/xml': xml(),
url: url(),

View File

@@ -7,7 +7,7 @@ import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
import type { TwigCompletionOption } from './completion';
import { twigCompletion } from './completion';
import { templateTags } from './templateTags';
import { templateTagsPlugin } from './templateTags';
import { parser as twigParser } from './twig';
export function twig({
@@ -62,7 +62,7 @@ export function twig({
return [
language,
base.support,
templateTags(options, onClickMissingVariable),
templateTagsPlugin(options, onClickMissingVariable),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),

View File

@@ -1,6 +1,8 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import { BetterMatchDecorator } from '../BetterMatchDecorator';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import { EditorView } from 'codemirror';
import type { TwigCompletionOption } from './completion';
class TemplateTagWidget extends WidgetType {
@@ -22,7 +24,8 @@ class TemplateTagWidget extends WidgetType {
this.option.name === other.option.name &&
this.option.type === other.option.type &&
this.option.value === other.option.value &&
this.rawTag === other.rawTag
this.rawTag === other.rawTag &&
this.startPos === other.startPos
);
}
@@ -55,69 +58,91 @@ class TemplateTagWidget extends WidgetType {
}
}
export function templateTags(
function templateTags(
view: EditorView,
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
) {
const templateTagMatcher = new BetterMatchDecorator({
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
decoration(match, view, matchStartPos) {
const matchEndPos = matchStartPos + match[0].length - 1;
): DecorationSet {
const widgets: Range<Decoration>[] = [];
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.name == 'Tag') {
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > node.from && r.to < node.to) {
return;
}
}
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > matchStartPos && r.to <= matchEndPos) {
return Decoration.replace({});
const rawTag = view.state.doc.sliceString(node.from, node.to);
// TODO: Search `node.tree` instead of using Regex here
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
let name = inner.match(/(\w+)[(]/)?.[1] ?? inner;
// 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') {
name = 'response';
}
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: inner,
value: null,
label: inner,
onClick: () => onClickMissingVariable(name, rawTag, node.from),
};
}
const widget = new TemplateTagWidget(option, rawTag, node.from);
const deco = Decoration.replace({ widget, inclusive: true });
widgets.push(deco.range(node.from, node.to));
}
}
const innerTagMatch = match[1];
if (innerTagMatch == null) {
// Should never happen, but make TS happy
console.warn('Group match was empty', match);
return Decoration.replace({});
}
// TODO: Replace this hacky match with a proper template parser
const name = innerTagMatch.match(/\s*(\w+)[(\s]*/)?.[1] ?? innerTagMatch;
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: innerTagMatch,
value: null,
label: innerTagMatch,
onClick: () => onClickMissingVariable(name, match[0], matchStartPos),
};
}
return Decoration.replace({
inclusive: true,
widget: new TemplateTagWidget(option, match[0], matchStartPos),
});
},
});
},
});
}
return Decoration.set(widgets);
}
export function templateTagsPlugin(
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = templateTagMatcher.createDeco(view);
this.decorations = templateTags(view, options, onClickMissingVariable);
}
update(update: ViewUpdate) {
this.decorations = templateTagMatcher.updateDeco(update, this.decorations);
this.decorations = templateTags(update.view, options, onClickMissingVariable);
}
},
{
decorations: (instance) => instance.decorations,
provide: (plugin) =>
EditorView.atomicRanges.of((view) => {
decorations(v) {
return v.decorations;
},
provide(plugin) {
return EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.decorations || Decoration.none;
}),
});
},
eventHandlers: {
mousedown(e) {
const target = e.target as HTMLElement;
if (target.classList.contains('template-tag')) console.log('CLICKED TEMPLATE TAG');
// return toggleBoolean(view, view.posAtDOM(target));
},
},
},
);
}

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import type { CookieJar } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';

View File

@@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';

View File

@@ -1,7 +1,7 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';

View File

@@ -1,6 +1,6 @@
import type { Settings } from '@yaakapp/api';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import type { Settings } from '../lib/models/Settings';
import { getSettings } from '../lib/store';
const settings = await getSettings();

View File

@@ -1,5 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import type { CookieJar } from '@yaakapp/api';
import { getCookieJar } from '../lib/store';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,5 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { Settings } from '../lib/models';
import type { Settings } from '@yaakapp/api';
import { getSettings } from '../lib/store';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,9 +1,4 @@
import type { GrpcConnection, HttpResponse, HttpResponseHeader, Model } from '@yaakapp/api';
import type { Cookie } from './models/Cookie';
import type { CookieJar } from './models/CookieJar';
import type { Settings } from './models/Settings';
export type { CookieJar, Cookie, Settings };
import type { Cookie, GrpcConnection, HttpResponse, HttpResponseHeader, Model } from '@yaakapp/api';
export const BODY_TYPE_NONE = null;
export const BODY_TYPE_GRAPHQL = 'graphql';

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CookieDomain } from "./CookieDomain";
import type { CookieExpires } from "./CookieExpires";
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CookieExpires = { "AtUtc": string } | "SessionEnd";

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Cookie } from "./Cookie";
export type CookieJar = { id: string, model: "cookie_jar", createdAt: string, updatedAt: string, workspaceId: string, name: string, cookies: Array<Cookie>, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };

View File

@@ -1,6 +1,12 @@
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import type { CookieJar } from './models/CookieJar';
import type { Settings } from './models/Settings';
import type {
CookieJar,
Environment,
Folder,
GrpcRequest,
HttpRequest,
Settings,
Workspace,
} from '@yaakapp/api';
import { invokeCmd } from './tauri';
export async function getSettings(): Promise<Settings> {

1
src-web/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />