diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 244dab87..3d372db3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -42,7 +42,7 @@ use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http::send_http_request; -use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, Environment, EnvironmentVariable, Folder, 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_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, list_workspaces, set_key_value_raw, Settings, 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_workspace, Workspace, WorkspaceExportResources}; +use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, Environment, EnvironmentVariable, Folder, 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_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces, set_key_value_raw, Settings, 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_workspace, Workspace, WorkspaceExportResources}; use crate::plugin::ImportResult; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; @@ -783,9 +783,9 @@ async fn cmd_import_data( async fn cmd_export_data( app_handle: AppHandle, export_path: &str, - workspace_id: &str, + workspace_ids: Vec<&str>, ) -> Result<(), String> { - let export_data = get_workspace_export_resources(&app_handle, workspace_id).await; + let export_data = get_workspace_export_resources(&app_handle, workspace_ids).await; let f = File::options() .create(true) .truncate(true) @@ -1194,7 +1194,7 @@ async fn cmd_list_grpc_requests(workspace_id: &str, w: Window) -> Result Result, String> { - let requests = list_requests(&w, workspace_id) + let requests = list_http_requests(&w, workspace_id) .await .expect("Failed to find requests"); // .map_err(|e| e.to_string()) diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 39f6ca47..06288ab1 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -1115,7 +1115,7 @@ pub async fn upsert_http_request( } } -pub async fn list_requests( +pub async fn list_http_requests( mgr: &impl Manager, workspace_id: &str, ) -> Result, sqlx::Error> { @@ -1524,49 +1524,68 @@ pub fn generate_id(prefix: Option<&str>) -> String { #[derive(Default, Debug, Deserialize, Serialize)] #[serde(default, rename_all = "camelCase")] pub struct WorkspaceExport { - pub yaak_version: String, - pub yaak_schema: i64, - pub timestamp: NaiveDateTime, - pub resources: WorkspaceExportResources, + pub yaak_version: String, + pub yaak_schema: i64, + pub timestamp: NaiveDateTime, + pub resources: WorkspaceExportResources, } #[derive(Default, Debug, Deserialize, Serialize)] #[serde(default, rename_all = "camelCase")] pub struct WorkspaceExportResources { - pub workspaces: Vec, - pub environments: Vec, - pub folders: Vec, - pub http_requests: Vec, - pub grpc_requests: Vec, + pub workspaces: Vec, + pub environments: Vec, + pub folders: Vec, + pub http_requests: Vec, + pub grpc_requests: Vec, } pub async fn get_workspace_export_resources( app_handle: &AppHandle, - workspace_id: &str, + workspace_ids: Vec<&str>, ) -> WorkspaceExport { - let workspace = get_workspace(app_handle, workspace_id) - .await - .expect("Failed to get workspace"); - return WorkspaceExport { + let mut data = WorkspaceExport { yaak_version: app_handle.package_info().version.clone().to_string(), yaak_schema: 2, timestamp: chrono::Utc::now().naive_utc(), resources: WorkspaceExportResources { - workspaces: vec![workspace], - environments: list_environments(app_handle, workspace_id) - .await - .expect("Failed to get environments"), - folders: list_folders(app_handle, workspace_id) - .await - .expect("Failed to get folders"), - http_requests: list_requests(app_handle, workspace_id) - .await - .expect("Failed to get requests"), - grpc_requests: list_grpc_requests(app_handle, workspace_id) - .await - .expect("Failed to get grpc requests"), + workspaces: Vec::new(), + environments: Vec::new(), + folders: Vec::new(), + http_requests: Vec::new(), + grpc_requests: Vec::new(), }, }; + + for workspace_id in workspace_ids { + data.resources.workspaces.push( + get_workspace(app_handle, workspace_id) + .await + .expect("Failed to get workspace"), + ); + data.resources.environments.append( + &mut list_environments(app_handle, workspace_id) + .await + .expect("Failed to get environments"), + ); + data.resources.folders.append( + &mut list_folders(app_handle, workspace_id) + .await + .expect("Failed to get folders"), + ); + data.resources.http_requests.append( + &mut list_http_requests(app_handle, workspace_id) + .await + .expect("Failed to get http requests"), + ); + data.resources.grpc_requests.append( + &mut list_grpc_requests(app_handle, workspace_id) + .await + .expect("Failed to get grpc requests"), + ); + } + + return data; } fn emit_upserted_model(mgr: &impl Manager, model: S) -> S { diff --git a/src-web/components/ExportDataDialog.tsx b/src-web/components/ExportDataDialog.tsx new file mode 100644 index 00000000..4118ef25 --- /dev/null +++ b/src-web/components/ExportDataDialog.tsx @@ -0,0 +1,107 @@ +import { invoke } from '@tauri-apps/api'; +import { save } from '@tauri-apps/api/dialog'; +import { useState } from 'react'; +import slugify from 'slugify'; +import type { Workspace } from '../lib/models'; +import { count } from '../lib/pluralize'; +import { Button } from './core/Button'; +import { Checkbox } from './core/Checkbox'; +import { HStack, VStack } from './core/Stacks'; + +interface Props { + onHide: () => void; + activeWorkspace: Workspace; + workspaces: Workspace[]; +} + +export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorkspaces }: Props) { + const [selectedWorkspaces, setSelectedWorkspaces] = useState>({ + [activeWorkspace.id]: true, + }); + + const workspaces = [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)]; + + const handleToggleAll = () => { + setSelectedWorkspaces( + allSelected ? {} : workspaces.reduce((acc, w) => ({ ...acc, [w.id]: true }), {}), + ); + }; + + const handleExport = async () => { + const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]); + const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined; + const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces'; + const exportPath = await save({ + title: 'Export Data', + defaultPath: `yaak.${slug}.json`, + }); + if (exportPath == null) { + return; + } + + await invoke('cmd_export_data', { workspaceIds: ids, exportPath }); + onHide(); + }; + + const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]); + const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length; + const noneSelected = numSelected === 0; + return ( + + + + + + + + + + {workspaces.map((w) => ( + { + setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] })); + }} + > + + + + ))} + +
+ + Workspace
+ { + setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] })); + }} + /> + + {w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''} +
+ + + + +
+ ); +} diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index f0df347e..58ebbd5b 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -60,17 +60,27 @@ export const Button = forwardRef(function Button size === 'sm' && 'h-sm px-2.5 text-sm', size === 'xs' && 'h-xs px-2 text-sm', // Solids - variant === 'solid' && color === 'custom' && 'ring-blue-400', + variant === 'solid' && + color === 'custom' && + 'ring-blue-400 enabled:hocus:bg-highlightSecondary', variant === 'solid' && color === 'default' && 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400', variant === 'solid' && color === 'gray' && - 'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-blue-400', - variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700', - variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700', - variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700', - variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700', + 'text-gray-800 bg-gray-200/70 enabled:hocus:bg-gray-200 ring-blue-400', + variant === 'solid' && + color === 'primary' && + 'bg-blue-400 text-white ring-blue-700 enabled:hocus:bg-blue-500', + variant === 'solid' && + color === 'secondary' && + 'bg-violet-400 text-white ring-violet-700 enabled:hocus:bg-violet-500', + variant === 'solid' && + color === 'warning' && + 'bg-orange-400 text-white ring-orange-700 enabled:hocus:bg-orange-500', + variant === 'solid' && + color === 'danger' && + 'bg-red-400 text-white ring-red-700 enabled:hocus:bg-red-500', // Borders variant === 'border' && 'border', variant === 'border' && diff --git a/src-web/components/core/Checkbox.tsx b/src-web/components/core/Checkbox.tsx index d247ad07..0a781423 100644 --- a/src-web/components/core/Checkbox.tsx +++ b/src-web/components/core/Checkbox.tsx @@ -8,10 +8,21 @@ interface Props { onChange: (checked: boolean) => void; disabled?: boolean; className?: string; + inputWrapperClassName?: string; + indeterminate?: boolean; hideLabel?: boolean; } -export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) { +export function Checkbox({ + checked, + indeterminate, + onChange, + className, + inputWrapperClassName, + disabled, + title, + hideLabel, +}: Props) { return ( -
+
onChange(!checked)} />
- +
{/* { alert({ id: 'export-failed', title: 'Export Failed', body: err }); }, mutationFn: async () => { - if (workspace == null) return; + if (activeWorkspace == null || workspaces.length === 0) return; - const workspaceSlug = slugify(workspace.name, { lower: true }); - const exportPath = await save({ - title: 'Export Data', - defaultPath: `yaak.${workspaceSlug}.json`, + dialog.show({ + id: 'export-data', + title: 'Export App Data', + size: 'md', + noPadding: true, + render: ({ hide }) => ( + + ), }); - if (exportPath == null) { - return; - } - - await invoke('cmd_export_data', { workspaceId: workspace.id, exportPath }); }, }); } diff --git a/src-web/lib/pluralize.ts b/src-web/lib/pluralize.ts index a982c7b1..ceddea1a 100644 --- a/src-web/lib/pluralize.ts +++ b/src-web/lib/pluralize.ts @@ -5,6 +5,16 @@ export function pluralize(word: string, count: number): string { return `${word}s`; } -export function count(word: string, count: number): string { +export function count( + word: string, + count: number, + opt: { omitSingle?: boolean; noneWord?: string } = {}, +): string { + if (opt.omitSingle && count === 1) { + return word; + } + if (opt.noneWord && count === 0) { + return opt.noneWord; + } return `${count} ${pluralize(word, count)}`; } diff --git a/src-web/main.css b/src-web/main.css index ff6e6642..8014a0c8 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -9,10 +9,7 @@ @apply w-full h-full overflow-hidden text-gray-900 bg-gray-50; } - /* Setup default transitions for elements */ * { - transition: background-color var(--transition-duration), border-color var(--transition-duration), - box-shadow var(--transition-duration); font-variant-ligatures: none; }