mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-19 07:53:54 +01:00
Ability to open workspace from directory, WorkspaceMeta, and many sync improvements
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"clipboard-manager:allow-read-text",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-read-text-file",
|
||||
{
|
||||
|
||||
2
src-tauri/gen/schemas/acl-manifests.json
generated
2
src-tauri/gen/schemas/acl-manifests.json
generated
File diff suppressed because one or more lines are too long
2
src-tauri/gen/schemas/capabilities.json
generated
2
src-tauri/gen/schemas/capabilities.json
generated
@@ -1 +1 @@
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"opener:allow-open-url","opener:allow-open-path","opener:allow-default-urls","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","yaak-license:default","yaak-sync:default"]}}
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"opener:allow-open-url","opener:allow-open-path","opener:allow-default-urls","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","yaak-license:default","yaak-sync:default"]}}
|
||||
188
src-tauri/gen/schemas/desktop-schema.json
generated
188
src-tauri/gen/schemas/desktop-schema.json
generated
@@ -5442,16 +5442,6 @@
|
||||
"type": "string",
|
||||
"const": "yaak-sync:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the apply command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -5463,105 +5453,15 @@
|
||||
"const": "yaak-sync:allow-calculate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"description": "Enables the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-check"
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Enables the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Enables the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-sync"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-unstage"
|
||||
"const": "yaak-sync:allow-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the watch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-watch"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the apply command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -5573,89 +5473,9 @@
|
||||
"const": "yaak-sync:deny-calculate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"description": "Denies the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-check"
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Denies the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Denies the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-sync"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-unstage"
|
||||
"const": "yaak-sync:deny-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the watch command without any pre-configured scope.",
|
||||
|
||||
188
src-tauri/gen/schemas/macOS-schema.json
generated
188
src-tauri/gen/schemas/macOS-schema.json
generated
@@ -5442,16 +5442,6 @@
|
||||
"type": "string",
|
||||
"const": "yaak-sync:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the apply command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -5463,105 +5453,15 @@
|
||||
"const": "yaak-sync:allow-calculate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"description": "Enables the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-check"
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Enables the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Enables the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-sync"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-unstage"
|
||||
"const": "yaak-sync:allow-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the watch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:allow-watch"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the apply command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -5573,89 +5473,9 @@
|
||||
"const": "yaak-sync:deny-calculate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"description": "Denies the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-check"
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Denies the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Denies the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-sync"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-sync:deny-unstage"
|
||||
"const": "yaak-sync:deny-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the watch command without any pre-configured scope.",
|
||||
|
||||
11
src-tauri/migrations/20250108205117_workspace-meta.sql
Normal file
11
src-tauri/migrations/20250108205117_workspace-meta.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE workspace_metas
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'workspace_meta' NOT NULL,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
setting_sync_dir TEXT
|
||||
);
|
||||
@@ -43,7 +43,7 @@ use crate::window_menu::app_menu;
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
|
||||
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
|
||||
ModelType, Plugin, Settings, Workspace,
|
||||
ModelType, Plugin, Settings, Workspace, WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::queries::{
|
||||
batch_upsert, cancel_pending_grpc_connections, cancel_pending_responses,
|
||||
@@ -55,14 +55,14 @@ use yaak_models::queries::{
|
||||
duplicate_http_request, ensure_base_environment, generate_id, generate_model_id,
|
||||
get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
|
||||
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
|
||||
get_or_create_settings, get_plugin, get_workspace, get_workspace_export_resources,
|
||||
list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace,
|
||||
list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_request,
|
||||
list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces,
|
||||
set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
|
||||
upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, BatchUpsertResult,
|
||||
UpdateSource,
|
||||
get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace,
|
||||
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
|
||||
list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests,
|
||||
list_http_responses_for_request, list_http_responses_for_workspace, list_key_values_raw,
|
||||
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
|
||||
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
|
||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
|
||||
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
|
||||
};
|
||||
use yaak_plugins::events::{
|
||||
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
||||
@@ -1294,6 +1294,16 @@ async fn cmd_update_workspace(workspace: Workspace, w: WebviewWindow) -> Result<
|
||||
upsert_workspace(&w, workspace, &UpdateSource::Window).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_update_workspace_meta(
|
||||
workspace_meta: WorkspaceMeta,
|
||||
w: WebviewWindow,
|
||||
) -> Result<WorkspaceMeta, String> {
|
||||
upsert_workspace_meta(&w, workspace_meta, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_update_environment(
|
||||
environment: Environment,
|
||||
@@ -1497,70 +1507,79 @@ async fn cmd_list_cookie_jars(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_list_key_values(w: WebviewWindow) -> Result<Vec<KeyValue>, String> {
|
||||
list_key_values_raw(&w).await.map_err(|e| e.to_string())
|
||||
async fn cmd_list_key_values(window: WebviewWindow) -> Result<Vec<KeyValue>, String> {
|
||||
list_key_values_raw(&window).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_get_environment(id: &str, w: WebviewWindow) -> Result<Environment, String> {
|
||||
get_environment(&w, id).await.map_err(|e| e.to_string())
|
||||
async fn cmd_get_environment(id: &str, window: WebviewWindow) -> Result<Environment, String> {
|
||||
get_environment(&window, id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_get_workspace(id: &str, w: WebviewWindow) -> Result<Workspace, String> {
|
||||
get_workspace(&w, id).await.map_err(|e| e.to_string())
|
||||
async fn cmd_get_workspace(id: &str, window: WebviewWindow) -> Result<Workspace, String> {
|
||||
get_workspace(&window, id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_list_http_responses(
|
||||
workspace_id: &str,
|
||||
limit: Option<i64>,
|
||||
w: WebviewWindow,
|
||||
window: WebviewWindow,
|
||||
) -> Result<Vec<HttpResponse>, String> {
|
||||
list_http_responses_for_workspace(&w, workspace_id, limit).await.map_err(|e| e.to_string())
|
||||
list_http_responses_for_workspace(&window, workspace_id, limit).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_delete_http_response(id: &str, w: WebviewWindow) -> Result<HttpResponse, String> {
|
||||
delete_http_response(&w, id, &UpdateSource::Window).await.map_err(|e| e.to_string())
|
||||
async fn cmd_delete_http_response(id: &str, window: WebviewWindow) -> Result<HttpResponse, String> {
|
||||
delete_http_response(&window, id, &UpdateSource::Window).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_delete_grpc_connection(id: &str, w: WebviewWindow) -> Result<GrpcConnection, String> {
|
||||
delete_grpc_connection(&w, id, &UpdateSource::Window).await.map_err(|e| e.to_string())
|
||||
async fn cmd_delete_grpc_connection(
|
||||
id: &str,
|
||||
window: WebviewWindow,
|
||||
) -> Result<GrpcConnection, String> {
|
||||
delete_grpc_connection(&window, id, &UpdateSource::Window).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_delete_all_grpc_connections(request_id: &str, w: WebviewWindow) -> Result<(), String> {
|
||||
delete_all_grpc_connections(&w, request_id, &UpdateSource::Window)
|
||||
async fn cmd_delete_all_grpc_connections(
|
||||
request_id: &str,
|
||||
window: WebviewWindow,
|
||||
) -> Result<(), String> {
|
||||
delete_all_grpc_connections(&window, request_id, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_delete_send_history(workspace_id: &str, w: WebviewWindow) -> Result<(), String> {
|
||||
delete_all_http_responses_for_workspace(&w, workspace_id, &UpdateSource::Window)
|
||||
async fn cmd_delete_send_history(workspace_id: &str, window: WebviewWindow) -> Result<(), String> {
|
||||
delete_all_http_responses_for_workspace(&window, workspace_id, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
delete_all_grpc_connections_for_workspace(&w, workspace_id, &UpdateSource::Window)
|
||||
delete_all_grpc_connections_for_workspace(&window, workspace_id, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_delete_all_http_responses(request_id: &str, w: WebviewWindow) -> Result<(), String> {
|
||||
delete_all_http_responses_for_request(&w, request_id, &UpdateSource::Window)
|
||||
async fn cmd_delete_all_http_responses(
|
||||
request_id: &str,
|
||||
window: WebviewWindow,
|
||||
) -> Result<(), String> {
|
||||
delete_all_http_responses_for_request(&window, request_id, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_list_workspaces(w: WebviewWindow) -> Result<Vec<Workspace>, String> {
|
||||
let workspaces = list_workspaces(&w).await.expect("Failed to find workspaces");
|
||||
async fn cmd_list_workspaces(window: WebviewWindow) -> Result<Vec<Workspace>, String> {
|
||||
let workspaces = list_workspaces(&window).await.expect("Failed to find workspaces");
|
||||
if workspaces.is_empty() {
|
||||
let workspace = upsert_workspace(
|
||||
&w,
|
||||
&window,
|
||||
Workspace {
|
||||
name: "Yaak".to_string(),
|
||||
setting_follow_redirects: true,
|
||||
@@ -1577,6 +1596,17 @@ async fn cmd_list_workspaces(w: WebviewWindow) -> Result<Vec<Workspace>, String>
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_get_workspace_meta(
|
||||
window: WebviewWindow,
|
||||
workspace_id: &str,
|
||||
) -> Result<WorkspaceMeta, String> {
|
||||
let workspace = get_workspace(&window, workspace_id).await.map_err(|e| e.to_string())?;
|
||||
get_or_create_workspace_meta(&window, &workspace, &UpdateSource::Window)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_new_child_window(
|
||||
parent_window: WebviewWindow,
|
||||
@@ -1800,6 +1830,7 @@ pub fn run() {
|
||||
cmd_get_settings,
|
||||
cmd_get_sse_events,
|
||||
cmd_get_workspace,
|
||||
cmd_get_workspace_meta,
|
||||
cmd_grpc_go,
|
||||
cmd_grpc_reflect,
|
||||
cmd_http_request_actions,
|
||||
@@ -1839,6 +1870,7 @@ pub fn run() {
|
||||
cmd_update_http_request,
|
||||
cmd_update_settings,
|
||||
cmd_update_workspace,
|
||||
cmd_update_workspace_meta,
|
||||
cmd_write_file_dev,
|
||||
])
|
||||
.register_uri_scheme_protocol("yaak", |_app, _req| {
|
||||
|
||||
@@ -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 AnyModel = CookieJar | Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | Plugin | Settings | KeyValue | Workspace;
|
||||
export type AnyModel = CookieJar | Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | Plugin | Settings | KeyValue | Workspace | WorkspaceMeta;
|
||||
|
||||
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
|
||||
|
||||
@@ -54,8 +54,12 @@ export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
|
||||
export type SyncHistory = { model: "sync_history", id: string, workspaceId: string, createdAt: string, states: Array<SyncState>, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
export type UpdateSource = "sync" | "window" | "plugin" | "background";
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingSyncDir: string | null, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, settingSyncDir: string | null, };
|
||||
|
||||
@@ -165,7 +165,6 @@ pub struct Workspace {
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_follow_redirects: bool,
|
||||
pub setting_request_timeout: i32,
|
||||
pub setting_sync_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
@@ -181,7 +180,6 @@ pub enum WorkspaceIden {
|
||||
Name,
|
||||
SettingFollowRedirects,
|
||||
SettingRequestTimeout,
|
||||
SettingSyncDir,
|
||||
SettingValidateCertificates,
|
||||
}
|
||||
|
||||
@@ -198,7 +196,6 @@ impl<'s> TryFrom<&Row<'s>> for Workspace {
|
||||
description: r.get("description")?,
|
||||
setting_follow_redirects: r.get("setting_follow_redirects")?,
|
||||
setting_request_timeout: r.get("setting_request_timeout")?,
|
||||
setting_sync_dir: r.get("setting_sync_dir")?,
|
||||
setting_validate_certificates: r.get("setting_validate_certificates")?,
|
||||
})
|
||||
}
|
||||
@@ -216,6 +213,47 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
pub struct WorkspaceMeta {
|
||||
#[ts(type = "\"workspace_meta\"")]
|
||||
pub model: String,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub setting_sync_dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
pub enum WorkspaceMetaIden {
|
||||
#[iden = "workspace_metas"]
|
||||
Table,
|
||||
Model,
|
||||
Id,
|
||||
WorkspaceId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
|
||||
SettingSyncDir,
|
||||
}
|
||||
|
||||
impl<'s> TryFrom<&Row<'s>> for WorkspaceMeta {
|
||||
type Error = rusqlite::Error;
|
||||
|
||||
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
|
||||
Ok(WorkspaceMeta {
|
||||
id: r.get("id")?,
|
||||
workspace_id: r.get("workspace_id")?,
|
||||
model: r.get("model")?,
|
||||
created_at: r.get("created_at")?,
|
||||
updated_at: r.get("updated_at")?,
|
||||
setting_sync_dir: r.get("setting_sync_dir")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
enum CookieDomain {
|
||||
@@ -933,6 +971,22 @@ pub struct SyncState {
|
||||
pub sync_dir: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
pub struct SyncHistory {
|
||||
#[ts(type = "\"sync_history\"")]
|
||||
pub model: String,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
|
||||
pub states: Vec<SyncState>,
|
||||
pub checksum: String,
|
||||
pub rel_path: String,
|
||||
pub sync_dir: String,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
pub enum SyncStateIden {
|
||||
#[iden = "sync_states"]
|
||||
@@ -1030,6 +1084,7 @@ pub enum ModelType {
|
||||
TypeHttpResponse,
|
||||
TypePlugin,
|
||||
TypeWorkspace,
|
||||
TypeWorkspaceMeta,
|
||||
TypeSyncState,
|
||||
}
|
||||
|
||||
@@ -1046,6 +1101,7 @@ impl ModelType {
|
||||
ModelType::TypeHttpResponse => "rs",
|
||||
ModelType::TypePlugin => "pg",
|
||||
ModelType::TypeWorkspace => "wk",
|
||||
ModelType::TypeWorkspaceMeta => "wm",
|
||||
ModelType::TypeSyncState => "ss",
|
||||
}
|
||||
.to_string()
|
||||
@@ -1068,6 +1124,7 @@ pub enum AnyModel {
|
||||
Settings(Settings),
|
||||
KeyValue(KeyValue),
|
||||
Workspace(Workspace),
|
||||
WorkspaceMeta(WorkspaceMeta),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AnyModel {
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::models::{
|
||||
GrpcConnection, GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest,
|
||||
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
|
||||
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
|
||||
Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden,
|
||||
Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden, WorkspaceMeta,
|
||||
WorkspaceMetaIden,
|
||||
};
|
||||
use crate::plugin::SqliteConnection;
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -18,7 +19,7 @@ use sea_query::{Cond, Expr, OnConflict, Order, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -177,6 +178,18 @@ pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Wo
|
||||
Ok(items.map(|v| v.unwrap()).collect())
|
||||
}
|
||||
|
||||
pub async fn list_workspace_metas<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<WorkspaceMeta>> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
let (sql, params) = Query::select()
|
||||
.from(WorkspaceMetaIden::Table)
|
||||
.column(Asterisk)
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = db.prepare(sql.as_str())?;
|
||||
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
|
||||
Ok(items.map(|v| v.unwrap()).collect())
|
||||
}
|
||||
|
||||
pub async fn get_workspace<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Workspace> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
@@ -189,6 +202,54 @@ pub async fn get_workspace<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Resul
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
|
||||
}
|
||||
|
||||
pub async fn get_workspace_meta<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
workspace: &Workspace,
|
||||
) -> Result<Option<WorkspaceMeta>> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
let (sql, params) = Query::select()
|
||||
.from(WorkspaceMetaIden::Table)
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(WorkspaceMetaIden::WorkspaceId).eq(&workspace.id))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = db.prepare(sql.as_str())?;
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
|
||||
}
|
||||
|
||||
pub async fn get_or_create_workspace_meta<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: &Workspace,
|
||||
update_source: &UpdateSource,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
let workspace_meta = get_workspace_meta(window, workspace).await?;
|
||||
if let Some(m) = workspace_meta {
|
||||
return Ok(m);
|
||||
}
|
||||
|
||||
upsert_workspace_meta(
|
||||
window,
|
||||
WorkspaceMeta {
|
||||
workspace_id: workspace.to_owned().id,
|
||||
..Default::default()
|
||||
},
|
||||
update_source,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn exists_workspace<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<bool> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
let (sql, params) = Query::select()
|
||||
.from(WorkspaceIden::Table)
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(WorkspaceIden::Id).eq(id))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = db.prepare(sql.as_str())?;
|
||||
Ok(stmt.exists(&*params.as_params())?)
|
||||
}
|
||||
|
||||
pub async fn upsert_workspace<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: Workspace,
|
||||
@@ -200,56 +261,96 @@ pub async fn upsert_workspace<R: Runtime>(
|
||||
};
|
||||
let trimmed_name = workspace.name.trim();
|
||||
|
||||
let m: Workspace = {
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
let (sql, params) = Query::insert()
|
||||
.into_table(WorkspaceIden::Table)
|
||||
.columns([
|
||||
WorkspaceIden::Id,
|
||||
WorkspaceIden::CreatedAt,
|
||||
WorkspaceIden::UpdatedAt,
|
||||
WorkspaceIden::Name,
|
||||
WorkspaceIden::Description,
|
||||
WorkspaceIden::SettingFollowRedirects,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingSyncDir,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
])
|
||||
.values_panic([
|
||||
id.as_str().into(),
|
||||
CurrentTimestamp.into(),
|
||||
CurrentTimestamp.into(),
|
||||
trimmed_name.into(),
|
||||
workspace.description.into(),
|
||||
workspace.setting_follow_redirects.into(),
|
||||
workspace.setting_request_timeout.into(),
|
||||
workspace.setting_sync_dir.into(),
|
||||
workspace.setting_validate_certificates.into(),
|
||||
])
|
||||
.on_conflict(
|
||||
OnConflict::column(GrpcRequestIden::Id)
|
||||
.update_columns([
|
||||
WorkspaceIden::UpdatedAt,
|
||||
WorkspaceIden::Name,
|
||||
WorkspaceIden::Description,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingFollowRedirects,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingSyncDir,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.returning_all()
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let (sql, params) = Query::insert()
|
||||
.into_table(WorkspaceIden::Table)
|
||||
.columns([
|
||||
WorkspaceIden::Id,
|
||||
WorkspaceIden::CreatedAt,
|
||||
WorkspaceIden::UpdatedAt,
|
||||
WorkspaceIden::Name,
|
||||
WorkspaceIden::Description,
|
||||
WorkspaceIden::SettingFollowRedirects,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
])
|
||||
.values_panic([
|
||||
id.as_str().into(),
|
||||
CurrentTimestamp.into(),
|
||||
CurrentTimestamp.into(),
|
||||
trimmed_name.into(),
|
||||
workspace.description.into(),
|
||||
workspace.setting_follow_redirects.into(),
|
||||
workspace.setting_request_timeout.into(),
|
||||
workspace.setting_validate_certificates.into(),
|
||||
])
|
||||
.on_conflict(
|
||||
OnConflict::column(GrpcRequestIden::Id)
|
||||
.update_columns([
|
||||
WorkspaceIden::UpdatedAt,
|
||||
WorkspaceIden::Name,
|
||||
WorkspaceIden::Description,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingFollowRedirects,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.returning_all()
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
|
||||
let mut stmt = db.prepare(&sql)?;
|
||||
stmt.query_row(&*params.as_params(), |row| row.try_into())?
|
||||
};
|
||||
let mut stmt = db.prepare(&sql)?;
|
||||
let m: Workspace = stmt.query_row(&*params.as_params(), |row| row.try_into())?;
|
||||
emit_upserted_model(window, &AnyModel::Workspace(m.to_owned()), update_source);
|
||||
ensure_base_environment(window, &m.id).await?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
pub async fn upsert_workspace_meta<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace_meta: WorkspaceMeta,
|
||||
update_source: &UpdateSource,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
let id = match workspace_meta.id.as_str() {
|
||||
"" => generate_model_id(ModelType::TypeWorkspaceMeta),
|
||||
_ => workspace_meta.id.to_string(),
|
||||
};
|
||||
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
let (sql, params) = Query::insert()
|
||||
.into_table(WorkspaceMetaIden::Table)
|
||||
.columns([
|
||||
WorkspaceMetaIden::Id,
|
||||
WorkspaceMetaIden::WorkspaceId,
|
||||
WorkspaceMetaIden::CreatedAt,
|
||||
WorkspaceMetaIden::UpdatedAt,
|
||||
WorkspaceMetaIden::SettingSyncDir,
|
||||
])
|
||||
.values_panic([
|
||||
id.as_str().into(),
|
||||
workspace_meta.workspace_id.into(),
|
||||
CurrentTimestamp.into(),
|
||||
CurrentTimestamp.into(),
|
||||
workspace_meta.setting_sync_dir.into(),
|
||||
])
|
||||
.on_conflict(
|
||||
OnConflict::column(GrpcRequestIden::Id)
|
||||
.update_columns([
|
||||
WorkspaceMetaIden::UpdatedAt,
|
||||
WorkspaceMetaIden::SettingSyncDir,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.returning_all()
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
|
||||
let mut stmt = db.prepare(&sql)?;
|
||||
let m: WorkspaceMeta = stmt.query_row(&*params.as_params(), |row| row.try_into())?;
|
||||
emit_upserted_model(window, &AnyModel::WorkspaceMeta(m.to_owned()), update_source);
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
@@ -1788,7 +1889,7 @@ pub async fn get_sync_state_for_model<R: Runtime>(
|
||||
pub async fn list_sync_states_for_workspace<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
workspace_id: &str,
|
||||
sync_dir: PathBuf,
|
||||
sync_dir: &Path,
|
||||
) -> Result<Vec<SyncState>> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
@@ -22,4 +22,4 @@ export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id: string, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingSyncDir: string | null, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -20,4 +20,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingSyncDir: string | null, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const COMMANDS: &[&str] = &["calculate", "apply", "watch"];
|
||||
const COMMANDS: &[&str] = &["calculate", "calculate_fs", "apply", "watch"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS).build();
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
import { Channel, invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { Workspace } from '@yaakapp-internal/models';
|
||||
import { useEffect } from 'react';
|
||||
import { SyncOp } from './bindings/sync';
|
||||
import { WatchEvent, WatchResult } from './bindings/watch';
|
||||
|
||||
export async function calculateSync(workspace: Workspace) {
|
||||
if (!workspace.settingSyncDir) return;
|
||||
|
||||
export async function calculateSync(workspaceId: string, syncDir: string) {
|
||||
return invoke<SyncOp[]>('plugin:yaak-sync|calculate', {
|
||||
workspaceId: workspace.id,
|
||||
dir: workspace.settingSyncDir,
|
||||
workspaceId,
|
||||
syncDir,
|
||||
});
|
||||
}
|
||||
|
||||
export async function applySync(workspace: Workspace, syncOps: SyncOp[]) {
|
||||
if (!workspace.settingSyncDir) return;
|
||||
export async function calculateSyncFsOnly(dir: string) {
|
||||
return invoke<SyncOp[]>('plugin:yaak-sync|calculate_fs', { dir });
|
||||
}
|
||||
|
||||
export async function applySync(workspaceId: string, syncDir: string, syncOps: SyncOp[]) {
|
||||
return invoke<void>('plugin:yaak-sync|apply', {
|
||||
workspaceId: workspace.id,
|
||||
dir: workspace.settingSyncDir,
|
||||
workspaceId,
|
||||
syncDir,
|
||||
syncOps: syncOps,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWatchWorkspace(workspace: Workspace | null, callback: (e: WatchEvent) => void) {
|
||||
useEffect(() => {
|
||||
if (workspace == null) return;
|
||||
if (!workspace.settingSyncDir) return;
|
||||
export function watchWorkspaceFiles(
|
||||
workspaceId: string,
|
||||
syncDir: string,
|
||||
callback: (e: WatchEvent) => void,
|
||||
) {
|
||||
const channel = new Channel<WatchEvent>();
|
||||
channel.onmessage = callback;
|
||||
const promise = invoke<WatchResult>('plugin:yaak-sync|watch', {
|
||||
workspaceId,
|
||||
syncDir,
|
||||
channel,
|
||||
});
|
||||
|
||||
const channel = new Channel<WatchEvent>();
|
||||
channel.onmessage = callback;
|
||||
const promise = invoke<WatchResult>('plugin:yaak-sync|watch', {
|
||||
workspaceId: workspace.id,
|
||||
channel,
|
||||
});
|
||||
|
||||
return () => {
|
||||
promise
|
||||
.then(({ unlistenEvent }) => {
|
||||
console.log('Cancelling workspace watch', workspace.id, unlistenEvent);
|
||||
return emit(unlistenEvent);
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}, [workspace]);
|
||||
return () => {
|
||||
promise
|
||||
.then(({ unlistenEvent }) => {
|
||||
console.log('Cancelling workspace watch', workspaceId, unlistenEvent);
|
||||
return emit(unlistenEvent);
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-activate"
|
||||
description = "Enables the activate command without any pre-configured scope."
|
||||
commands.allow = ["activate"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-activate"
|
||||
description = "Denies the activate command without any pre-configured scope."
|
||||
commands.deny = ["activate"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-add"
|
||||
description = "Enables the add command without any pre-configured scope."
|
||||
commands.allow = ["add"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-add"
|
||||
description = "Denies the add command without any pre-configured scope."
|
||||
commands.deny = ["add"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-calculate-fs"
|
||||
description = "Enables the calculate_fs command without any pre-configured scope."
|
||||
commands.allow = ["calculate_fs"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-calculate-fs"
|
||||
description = "Denies the calculate_fs command without any pre-configured scope."
|
||||
commands.deny = ["calculate_fs"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check"
|
||||
description = "Enables the check command without any pre-configured scope."
|
||||
commands.allow = ["check"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check"
|
||||
description = "Denies the check command without any pre-configured scope."
|
||||
commands.deny = ["check"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-checkout"
|
||||
description = "Enables the checkout command without any pre-configured scope."
|
||||
commands.allow = ["checkout"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-checkout"
|
||||
description = "Denies the checkout command without any pre-configured scope."
|
||||
commands.deny = ["checkout"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-add"
|
||||
description = "Enables the cmd_add command without any pre-configured scope."
|
||||
commands.allow = ["cmd_add"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-add"
|
||||
description = "Denies the cmd_add command without any pre-configured scope."
|
||||
commands.deny = ["cmd_add"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-checkout"
|
||||
description = "Enables the cmd_checkout command without any pre-configured scope."
|
||||
commands.allow = ["cmd_checkout"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-checkout"
|
||||
description = "Denies the cmd_checkout command without any pre-configured scope."
|
||||
commands.deny = ["cmd_checkout"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-commit"
|
||||
description = "Enables the cmd_commit command without any pre-configured scope."
|
||||
commands.allow = ["cmd_commit"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-commit"
|
||||
description = "Denies the cmd_commit command without any pre-configured scope."
|
||||
commands.deny = ["cmd_commit"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-init"
|
||||
description = "Enables the cmd_init command without any pre-configured scope."
|
||||
commands.allow = ["cmd_init"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-init"
|
||||
description = "Denies the cmd_init command without any pre-configured scope."
|
||||
commands.deny = ["cmd_init"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-log"
|
||||
description = "Enables the cmd_log command without any pre-configured scope."
|
||||
commands.allow = ["cmd_log"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-log"
|
||||
description = "Denies the cmd_log command without any pre-configured scope."
|
||||
commands.deny = ["cmd_log"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cmd-status"
|
||||
description = "Enables the cmd_status command without any pre-configured scope."
|
||||
commands.allow = ["cmd_status"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cmd-status"
|
||||
description = "Denies the cmd_status command without any pre-configured scope."
|
||||
commands.deny = ["cmd_status"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-commit"
|
||||
description = "Enables the commit command without any pre-configured scope."
|
||||
commands.allow = ["commit"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-commit"
|
||||
description = "Denies the commit command without any pre-configured scope."
|
||||
commands.deny = ["commit"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-init"
|
||||
description = "Enables the init command without any pre-configured scope."
|
||||
commands.allow = ["init"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-init"
|
||||
description = "Denies the init command without any pre-configured scope."
|
||||
commands.deny = ["init"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-init-repo"
|
||||
description = "Enables the init_repo command without any pre-configured scope."
|
||||
commands.allow = ["init_repo"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-init-repo"
|
||||
description = "Denies the init_repo command without any pre-configured scope."
|
||||
commands.deny = ["init_repo"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-initialize"
|
||||
description = "Enables the initialize command without any pre-configured scope."
|
||||
commands.allow = ["initialize"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-initialize"
|
||||
description = "Denies the initialize command without any pre-configured scope."
|
||||
commands.deny = ["initialize"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-log"
|
||||
description = "Enables the log command without any pre-configured scope."
|
||||
commands.allow = ["log"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-log"
|
||||
description = "Denies the log command without any pre-configured scope."
|
||||
commands.deny = ["log"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-status"
|
||||
description = "Enables the status command without any pre-configured scope."
|
||||
commands.allow = ["status"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-status"
|
||||
description = "Denies the status command without any pre-configured scope."
|
||||
commands.deny = ["status"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-sync"
|
||||
description = "Enables the sync command without any pre-configured scope."
|
||||
commands.allow = ["sync"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-sync"
|
||||
description = "Denies the sync command without any pre-configured scope."
|
||||
commands.deny = ["sync"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-sync-fs"
|
||||
description = "Enables the sync_fs command without any pre-configured scope."
|
||||
commands.allow = ["sync_fs"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-sync-fs"
|
||||
description = "Denies the sync_fs command without any pre-configured scope."
|
||||
commands.deny = ["sync_fs"]
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-unstage"
|
||||
description = "Enables the unstage command without any pre-configured scope."
|
||||
commands.allow = ["unstage"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-unstage"
|
||||
description = "Denies the unstage command without any pre-configured scope."
|
||||
commands.deny = ["unstage"]
|
||||
@@ -3,6 +3,7 @@
|
||||
Default permissions for the plugin
|
||||
|
||||
- `allow-calculate`
|
||||
- `allow-calculate-fs`
|
||||
- `allow-apply`
|
||||
- `allow-watch`
|
||||
|
||||
@@ -15,58 +16,6 @@ Default permissions for the plugin
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-activate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the activate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-activate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the activate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-add`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the add command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-add`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the add command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -122,12 +71,12 @@ Denies the calculate command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-check`
|
||||
`yaak-sync:allow-calculate-fs`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the check command without any pre-configured scope.
|
||||
Enables the calculate_fs command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -135,428 +84,12 @@ Enables the check command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-check`
|
||||
`yaak-sync:deny-calculate-fs`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the check command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-checkout`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the checkout command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-checkout`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the checkout command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-add`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_add command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-add`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_add command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-checkout`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_checkout command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-checkout`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_checkout command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-commit`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_commit command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-commit`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_commit command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-log`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_log command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-log`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_log command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-cmd-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cmd_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-cmd-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cmd_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-commit`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the commit command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-commit`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the commit command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-init-repo`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the init_repo command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-init-repo`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the init_repo command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-initialize`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the initialize command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-initialize`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the initialize command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-log`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the log command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-log`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the log command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-sync`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the sync command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-sync`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the sync command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-sync-fs`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the sync_fs command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-sync-fs`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the sync_fs command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:allow-unstage`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the unstage command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-sync:deny-unstage`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the unstage command without any pre-configured scope.
|
||||
Denies the calculate_fs command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = [
|
||||
"allow-calculate",
|
||||
"allow-calculate-fs",
|
||||
"allow-apply",
|
||||
"allow-watch",
|
||||
]
|
||||
|
||||
@@ -294,26 +294,6 @@
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the apply command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -335,174 +315,14 @@
|
||||
"const": "deny-calculate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"description": "Enables the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check"
|
||||
"const": "allow-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"description": "Denies the calculate_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check"
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_add command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-add"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_checkout command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-checkout"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cmd_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cmd-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-commit"
|
||||
},
|
||||
{
|
||||
"description": "Denies the commit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-commit"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-init"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-init"
|
||||
},
|
||||
{
|
||||
"description": "Enables the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Denies the init_repo command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-init-repo"
|
||||
},
|
||||
{
|
||||
"description": "Enables the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Denies the initialize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-initialize"
|
||||
},
|
||||
{
|
||||
"description": "Enables the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-log"
|
||||
},
|
||||
{
|
||||
"description": "Denies the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-log"
|
||||
},
|
||||
{
|
||||
"description": "Enables the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-status"
|
||||
},
|
||||
{
|
||||
"description": "Denies the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-status"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-sync"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-sync"
|
||||
},
|
||||
{
|
||||
"description": "Enables the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Denies the sync_fs command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-sync-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-unstage"
|
||||
},
|
||||
{
|
||||
"description": "Denies the unstage command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-unstage"
|
||||
"const": "deny-calculate-fs"
|
||||
},
|
||||
{
|
||||
"description": "Enables the watch command without any pre-configured scope.",
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates, get_fs_candidates,
|
||||
get_workspace_sync_dir, SyncOp,
|
||||
SyncOp,
|
||||
};
|
||||
use crate::watch::{watch_directory, WatchEvent};
|
||||
use chrono::Utc;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{command, Listener, Runtime, WebviewWindow};
|
||||
use tokio::sync::watch;
|
||||
use ts_rs::TS;
|
||||
use yaak_models::queries::get_workspace;
|
||||
|
||||
#[command]
|
||||
pub async fn calculate<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
) -> Result<Vec<SyncOp>> {
|
||||
let workspace = get_workspace(&window, workspace_id).await?;
|
||||
let db_candidates = get_db_candidates(&window, &workspace).await?;
|
||||
let fs_candidates = get_fs_candidates(&workspace).await?;
|
||||
let db_candidates = get_db_candidates(&window, workspace_id, sync_dir).await?;
|
||||
let fs_candidates = get_fs_candidates(sync_dir)
|
||||
.await?
|
||||
.into_iter()
|
||||
// Strip out any non-workspace candidates
|
||||
.filter(|fs| fs.model.workspace_id() == workspace_id)
|
||||
.collect();
|
||||
Ok(compute_sync_ops(db_candidates, fs_candidates))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn calculate_fs(dir: &Path) -> Result<Vec<SyncOp>> {
|
||||
let db_candidates = Vec::new();
|
||||
let fs_candidates = get_fs_candidates(Path::new(&dir)).await?;
|
||||
Ok(compute_sync_ops(db_candidates, fs_candidates))
|
||||
}
|
||||
|
||||
@@ -28,11 +40,11 @@ pub async fn calculate<R: Runtime>(
|
||||
pub async fn apply<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
sync_ops: Vec<SyncOp>,
|
||||
sync_dir: &Path,
|
||||
workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
let workspace = get_workspace(&window, workspace_id).await?;
|
||||
let sync_state_ops = apply_sync_ops(&window, &workspace, sync_ops).await?;
|
||||
apply_sync_state_ops(&window, &workspace, sync_state_ops).await
|
||||
let sync_state_ops = apply_sync_ops(&window, &workspace_id, sync_dir, sync_ops).await?;
|
||||
apply_sync_state_ops(&window, workspace_id, sync_dir, sync_state_ops).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
@@ -45,11 +57,10 @@ pub(crate) struct WatchResult {
|
||||
#[command]
|
||||
pub async fn watch<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
sync_dir: &Path,
|
||||
workspace_id: &str,
|
||||
channel: Channel<WatchEvent>,
|
||||
) -> Result<WatchResult> {
|
||||
let workspace = get_workspace(&window, workspace_id).await?;
|
||||
let sync_dir = get_workspace_sync_dir(&workspace)?;
|
||||
let (cancel_tx, cancel_rx) = watch::channel(());
|
||||
|
||||
watch_directory(&sync_dir, channel, cancel_rx).await?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::commands::{apply, calculate, watch};
|
||||
use crate::commands::{apply, calculate, calculate_fs, watch};
|
||||
use tauri::{
|
||||
generate_handler,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
@@ -12,5 +12,7 @@ mod sync;
|
||||
mod watch;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-sync").invoke_handler(generate_handler![calculate, apply, watch]).build()
|
||||
Builder::new("yaak-sync")
|
||||
.invoke_handler(generate_handler![calculate, calculate_fs, apply, watch])
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ impl TryFrom<AnyModel> for SyncModel {
|
||||
AnyModel::GrpcRequest(m) => SyncModel::GrpcRequest(m),
|
||||
AnyModel::HttpRequest(m) => SyncModel::HttpRequest(m),
|
||||
AnyModel::Workspace(m) => SyncModel::Workspace(m),
|
||||
AnyModel::WorkspaceMeta(m) => return Err(UnknownModel(m.model)),
|
||||
AnyModel::CookieJar(m) => return Err(UnknownModel(m.model)),
|
||||
AnyModel::GrpcConnection(m) => return Err(UnknownModel(m.model)),
|
||||
AnyModel::GrpcEvent(m) => return Err(UnknownModel(m.model)),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error::Error::{InvalidSyncFile, WorkspaceSyncNotConfigured};
|
||||
use crate::error::Error::InvalidSyncFile;
|
||||
use crate::error::Result;
|
||||
use crate::models::SyncModel;
|
||||
use chrono::Utc;
|
||||
@@ -12,12 +12,11 @@ use tokio::fs;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use ts_rs::TS;
|
||||
use yaak_models::models::{SyncState, Workspace};
|
||||
use yaak_models::models::{SyncState, WorkspaceMeta};
|
||||
use yaak_models::queries::{
|
||||
delete_environment, delete_folder, delete_grpc_request, delete_http_request, delete_sync_state,
|
||||
delete_workspace, get_workspace_export_resources, list_sync_states_for_workspace,
|
||||
upsert_environment, upsert_folder, upsert_grpc_request, upsert_http_request, upsert_sync_state,
|
||||
upsert_workspace, UpdateSource,
|
||||
batch_upsert, delete_environment, delete_folder, delete_grpc_request, delete_http_request,
|
||||
delete_sync_state, delete_workspace, get_workspace_export_resources, get_workspace_meta,
|
||||
list_sync_states_for_workspace, upsert_sync_state, upsert_workspace_meta, UpdateSource,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
@@ -48,6 +47,19 @@ pub(crate) enum SyncOp {
|
||||
},
|
||||
}
|
||||
|
||||
impl SyncOp {
|
||||
fn workspace_id(&self) -> String {
|
||||
match self {
|
||||
SyncOp::FsCreate { model } => model.workspace_id(),
|
||||
SyncOp::FsUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::FsDelete { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::DbCreate { fs } => fs.model.workspace_id(),
|
||||
SyncOp::DbUpdate { state, .. } => state.workspace_id.clone(),
|
||||
SyncOp::DbDelete { model, .. } => model.workspace_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SyncOp {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
@@ -87,24 +99,23 @@ impl DbCandidate {
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
#[ts(export, export_to = "sync.ts")]
|
||||
pub(crate) struct FsCandidate {
|
||||
model: SyncModel,
|
||||
rel_path: PathBuf,
|
||||
checksum: String,
|
||||
pub(crate) model: SyncModel,
|
||||
pub(crate) rel_path: PathBuf,
|
||||
pub(crate) checksum: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn get_db_candidates<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
workspace: &Workspace,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
) -> Result<Vec<DbCandidate>> {
|
||||
let sync_dir = get_workspace_sync_dir(workspace)?;
|
||||
let models: HashMap<_, _> =
|
||||
workspace_models(mgr, workspace).await.into_iter().map(|m| (m.id(), m)).collect();
|
||||
let sync_states: HashMap<_, _> =
|
||||
list_sync_states_for_workspace(mgr, workspace.id.as_str(), sync_dir)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| (s.model_id.clone(), s))
|
||||
.collect();
|
||||
workspace_models(mgr, workspace_id).await.into_iter().map(|m| (m.id(), m)).collect();
|
||||
let sync_states: HashMap<_, _> = list_sync_states_for_workspace(mgr, workspace_id, sync_dir)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| (s.model_id.clone(), s))
|
||||
.collect();
|
||||
|
||||
// 1. Add candidates for models (created/modified/unmodified)
|
||||
let mut candidates: Vec<DbCandidate> = models
|
||||
@@ -139,14 +150,9 @@ pub(crate) async fn get_db_candidates<R: Runtime>(
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_fs_candidates(workspace: &Workspace) -> Result<Vec<FsCandidate>> {
|
||||
let dir = match workspace.setting_sync_dir.clone() {
|
||||
None => return Ok(Vec::new()),
|
||||
Some(d) => d,
|
||||
};
|
||||
|
||||
pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
|
||||
// Ensure the root directory exists
|
||||
fs::create_dir_all(dir.clone()).await?;
|
||||
fs::create_dir_all(dir).await?;
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
let mut entries = fs::read_dir(dir).await?;
|
||||
@@ -166,12 +172,6 @@ pub(crate) async fn get_fs_candidates(workspace: &Workspace) -> Result<Vec<FsCan
|
||||
}
|
||||
};
|
||||
|
||||
// Skip models belonging to different workspace
|
||||
if model.workspace_id() != workspace.id.as_str() {
|
||||
debug!("Skipping non-workspace file");
|
||||
continue;
|
||||
}
|
||||
|
||||
let rel_path = Path::new(&dir_entry.file_name()).to_path_buf();
|
||||
candidates.push(FsCandidate {
|
||||
rel_path,
|
||||
@@ -254,7 +254,7 @@ pub(crate) fn compute_sync_ops(
|
||||
}
|
||||
}
|
||||
(Some(DbCandidate::Added(model)), Some(_)) => {
|
||||
// This would be super rare, so let's follow the user's intention
|
||||
// This would be super rare (impossible?), so let's follow the user's intention
|
||||
SyncOp::FsCreate {
|
||||
model: model.to_owned(),
|
||||
}
|
||||
@@ -269,14 +269,17 @@ pub(crate) fn compute_sync_ops(
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn workspace_models<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
workspace: &Workspace,
|
||||
) -> Vec<SyncModel> {
|
||||
let workspace_id = workspace.id.as_str();
|
||||
async fn workspace_models<R: Runtime>(mgr: &impl Manager<R>, workspace_id: &str) -> Vec<SyncModel> {
|
||||
let resources = get_workspace_export_resources(mgr, vec![workspace_id]).await.resources;
|
||||
let workspace = resources.workspaces.iter().find(|w| w.id == workspace_id);
|
||||
|
||||
let workspace = match workspace {
|
||||
None => return Vec::new(),
|
||||
Some(w) => w,
|
||||
};
|
||||
|
||||
let mut sync_models = vec![SyncModel::Workspace(workspace.to_owned())];
|
||||
|
||||
for m in resources.environments {
|
||||
sync_models.push(SyncModel::Environment(m));
|
||||
}
|
||||
@@ -295,7 +298,8 @@ async fn workspace_models<R: Runtime>(
|
||||
|
||||
pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: &Workspace,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
sync_ops: Vec<SyncOp>,
|
||||
) -> Result<Vec<SyncStateOp>> {
|
||||
if sync_ops.is_empty() {
|
||||
@@ -307,10 +311,145 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
sync_ops.iter().map(|op| op.to_string()).collect::<Vec<String>>().join(", ")
|
||||
);
|
||||
let mut sync_state_ops = Vec::new();
|
||||
let mut workspaces_to_upsert = Vec::new();
|
||||
let mut environments_to_upsert = Vec::new();
|
||||
let mut folders_to_upsert = Vec::new();
|
||||
let mut http_requests_to_upsert = Vec::new();
|
||||
let mut grpc_requests_to_upsert = Vec::new();
|
||||
|
||||
for op in sync_ops {
|
||||
let op = apply_sync_op(window, workspace, &op).await?;
|
||||
sync_state_ops.push(op);
|
||||
// Only apply things if workspace ID matches
|
||||
if op.workspace_id() != workspace_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
sync_state_ops.push(match op {
|
||||
SyncOp::FsCreate { model } => {
|
||||
let rel_path = derive_model_filename(&model);
|
||||
let abs_path = sync_dir.join(rel_path.clone());
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
SyncStateOp::Create {
|
||||
model_id: model.id(),
|
||||
checksum,
|
||||
rel_path,
|
||||
}
|
||||
}
|
||||
SyncOp::FsUpdate { model, state } => {
|
||||
// Always write the existing path
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum,
|
||||
rel_path: rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::FsDelete {
|
||||
state,
|
||||
fs: fs_candidate,
|
||||
} => match fs_candidate {
|
||||
None => SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
},
|
||||
Some(_) => {
|
||||
// Always delete the existing path
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
fs::remove_file(&abs_path).await?;
|
||||
SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
}
|
||||
}
|
||||
},
|
||||
SyncOp::DbCreate { fs } => {
|
||||
let model_id = fs.model.id();
|
||||
|
||||
// Push updates to arrays so we can do them all in a single
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
};
|
||||
SyncStateOp::Create {
|
||||
model_id,
|
||||
checksum: fs.checksum.to_owned(),
|
||||
rel_path: fs.rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::DbUpdate { state, fs } => {
|
||||
// Push updates to arrays so we can do them all in a single
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
}
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum: fs.checksum.to_owned(),
|
||||
rel_path: fs.rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::DbDelete { model, state } => {
|
||||
delete_model(window, &model).await?;
|
||||
SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let upserted_models = batch_upsert(
|
||||
window,
|
||||
workspaces_to_upsert,
|
||||
environments_to_upsert,
|
||||
folders_to_upsert,
|
||||
http_requests_to_upsert,
|
||||
grpc_requests_to_upsert,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Ensure we creat WorkspaceMeta models for each new workspace, with the appropriate sync dir
|
||||
let sync_dir_string = sync_dir.to_string_lossy().to_string();
|
||||
for workspace in upserted_models.workspaces {
|
||||
let r = match get_workspace_meta(window, &workspace).await {
|
||||
Ok(Some(m)) => {
|
||||
if m.setting_sync_dir == Some(sync_dir_string.clone()) {
|
||||
// We don't need to update if unchanged
|
||||
continue;
|
||||
}
|
||||
let wm = WorkspaceMeta {
|
||||
setting_sync_dir: Some(sync_dir.to_string_lossy().to_string()),
|
||||
..m
|
||||
};
|
||||
upsert_workspace_meta(window, wm, &UpdateSource::Sync).await
|
||||
}
|
||||
Ok(None) => {
|
||||
let wm = WorkspaceMeta {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
setting_sync_dir: Some(sync_dir.to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
upsert_workspace_meta(window, wm, &UpdateSource::Sync).await
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
if let Err(e) = r {
|
||||
warn!("Failed to upsert workspace meta for synced workspace {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sync_state_ops)
|
||||
}
|
||||
|
||||
@@ -331,178 +470,55 @@ pub(crate) enum SyncStateOp {
|
||||
},
|
||||
}
|
||||
|
||||
/// Flush a DB model to the filesystem
|
||||
async fn apply_sync_op<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: &Workspace,
|
||||
op: &SyncOp,
|
||||
) -> Result<SyncStateOp> {
|
||||
let sync_state_op = match op {
|
||||
SyncOp::FsCreate { model } => {
|
||||
let rel_path = derive_model_filename(&model);
|
||||
let abs_path = derive_full_model_path(workspace, &model)?;
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
SyncStateOp::Create {
|
||||
model_id: model.id(),
|
||||
checksum,
|
||||
rel_path,
|
||||
}
|
||||
}
|
||||
SyncOp::FsUpdate { model, state } => {
|
||||
// Always write the existing path
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
let (content, checksum) = model.to_file_contents(&rel_path)?;
|
||||
let mut f = File::create(&abs_path).await?;
|
||||
f.write_all(&content).await?;
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum,
|
||||
rel_path: rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::FsDelete {
|
||||
state,
|
||||
fs: fs_candidate,
|
||||
} => match fs_candidate {
|
||||
None => SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
},
|
||||
Some(_) => {
|
||||
// Always delete the existing path
|
||||
let rel_path = Path::new(&state.rel_path);
|
||||
let abs_path = Path::new(&state.sync_dir).join(&rel_path);
|
||||
fs::remove_file(&abs_path).await?;
|
||||
SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
}
|
||||
}
|
||||
},
|
||||
SyncOp::DbCreate { fs } => {
|
||||
upsert_model(window, &fs.model).await?;
|
||||
SyncStateOp::Create {
|
||||
model_id: fs.model.id(),
|
||||
checksum: fs.checksum.to_owned(),
|
||||
rel_path: fs.rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::DbUpdate { state, fs } => {
|
||||
upsert_model(window, &fs.model).await?;
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
checksum: fs.checksum.to_owned(),
|
||||
rel_path: fs.rel_path.to_owned(),
|
||||
}
|
||||
}
|
||||
SyncOp::DbDelete { model, state } => {
|
||||
delete_model(window, model).await?;
|
||||
SyncStateOp::Delete {
|
||||
state: state.to_owned(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(sync_state_op)
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_sync_state_ops<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: &Workspace,
|
||||
workspace_id: &str,
|
||||
sync_dir: &Path,
|
||||
ops: Vec<SyncStateOp>,
|
||||
) -> Result<()> {
|
||||
for op in ops {
|
||||
apply_sync_state_op(window, workspace, op).await?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_sync_state_op<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
workspace: &Workspace,
|
||||
op: SyncStateOp,
|
||||
) -> Result<()> {
|
||||
let dir_path = get_workspace_sync_dir(workspace)?;
|
||||
match op {
|
||||
SyncStateOp::Create {
|
||||
checksum,
|
||||
rel_path,
|
||||
model_id,
|
||||
} => {
|
||||
let sync_state = SyncState {
|
||||
workspace_id: workspace.to_owned().id,
|
||||
match op {
|
||||
SyncStateOp::Create {
|
||||
checksum,
|
||||
rel_path,
|
||||
model_id,
|
||||
} => {
|
||||
let sync_state = SyncState {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
model_id,
|
||||
checksum,
|
||||
sync_dir: sync_dir.to_str().unwrap().to_string(),
|
||||
rel_path: rel_path.to_str().unwrap().to_string(),
|
||||
flushed_at: Utc::now().naive_utc(),
|
||||
..Default::default()
|
||||
};
|
||||
upsert_sync_state(window, sync_state).await?;
|
||||
}
|
||||
SyncStateOp::Update {
|
||||
state: sync_state,
|
||||
checksum,
|
||||
sync_dir: dir_path.to_str().unwrap().to_string(),
|
||||
rel_path: rel_path.to_str().unwrap().to_string(),
|
||||
flushed_at: Utc::now().naive_utc(),
|
||||
..Default::default()
|
||||
};
|
||||
upsert_sync_state(window, sync_state).await?;
|
||||
}
|
||||
SyncStateOp::Update {
|
||||
state: sync_state,
|
||||
checksum,
|
||||
rel_path,
|
||||
} => {
|
||||
let sync_state = SyncState {
|
||||
checksum,
|
||||
sync_dir: dir_path.to_str().unwrap().to_string(),
|
||||
rel_path: rel_path.to_str().unwrap().to_string(),
|
||||
flushed_at: Utc::now().naive_utc(),
|
||||
..sync_state
|
||||
};
|
||||
upsert_sync_state(window, sync_state).await?;
|
||||
}
|
||||
SyncStateOp::Delete { state } => {
|
||||
delete_sync_state(window, state.id.as_str()).await?;
|
||||
rel_path,
|
||||
} => {
|
||||
let sync_state = SyncState {
|
||||
checksum,
|
||||
sync_dir: sync_dir.to_str().unwrap().to_string(),
|
||||
rel_path: rel_path.to_str().unwrap().to_string(),
|
||||
flushed_at: Utc::now().naive_utc(),
|
||||
..sync_state
|
||||
};
|
||||
upsert_sync_state(window, sync_state).await?;
|
||||
}
|
||||
SyncStateOp::Delete { state } => {
|
||||
delete_sync_state(window, state.id.as_str()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_workspace_sync_dir(workspace: &Workspace) -> Result<PathBuf> {
|
||||
let workspace_id = workspace.to_owned().id;
|
||||
match workspace.setting_sync_dir.to_owned() {
|
||||
Some(d) => Ok(Path::new(&d).to_path_buf()),
|
||||
None => Err(WorkspaceSyncNotConfigured(workspace_id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_full_model_path(workspace: &Workspace, m: &SyncModel) -> Result<PathBuf> {
|
||||
let dir = get_workspace_sync_dir(workspace)?;
|
||||
Ok(dir.join(derive_model_filename(m)))
|
||||
}
|
||||
|
||||
fn derive_model_filename(m: &SyncModel) -> PathBuf {
|
||||
let rel = format!("yaak.2.{}.yaml", m.id());
|
||||
let rel = Path::new(&rel).to_path_buf();
|
||||
|
||||
// Ensure parent dir exists
|
||||
rel
|
||||
}
|
||||
|
||||
async fn upsert_model<R: Runtime>(window: &WebviewWindow<R>, m: &SyncModel) -> Result<()> {
|
||||
match m {
|
||||
SyncModel::Workspace(m) => {
|
||||
upsert_workspace(window, m.to_owned(), &UpdateSource::Sync).await?;
|
||||
}
|
||||
SyncModel::Environment(m) => {
|
||||
upsert_environment(window, m.to_owned(), &UpdateSource::Sync).await?;
|
||||
}
|
||||
SyncModel::Folder(m) => {
|
||||
upsert_folder(window, m.to_owned(), &UpdateSource::Sync).await?;
|
||||
}
|
||||
SyncModel::HttpRequest(m) => {
|
||||
upsert_http_request(window, m.to_owned(), &UpdateSource::Sync).await?;
|
||||
}
|
||||
SyncModel::GrpcRequest(m) => {
|
||||
upsert_grpc_request(window, m.to_owned(), &UpdateSource::Sync).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
let rel = format!("yaak.{}.yaml", m.id());
|
||||
Path::new(&rel).to_path_buf()
|
||||
}
|
||||
|
||||
async fn delete_model<R: Runtime>(window: &WebviewWindow<R>, model: &SyncModel) -> Result<()> {
|
||||
|
||||
150
src-web/commands/commands.tsx
Normal file
150
src-web/commands/commands.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { Folder, Workspace } from '@yaakapp-internal/models';
|
||||
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
||||
import { Banner } from '../components/core/Banner';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { VStack } from '../components/core/Stacks';
|
||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirm } from '../lib/confirm';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { router } from '../lib/router';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export const createWorkspace = createFastMutation<Workspace, void, Partial<Workspace>>({
|
||||
mutationKey: ['create_workspace'],
|
||||
mutationFn: (patch) => invokeCmd<Workspace>('cmd_update_workspace', { workspace: patch }),
|
||||
onSuccess: async (workspace) => {
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
params: { workspaceId: workspace.id },
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
});
|
||||
|
||||
export const createFolder = createFastMutation<
|
||||
Folder | null,
|
||||
void,
|
||||
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
|
||||
>({
|
||||
mutationKey: ['create_folder'],
|
||||
mutationFn: async (patch) => {
|
||||
const workspaceId = getActiveWorkspaceId();
|
||||
if (workspaceId == null) {
|
||||
throw new Error("Cannot create folder when there's no active workspace");
|
||||
}
|
||||
|
||||
if (!patch.name) {
|
||||
const name = await showPrompt({
|
||||
id: 'new-folder',
|
||||
label: 'Name',
|
||||
defaultValue: 'Folder',
|
||||
title: 'New Folder',
|
||||
confirmText: 'Create',
|
||||
placeholder: 'Name',
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
patch.name = name;
|
||||
}
|
||||
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
});
|
||||
|
||||
export const syncWorkspace = createFastMutation<
|
||||
void,
|
||||
void,
|
||||
{ workspaceId: string; syncDir: string }
|
||||
>({
|
||||
mutationKey: [],
|
||||
mutationFn: async ({ workspaceId, syncDir }) => {
|
||||
const ops = (await calculateSync(workspaceId, syncDir)) ?? [];
|
||||
console.log('SYNCING WORKSPACE', ops);
|
||||
if (ops.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dbOps = ops.filter((o) => o.type.startsWith('db'));
|
||||
|
||||
if (dbOps.length === 0) {
|
||||
await applySync(workspaceId, syncDir, ops);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeletingWorkspace = ops.some(
|
||||
(o) => o.type === 'dbDelete' && o.model.model === 'workspace',
|
||||
);
|
||||
|
||||
console.log('Filesystem changes detected', { dbOps, ops });
|
||||
|
||||
const confirmed = await showConfirm({
|
||||
id: 'commit-sync',
|
||||
title: 'Filesystem Changes Detected',
|
||||
confirmText: 'Apply Changes',
|
||||
description: (
|
||||
<VStack space={3}>
|
||||
{isDeletingWorkspace && (
|
||||
<Banner color="danger">
|
||||
🚨 <strong>Changes contain a workspace deletion!</strong>
|
||||
</Banner>
|
||||
)}
|
||||
<p>
|
||||
{pluralizeCount('file', dbOps.length)} in the directory have changed. Do you want to
|
||||
apply the updates to your workspace?
|
||||
</p>
|
||||
<div className="overflow-y-auto max-h-[10rem]">
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-1 text-left">Name</th>
|
||||
<th className="py-1 text-right pl-4">Operation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{dbOps.map((op, i) => {
|
||||
let name = '';
|
||||
let label = '';
|
||||
let color = '';
|
||||
|
||||
if (op.type === 'dbCreate') {
|
||||
label = 'create';
|
||||
name = fallbackRequestName(op.fs.model);
|
||||
color = 'text-success';
|
||||
} else if (op.type === 'dbUpdate') {
|
||||
label = 'update';
|
||||
name = fallbackRequestName(op.fs.model);
|
||||
color = 'text-info';
|
||||
} else if (op.type === 'dbDelete') {
|
||||
label = 'delete';
|
||||
name = fallbackRequestName(op.model);
|
||||
color = 'text-danger';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={i} className="text-text">
|
||||
<td className="py-1">{name}</td>
|
||||
<td className="py-1 pl-4 text-right">
|
||||
<InlineCode className={color}>{label}</InlineCode>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
await applySync(workspaceId, syncDir, ops);
|
||||
}
|
||||
},
|
||||
});
|
||||
35
src-web/commands/openWorkspace.tsx
Normal file
35
src-web/commands/openWorkspace.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { applySync, calculateSyncFsOnly } from '@yaakapp-internal/sync';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { showSimpleAlert } from '../lib/alert';
|
||||
import { router } from '../lib/router';
|
||||
|
||||
export const openWorkspace = createFastMutation({
|
||||
mutationKey: [],
|
||||
mutationFn: async () => {
|
||||
const dir = await open({
|
||||
title: 'Select Workspace Directory',
|
||||
directory: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
if (dir == null) return;
|
||||
|
||||
const ops = await calculateSyncFsOnly(dir);
|
||||
|
||||
const workspace = ops
|
||||
.map((o) => (o.type === 'dbCreate' && o.fs.model.type === 'workspace' ? o.fs.model : null))
|
||||
.filter((m) => m)[0];
|
||||
if (workspace == null) {
|
||||
showSimpleAlert('Failed to Open', 'No workspace found in directory');
|
||||
return;
|
||||
}
|
||||
|
||||
await applySync(workspace.id, dir, ops);
|
||||
|
||||
router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
params: { workspaceId: workspace.id },
|
||||
});
|
||||
},
|
||||
});
|
||||
19
src-web/commands/upsertWorkspaceMeta.ts
Normal file
19
src-web/commands/upsertWorkspaceMeta.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { WorkspaceMeta } from '@yaakapp-internal/models';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { workspaceMetaAtom } from '../hooks/useWorkspaceMeta';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export const upsertWorkspaceMeta = createFastMutation<
|
||||
WorkspaceMeta,
|
||||
unknown,
|
||||
Partial<WorkspaceMeta>
|
||||
>({
|
||||
mutationKey: ['update_workspace_meta'],
|
||||
mutationFn: async (patch) => {
|
||||
const workspaceMeta = jotaiStore.get(workspaceMetaAtom);
|
||||
return invokeCmd<WorkspaceMeta>('cmd_update_workspace_meta', {
|
||||
workspaceMeta: { ...workspaceMeta, ...patch },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import type { HotkeyAction } from '../hooks/useHotKey';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
|
||||
import { useOpenSettings } from '../hooks/useOpenSettings';
|
||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||
import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
@@ -26,7 +26,6 @@ import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
||||
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { createFolder } from '../lib/commands';
|
||||
import { showDialog, toggleDialog } from '../lib/dialog';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { router } from '../lib/router';
|
||||
@@ -39,6 +38,7 @@ import { Icon } from './core/Icon';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
import { createFolder } from '../commands/commands';
|
||||
|
||||
interface CommandPaletteGroup {
|
||||
key: string;
|
||||
@@ -71,7 +71,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
const [, setSidebarHidden] = useSidebarHidden();
|
||||
const { baseEnvironment } = useEnvironments();
|
||||
const { mutate: openSettings } = useOpenSettings();
|
||||
const { mutate: openWorkspace } = useOpenWorkspace();
|
||||
const { mutate: switchWorkspace } = useSwitchWorkspace();
|
||||
const { mutate: createHttpRequest } = useCreateHttpRequest();
|
||||
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
|
||||
const { mutate: createEnvironment } = useCreateEnvironment();
|
||||
@@ -315,7 +315,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
workspaceGroup.items.push({
|
||||
key: `switch-workspace-${w.id}`,
|
||||
label: w.name,
|
||||
onSelect: () => openWorkspace({ workspaceId: w.id, inNewWindow: false }),
|
||||
onSelect: () => switchWorkspace({ workspaceId: w.id, inNewWindow: false }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
activeEnvironment?.id,
|
||||
setActiveEnvironmentId,
|
||||
sortedWorkspaces,
|
||||
openWorkspace,
|
||||
switchWorkspace,
|
||||
]);
|
||||
|
||||
const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { createWorkspace } from '../lib/commands';
|
||||
import { createWorkspace } from '../commands/commands';
|
||||
import { Button } from './core/Button';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { VStack } from './core/Stacks';
|
||||
@@ -32,7 +32,11 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
||||
>
|
||||
<PlainInput require label="Workspace Name" defaultValue={name} onChange={setName} />
|
||||
|
||||
<SyncToFilesystemSetting onChange={setSettingSyncDir} value={settingSyncDir.value} />
|
||||
<SyncToFilesystemSetting
|
||||
onChange={setSettingSyncDir}
|
||||
value={settingSyncDir.value}
|
||||
allowNonEmptyDirectory // Will do initial import when the workspace is created
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
|
||||
import { useWatchWorkspace } from '@yaakapp-internal/sync';
|
||||
import type { ShowToastRequest } from '@yaakapp/api';
|
||||
import { useActiveWorkspace, useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
||||
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useNotificationToast } from '../hooks/useNotificationToast';
|
||||
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
|
||||
import { useSyncModelStores } from '../hooks/useSyncModelStores';
|
||||
import { useSyncWorkspace } from '../hooks/useSyncWorkspace';
|
||||
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
|
||||
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
|
||||
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
|
||||
@@ -31,12 +29,6 @@ export function GlobalHooks() {
|
||||
useNotificationToast();
|
||||
useActiveWorkspaceChangedToast();
|
||||
|
||||
// Trigger workspace sync operation when workspace files change
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const { debouncedSync } = useSyncWorkspace(activeWorkspace, { debounceMillis: 1000 });
|
||||
useListenToTauriEvent('upserted_model', debouncedSync);
|
||||
useWatchWorkspace(activeWorkspace, debouncedSync);
|
||||
|
||||
// Listen for toasts
|
||||
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
|
||||
showToast({ ...event.payload });
|
||||
|
||||
@@ -29,7 +29,7 @@ export function SelectFile({
|
||||
}: Props) {
|
||||
const handleClick = async () => {
|
||||
const filePath = await open({
|
||||
title: 'Select File',
|
||||
title: directory ? 'Select Folder' : 'Select File',
|
||||
multiple: false,
|
||||
directory,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SettingsGeneral() {
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
const appInfo = useAppInfo();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
return null;
|
||||
@@ -53,12 +53,12 @@ export function SettingsGeneral() {
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
name="openWorkspace"
|
||||
label="Open Workspace"
|
||||
name="switchWorkspaceBehavior"
|
||||
label="Switch Workspace Behavior"
|
||||
labelPosition="left"
|
||||
labelClassName="w-[12rem]"
|
||||
size="sm"
|
||||
event="workspace-open"
|
||||
event="workspace-switch-behavior"
|
||||
value={
|
||||
settings.openWorkspaceNewWindow === true
|
||||
? 'new'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||
import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
@@ -14,8 +14,8 @@ interface Props {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export function OpenWorkspaceDialog({ hide, workspace }: Props) {
|
||||
const openWorkspace = useOpenWorkspace();
|
||||
export function SwitchWorkspaceDialog({ hide, workspace }: Props) {
|
||||
const switchWorkspace = useSwitchWorkspace();
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
const [remember, setRemember] = useState<boolean>(false);
|
||||
@@ -31,7 +31,7 @@ export function OpenWorkspaceDialog({ hide, workspace }: Props) {
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
hide();
|
||||
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: false });
|
||||
switchWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: false });
|
||||
if (remember) {
|
||||
updateSettings.mutate({ openWorkspaceNewWindow: false });
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export function OpenWorkspaceDialog({ hide, workspace }: Props) {
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={() => {
|
||||
hide();
|
||||
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: true });
|
||||
switchWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: true });
|
||||
if (remember) {
|
||||
updateSettings.mutate({ openWorkspaceNewWindow: true });
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readDir } from '@tauri-apps/plugin-fs';
|
||||
import { useState } from 'react';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { VStack } from './core/Stacks';
|
||||
@@ -6,10 +7,16 @@ import { SelectFile } from './SelectFile';
|
||||
export interface SyncToFilesystemSettingProps {
|
||||
onChange: (args: { value: string | null; enabled: boolean }) => void;
|
||||
value: string | null;
|
||||
allowNonEmptyDirectory?: boolean;
|
||||
}
|
||||
|
||||
export function SyncToFilesystemSetting({ onChange, value }: SyncToFilesystemSettingProps) {
|
||||
export function SyncToFilesystemSetting({
|
||||
onChange,
|
||||
value,
|
||||
allowNonEmptyDirectory,
|
||||
}: SyncToFilesystemSettingProps) {
|
||||
const [useSyncDir, setUseSyncDir] = useState<boolean>(!!value);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="w-full">
|
||||
@@ -26,19 +33,28 @@ export function SyncToFilesystemSetting({ onChange, value }: SyncToFilesystemSet
|
||||
}}
|
||||
title="Sync to a filesystem directory"
|
||||
/>
|
||||
{error && <div className="text-danger">{error}</div>}
|
||||
{useSyncDir && (
|
||||
<>
|
||||
<SelectFile
|
||||
directory
|
||||
size="xs"
|
||||
noun="Directory"
|
||||
filePath={value}
|
||||
onChange={({ filePath }) => {
|
||||
if (filePath == null) setUseSyncDir(false);
|
||||
onChange({ value: filePath, enabled: useSyncDir });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<SelectFile
|
||||
directory
|
||||
size="xs"
|
||||
noun="Directory"
|
||||
filePath={value}
|
||||
onChange={async ({ filePath }) => {
|
||||
setError(null);
|
||||
if (filePath == null) {
|
||||
setUseSyncDir(false);
|
||||
} else {
|
||||
const files = await readDir(filePath);
|
||||
if (files.length > 0 && !allowNonEmptyDirectory) {
|
||||
setError('Directory must be empty');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onChange({ value: filePath, enabled: useSyncDir });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import {openWorkspace} from "../commands/openWorkspace";
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
|
||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||
import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
@@ -14,7 +15,7 @@ import type { DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import type { RadioDropdownItem } from './core/RadioDropdown';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
|
||||
import { SwitchWorkspaceDialog } from './SwitchWorkspaceDialog';
|
||||
import { WorkspaceSettingsDialog } from './WorkspaceSettingsDialog';
|
||||
|
||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
||||
@@ -28,7 +29,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const createWorkspace = useCreateWorkspace();
|
||||
const { mutate: deleteSendHistory } = useDeleteSendHistory();
|
||||
const settings = useSettings();
|
||||
const openWorkspace = useOpenWorkspace();
|
||||
const switchWorkspace = useSwitchWorkspace();
|
||||
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
|
||||
|
||||
const orderedWorkspaces = useMemo(
|
||||
@@ -77,6 +78,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: createWorkspace,
|
||||
},
|
||||
{
|
||||
key: 'open-workspace',
|
||||
label: 'Open Workspace',
|
||||
leftSlot: <Icon icon="folder" />,
|
||||
onSelect: openWorkspace.mutate,
|
||||
},
|
||||
];
|
||||
|
||||
return { workspaceItems, extraItems };
|
||||
@@ -87,7 +94,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
if (workspaceId == null) return;
|
||||
|
||||
if (typeof openWorkspaceNewWindow === 'boolean') {
|
||||
openWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow });
|
||||
switchWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,13 +102,13 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
if (workspace == null) return;
|
||||
|
||||
showDialog({
|
||||
id: 'open-workspace',
|
||||
id: 'switch-workspace',
|
||||
size: 'sm',
|
||||
title: 'Open Workspace',
|
||||
render: ({ hide }) => <OpenWorkspaceDialog workspace={workspace} hide={hide} />,
|
||||
title: 'Switch Workspace',
|
||||
render: ({ hide }) => <SwitchWorkspaceDialog workspace={workspace} hide={hide} />,
|
||||
});
|
||||
},
|
||||
[openWorkspace, openWorkspaceNewWindow],
|
||||
[switchWorkspace, openWorkspaceNewWindow],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { upsertWorkspaceMeta } from '../commands/upsertWorkspaceMeta';
|
||||
import { useDeleteActiveWorkspace } from '../hooks/useDeleteActiveWorkspace';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { useWorkspaceMeta } from '../hooks/useWorkspaceMeta';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { Input } from './core/Input';
|
||||
import { Separator } from './core/Separator';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
|
||||
@@ -15,10 +20,17 @@ interface Props {
|
||||
export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
||||
const workspaces = useWorkspaces();
|
||||
const workspace = workspaces.find((w) => w.id === workspaceId);
|
||||
const workspaceMeta = useWorkspaceMeta();
|
||||
const { mutate: updateWorkspace } = useUpdateWorkspace(workspaceId ?? null);
|
||||
const { mutateAsync: deleteActiveWorkspace } = useDeleteActiveWorkspace();
|
||||
|
||||
if (workspace == null) return null;
|
||||
if (workspaceMeta == null)
|
||||
return (
|
||||
<Banner color="danger">
|
||||
<InlineCode>WorkspaceMeta</InlineCode> not found for workspace
|
||||
</Banner>
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack space={3} alignItems="start" className="pb-3 h-full">
|
||||
@@ -39,21 +51,24 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
||||
heightMode="auto"
|
||||
/>
|
||||
|
||||
<VStack space={3} className="mt-3" alignItems="start">
|
||||
<VStack space={6} className="mt-3 w-full" alignItems="start">
|
||||
<SyncToFilesystemSetting
|
||||
value={workspace.settingSyncDir}
|
||||
value={workspaceMeta.settingSyncDir}
|
||||
onChange={({ value: settingSyncDir }) => {
|
||||
updateWorkspace({ settingSyncDir });
|
||||
upsertWorkspaceMeta.mutate({ settingSyncDir });
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await deleteActiveWorkspace();
|
||||
hide();
|
||||
const workspace = await deleteActiveWorkspace();
|
||||
if (workspace) {
|
||||
hide(); // Only hide if actually deleted workspace
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
variant="border"
|
||||
size="sm"
|
||||
size="xs"
|
||||
>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface DialogProps {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
disableBackdropClose?: boolean;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
className?: string;
|
||||
@@ -27,6 +28,7 @@ export function Dialog({
|
||||
size = 'full',
|
||||
open,
|
||||
onClose,
|
||||
disableBackdropClose,
|
||||
title,
|
||||
description,
|
||||
hideX,
|
||||
@@ -51,7 +53,7 @@ export function Dialog({
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay open={open} onClose={onClose} portalName="dialog">
|
||||
<Overlay open={open} onClose={disableBackdropClose ? undefined : onClose} portalName="dialog">
|
||||
<div
|
||||
role="dialog"
|
||||
className={classNames(
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
export function Separator({ className, dashed, orientation = 'horizontal', children }: Props) {
|
||||
return (
|
||||
<div role="separator" className={classNames(className, 'flex items-center')}>
|
||||
<div role="separator" className={classNames(className, 'flex items-center w-full')}>
|
||||
{children && (
|
||||
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { createFolder } from '../commands/commands';
|
||||
import type { DropdownItem } from '../components/core/Dropdown';
|
||||
import { Icon } from '../components/core/Icon';
|
||||
import { createFolder } from '../lib/commands';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
|
||||
import { getActiveRequest } from './useActiveRequest';
|
||||
|
||||
@@ -36,6 +36,8 @@ export function createFastMutation<TData = unknown, TError = unknown, TVariables
|
||||
} finally {
|
||||
onSettled?.();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const mutate = (
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import type {EventCallback, EventName} from '@tauri-apps/api/event';
|
||||
import {listen} from '@tauri-apps/api/event';
|
||||
import {getCurrentWebviewWindow} from '@tauri-apps/api/webviewWindow';
|
||||
import {useEffect} from 'react';
|
||||
import type { EventCallback, EventName } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* React hook to listen to a Tauri event.
|
||||
*/
|
||||
export function useListenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
||||
useEffect(() => {
|
||||
const unlisten = listen<T>(
|
||||
event,
|
||||
fn,
|
||||
// Listen to `emit_all()` events or events specific to the current window
|
||||
{ target: { label: getCurrentWebviewWindow().label, kind: 'Window' } },
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
}
|
||||
}, [event, fn]);
|
||||
useEffect(() => listenToTauriEvent(event, fn), [event, fn]);
|
||||
}
|
||||
|
||||
export function listenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
||||
const unlisten = listen<T>(
|
||||
event,
|
||||
fn,
|
||||
// Listen to `emit_all()` events or events specific to the current window
|
||||
{ target: { label: getCurrentWebviewWindow().label, kind: 'Window' } },
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getRecentCookieJars } from './useRecentCookieJars';
|
||||
import { getRecentEnvironments } from './useRecentEnvironments';
|
||||
import { getRecentRequests } from './useRecentRequests';
|
||||
|
||||
export function useOpenWorkspace() {
|
||||
export function useSwitchWorkspace() {
|
||||
return useFastMutation({
|
||||
mutationKey: ['open_workspace'],
|
||||
mutationFn: async ({
|
||||
@@ -19,6 +19,7 @@ import { useListenToTauriEvent } from './useListenToTauriEvent';
|
||||
import { pluginsAtom } from './usePlugins';
|
||||
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
||||
import { settingsAtom } from './useSettings';
|
||||
import { workspaceMetaAtom } from './useWorkspaceMeta';
|
||||
import { workspacesAtom } from './useWorkspaces';
|
||||
|
||||
export function useSyncModelStores() {
|
||||
@@ -51,6 +52,8 @@ export function useSyncModelStores() {
|
||||
|
||||
if (payload.model.model === 'workspace') {
|
||||
jotaiStore.set(workspacesAtom, updateModelList(payload.model));
|
||||
} else if (payload.model.model === 'workspace_meta') {
|
||||
jotaiStore.set(workspaceMetaAtom, payload.model);
|
||||
} else if (payload.model.model === 'plugin') {
|
||||
jotaiStore.set(pluginsAtom, updateModelList(payload.model));
|
||||
} else if (payload.model.model === 'http_request') {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { VStack } from '../components/core/Stacks';
|
||||
import {showConfirm} from "../lib/confirm";
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
|
||||
export function useSyncWorkspace(
|
||||
workspace: Workspace | null,
|
||||
{
|
||||
debounceMillis = 1000,
|
||||
}: {
|
||||
debounceMillis?: number;
|
||||
} = {},
|
||||
) {
|
||||
const sync = useCallback(async () => {
|
||||
if (workspace == null || !workspace.settingSyncDir) return;
|
||||
|
||||
const ops = await calculateSync(workspace) ?? [];
|
||||
if (ops.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dbChanges = ops.filter((o) => o.type.startsWith('db'));
|
||||
|
||||
if (dbChanges.length === 0) {
|
||||
await applySync(workspace, ops);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Filesystem changes detected", dbChanges);
|
||||
|
||||
const confirmed = await showConfirm({
|
||||
id: 'commit-sync',
|
||||
title: 'Filesystem Changes Detected',
|
||||
confirmText: 'Apply Changes',
|
||||
description: (
|
||||
<VStack space={3}>
|
||||
<p>
|
||||
{pluralizeCount('file', dbChanges.length)} in the directory have changed. Do you want to
|
||||
apply the updates to your workspace?
|
||||
</p>
|
||||
<div className="overflow-y-auto max-h-[10rem]">
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-1 text-left">Name</th>
|
||||
<th className="py-1 text-right pl-4">Operation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{dbChanges.map((op, i) => {
|
||||
let name = '';
|
||||
let label = '';
|
||||
let color = '';
|
||||
|
||||
if (op.type === 'dbCreate') {
|
||||
label = 'create';
|
||||
name = fallbackRequestName(op.fs.model);
|
||||
color = 'text-success';
|
||||
} else if (op.type === 'dbUpdate') {
|
||||
label = 'update';
|
||||
name = fallbackRequestName(op.fs.model);
|
||||
color = 'text-info';
|
||||
} else if (op.type === 'dbDelete') {
|
||||
label = 'delete';
|
||||
name = fallbackRequestName(op.model);
|
||||
color = 'text-danger';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={i} className="text-text">
|
||||
<td className="py-1">{name}</td>
|
||||
<td className="py-1 pl-4 text-right">
|
||||
<InlineCode className={color}>{label}</InlineCode>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await applySync(workspace, ops);
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
const debouncedSync = useMemo(() => {
|
||||
return debounce(sync, debounceMillis);
|
||||
}, [debounceMillis, sync]);
|
||||
|
||||
return { sync, debouncedSync };
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { WorkspaceMeta } from '@yaakapp-internal/models';
|
||||
import { useEffect } from 'react';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
@@ -10,6 +11,7 @@ import { grpcRequestsAtom } from './useGrpcRequests';
|
||||
import { httpRequestsAtom } from './useHttpRequests';
|
||||
import { httpResponsesAtom } from './useHttpResponses';
|
||||
import { keyValuesAtom } from './useKeyValue';
|
||||
import { workspaceMetaAtom } from './useWorkspaceMeta';
|
||||
|
||||
export function useSyncWorkspaceChildModels() {
|
||||
useEffect(() => {
|
||||
@@ -39,4 +41,7 @@ async function sync() {
|
||||
jotaiStore.set(httpResponsesAtom, await invokeCmd('cmd_list_http_responses', args));
|
||||
jotaiStore.set(grpcConnectionsAtom, await invokeCmd('cmd_list_grpc_connections', args));
|
||||
jotaiStore.set(environmentsAtom, await invokeCmd('cmd_list_environments', args));
|
||||
|
||||
// Single models
|
||||
jotaiStore.set(workspaceMetaAtom, await invokeCmd<WorkspaceMeta>('cmd_get_workspace_meta', args));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useFastMutation } from './useFastMutation';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai/index";
|
||||
import { getWorkspace } from '../lib/store';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {updateModelList} from "./useSyncModelStores";
|
||||
import {workspacesAtom} from "./useWorkspaces";
|
||||
import { useFastMutation } from './useFastMutation';
|
||||
|
||||
export function useUpdateWorkspace(id: string | null) {
|
||||
const setWorkspaces = useSetAtom(workspacesAtom);
|
||||
return useFastMutation<Workspace, unknown, Partial<Workspace> | ((w: Workspace) => Workspace)>({
|
||||
mutationKey: ['update_workspace', id],
|
||||
mutationFn: async (v) => {
|
||||
@@ -19,8 +15,5 @@ export function useUpdateWorkspace(id: string | null) {
|
||||
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
|
||||
return invokeCmd('cmd_update_workspace', { workspace: newWorkspace });
|
||||
},
|
||||
onSuccess: async (workspace) => {
|
||||
setWorkspaces(updateModelList(workspace));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
13
src-web/hooks/useWorkspaceMeta.ts
Normal file
13
src-web/hooks/useWorkspaceMeta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { WorkspaceMeta } from '@yaakapp-internal/models';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
export const workspaceMetaAtom = atom<WorkspaceMeta>();
|
||||
|
||||
export function useWorkspaceMeta() {
|
||||
const workspaceMeta = useAtomValue(workspaceMetaAtom);
|
||||
if (!workspaceMeta) {
|
||||
throw new Error('WorkspaceMeta not found');
|
||||
}
|
||||
|
||||
return workspaceMeta;
|
||||
}
|
||||
55
src-web/init/sync.ts
Normal file
55
src-web/init/sync.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { debounce } from '@yaakapp-internal/lib';
|
||||
import type { AnyModel, ModelPayload } from '@yaakapp-internal/models';
|
||||
import { watchWorkspaceFiles } from '@yaakapp-internal/sync';
|
||||
import { syncWorkspace } from '../commands/commands';
|
||||
import { listenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { workspaceMetaAtom } from '../hooks/useWorkspaceMeta';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
|
||||
export function initSync() {
|
||||
let unsub: (() => void) | undefined;
|
||||
jotaiStore.sub(workspaceMetaAtom, () => {
|
||||
unsub?.(); // Unsub from any previous watcher
|
||||
const workspaceMeta = jotaiStore.get(workspaceMetaAtom);
|
||||
if (workspaceMeta == null) return;
|
||||
unsub = initForWorkspace(workspaceMeta.workspaceId, workspaceMeta.settingSyncDir);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: This list should be derived from something, because we might forget something here
|
||||
const relevantModels: AnyModel['model'][] = [
|
||||
'workspace',
|
||||
'folder',
|
||||
'environment',
|
||||
'http_request',
|
||||
'grpc_request',
|
||||
];
|
||||
|
||||
function initForWorkspace(workspaceId: string, syncDir: string | null) {
|
||||
console.log('Initializing directory sync for', workspaceId, syncDir);
|
||||
|
||||
const debouncedSync = debounce(() => {
|
||||
if (syncDir == null) return;
|
||||
syncWorkspace.mutate({ workspaceId, syncDir });
|
||||
});
|
||||
|
||||
// Sync on model upsert
|
||||
listenToTauriEvent<ModelPayload>('upserted_model', (p) => {
|
||||
const isRelevant = relevantModels.includes(p.payload.model.model);
|
||||
if (isRelevant) debouncedSync();
|
||||
});
|
||||
|
||||
// Sync on model deletion
|
||||
listenToTauriEvent<ModelPayload>('deleted_model', (p) => {
|
||||
const isRelevant = relevantModels.includes(p.payload.model.model);
|
||||
if (isRelevant) debouncedSync();
|
||||
});
|
||||
|
||||
// Sync on sync dir changes
|
||||
if (syncDir != null) {
|
||||
return watchWorkspaceFiles(workspaceId, syncDir, debouncedSync);
|
||||
}
|
||||
|
||||
// Perform an initial sync operation
|
||||
debouncedSync();
|
||||
}
|
||||
@@ -16,6 +16,15 @@ export function showAlert({ id, title, body, size = 'sm' }: AlertArgs) {
|
||||
title,
|
||||
hideX: true,
|
||||
size,
|
||||
disableBackdropClose: true, // Prevent accidental dismisses
|
||||
render: ({ hide }) => Alert({ onHide: hide, body }),
|
||||
});
|
||||
}
|
||||
|
||||
export function showSimpleAlert(title: string, message: string) {
|
||||
showAlert({
|
||||
id: 'simple-alert',
|
||||
body: message,
|
||||
title: title,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Folder, Workspace } from '@yaakapp-internal/models';
|
||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from './analytics';
|
||||
import { showPrompt } from './prompt';
|
||||
import { router } from './router';
|
||||
import { invokeCmd } from './tauri';
|
||||
|
||||
export const createWorkspace = createFastMutation<Workspace, void, Partial<Workspace>>({
|
||||
mutationKey: ['create_workspace'],
|
||||
mutationFn: (patch) => invokeCmd<Workspace>('cmd_update_workspace', { workspace: patch }),
|
||||
onSuccess: async (workspace) => {
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
params: { workspaceId: workspace.id },
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
});
|
||||
|
||||
export const createFolder = createFastMutation<
|
||||
Folder | null,
|
||||
void,
|
||||
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
|
||||
>({
|
||||
mutationKey: ['create_folder'],
|
||||
mutationFn: async (patch) => {
|
||||
const workspaceId = getActiveWorkspaceId();
|
||||
if (workspaceId == null) {
|
||||
throw new Error("Cannot create folder when there's no active workspace");
|
||||
}
|
||||
|
||||
if (!patch.name) {
|
||||
const name = await showPrompt({
|
||||
id: 'new-folder',
|
||||
label: 'Name',
|
||||
defaultValue: 'Folder',
|
||||
title: 'New Folder',
|
||||
confirmText: 'Create',
|
||||
placeholder: 'Name',
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
patch.name = name;
|
||||
}
|
||||
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
});
|
||||
@@ -3,13 +3,10 @@ import { Confirm } from '../components/core/Confirm';
|
||||
import type { DialogProps } from '../components/core/Dialog';
|
||||
import { showDialog } from './dialog';
|
||||
|
||||
interface ConfirmArgs {
|
||||
type ConfirmArgs = {
|
||||
id: string;
|
||||
title: DialogProps['title'];
|
||||
description?: DialogProps['description'];
|
||||
variant?: ConfirmProps['variant'];
|
||||
confirmText?: ConfirmProps['confirmText'];
|
||||
}
|
||||
} & Pick<DialogProps, 'title' | 'description'> &
|
||||
Pick<ConfirmProps, 'variant' | 'confirmText'>;
|
||||
|
||||
export async function showConfirm({ id, title, description, variant, confirmText }: ConfirmArgs) {
|
||||
return new Promise((onResult: ConfirmProps['onResult']) => {
|
||||
@@ -19,6 +16,7 @@ export async function showConfirm({ id, title, description, variant, confirmText
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
disableBackdropClose: true, // Prevent accidental dismisses
|
||||
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function showPrompt({ id, title, description, ...props }: PromptArg
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
disableBackdropClose: true, // Prevent accidental dismisses
|
||||
onClose: () => {
|
||||
// Click backdrop, close, or escape
|
||||
resolve(null);
|
||||
|
||||
@@ -37,6 +37,7 @@ type TauriCmd =
|
||||
| 'cmd_get_key_value'
|
||||
| 'cmd_get_settings'
|
||||
| 'cmd_get_workspace'
|
||||
| 'cmd_get_workspace_meta'
|
||||
| 'cmd_grpc_go'
|
||||
| 'cmd_grpc_reflect'
|
||||
| 'cmd_http_request_actions'
|
||||
@@ -75,6 +76,7 @@ type TauriCmd =
|
||||
| 'cmd_update_http_request'
|
||||
| 'cmd_update_settings'
|
||||
| 'cmd_update_workspace'
|
||||
| 'cmd_update_workspace_meta'
|
||||
| 'cmd_write_file_dev';
|
||||
|
||||
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { initSync } from './init/sync';
|
||||
import { router } from './lib/router';
|
||||
|
||||
import('react-pdf').then(({ pdfjs }) => {
|
||||
@@ -36,6 +37,9 @@ window.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize a bunch of watchers
|
||||
initSync();
|
||||
|
||||
console.log('Creating React root');
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user