diff --git a/src-tauri/capabilities/capabilities.json b/src-tauri/capabilities/capabilities.json index 68fe2778..c9a7c391 100644 --- a/src-tauri/capabilities/capabilities.json +++ b/src-tauri/capabilities/capabilities.json @@ -33,6 +33,7 @@ "opener:allow-open-url", "opener:allow-open-path", "opener:allow-default-urls", + "opener:allow-reveal-item-in-dir", "core:webview:allow-set-webview-zoom", "core:window:allow-close", "core:window:allow-internal-toggle-maximize", diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 638c0406..54fc2460 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -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-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"]}} \ No newline at end of file +{"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","opener:allow-reveal-item-in-dir","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"]}} \ No newline at end of file diff --git a/src-tauri/yaak-models/src/queries.rs b/src-tauri/yaak-models/src/queries.rs index 664d0260..a63da121 100644 --- a/src-tauri/yaak-models/src/queries.rs +++ b/src-tauri/yaak-models/src/queries.rs @@ -9,7 +9,7 @@ use crate::models::{ WorkspaceMetaIden, }; use crate::plugin::SqliteConnection; -use chrono::NaiveDateTime; +use chrono::{NaiveDateTime, Utc}; use log::{debug, error, info, warn}; use nanoid::nanoid; use rusqlite::OptionalExtension; @@ -278,8 +278,8 @@ pub async fn upsert_workspace( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, workspace.created_at).into(), + timestamp_for_upsert(update_source, workspace.updated_at).into(), trimmed_name.into(), workspace.description.into(), workspace.setting_follow_redirects.into(), @@ -297,6 +297,7 @@ pub async fn upsert_workspace( WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingValidateCertificates, ]) + .values([(WorkspaceIden::UpdatedAt, CurrentTimestamp.into())]) .to_owned(), ) .returning_all() @@ -333,8 +334,8 @@ pub async fn upsert_workspace_meta( .values_panic([ id.as_str().into(), workspace_meta.workspace_id.into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, workspace_meta.created_at).into(), + timestamp_for_upsert(update_source, workspace_meta.updated_at).into(), workspace_meta.setting_sync_dir.into(), ]) .on_conflict( @@ -500,8 +501,8 @@ pub async fn upsert_grpc_request( ]) .values_panic([ id.into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, request.created_at).into(), + timestamp_for_upsert(update_source, request.updated_at).into(), trimmed_name.into(), request.description.into(), request.workspace_id.into(), @@ -612,8 +613,8 @@ pub async fn upsert_grpc_connection( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, connection.created_at).into(), + timestamp_for_upsert(update_source, connection.updated_at).into(), connection.workspace_id.as_str().into(), connection.request_id.as_str().into(), connection.service.as_str().into(), @@ -771,8 +772,8 @@ pub async fn upsert_grpc_event( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, event.created_at).into(), + timestamp_for_upsert(update_source, event.updated_at).into(), event.workspace_id.as_str().into(), event.request_id.as_str().into(), event.connection_id.as_str().into(), @@ -859,8 +860,8 @@ pub async fn upsert_cookie_jar( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, cookie_jar.created_at).into(), + timestamp_for_upsert(update_source, cookie_jar.updated_at).into(), cookie_jar.workspace_id.as_str().into(), trimmed_name.into(), serde_json::to_string(&cookie_jar.cookies)?.into(), @@ -1064,8 +1065,8 @@ pub async fn upsert_environment( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, environment.created_at).into(), + timestamp_for_upsert(update_source, environment.updated_at).into(), environment.environment_id.into(), environment.workspace_id.into(), trimmed_name.into(), @@ -1174,8 +1175,8 @@ pub async fn upsert_plugin( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), + timestamp_for_upsert(update_source, plugin.created_at).into(), + timestamp_for_upsert(update_source, plugin.updated_at).into(), plugin.checked_at.into(), plugin.directory.into(), plugin.url.into(), @@ -1276,15 +1277,15 @@ pub async fn delete_folder( pub async fn upsert_folder( window: &WebviewWindow, - r: Folder, + folder: Folder, update_source: &UpdateSource, ) -> Result { - let id = match r.id.as_str() { + let id = match folder.id.as_str() { "" => generate_model_id(ModelType::TypeFolder), - _ => r.id.to_string(), + _ => folder.id.to_string(), }; - let trimmed_name = r.name.trim(); + let trimmed_name = folder.name.trim(); let dbm = &*window.app_handle().state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -1303,13 +1304,13 @@ pub async fn upsert_folder( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), - r.workspace_id.as_str().into(), - r.folder_id.as_ref().map(|s| s.as_str()).into(), + timestamp_for_upsert(update_source, folder.created_at).into(), + timestamp_for_upsert(update_source, folder.updated_at).into(), + folder.workspace_id.as_str().into(), + folder.folder_id.as_ref().map(|s| s.as_str()).into(), trimmed_name.into(), - r.description.into(), - r.sort_priority.into(), + folder.description.into(), + folder.sort_priority.into(), ]) .on_conflict( OnConflict::column(GrpcEventIden::Id) @@ -1419,14 +1420,14 @@ pub async fn duplicate_folder( pub async fn upsert_http_request( window: &WebviewWindow, - r: HttpRequest, + request: HttpRequest, update_source: &UpdateSource, ) -> Result { - let id = match r.id.as_str() { + let id = match request.id.as_str() { "" => generate_model_id(ModelType::TypeHttpRequest), - _ => r.id.to_string(), + _ => request.id.to_string(), }; - let trimmed_name = r.name.trim(); + let trimmed_name = request.name.trim(); let dbm = &*window.app_handle().state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -1453,21 +1454,21 @@ pub async fn upsert_http_request( ]) .values_panic([ id.as_str().into(), - CurrentTimestamp.into(), - CurrentTimestamp.into(), - r.workspace_id.into(), - r.folder_id.as_ref().map(|s| s.as_str()).into(), + timestamp_for_upsert(update_source, request.created_at).into(), + timestamp_for_upsert(update_source, request.updated_at).into(), + request.workspace_id.into(), + request.folder_id.as_ref().map(|s| s.as_str()).into(), trimmed_name.into(), - r.description.into(), - r.url.into(), - serde_json::to_string(&r.url_parameters)?.into(), - r.method.into(), - serde_json::to_string(&r.body)?.into(), - r.body_type.as_ref().map(|s| s.as_str()).into(), - serde_json::to_string(&r.authentication)?.into(), - r.authentication_type.as_ref().map(|s| s.as_str()).into(), - serde_json::to_string(&r.headers)?.into(), - r.sort_priority.into(), + request.description.into(), + request.url.into(), + serde_json::to_string(&request.url_parameters)?.into(), + request.method.into(), + serde_json::to_string(&request.body)?.into(), + request.body_type.as_ref().map(|s| s.as_str()).into(), + serde_json::to_string(&request.authentication)?.into(), + request.authentication_type.as_ref().map(|s| s.as_str()).into(), + serde_json::to_string(&request.headers)?.into(), + request.sort_priority.into(), ]) .on_conflict( OnConflict::column(GrpcEventIden::Id) @@ -2167,7 +2168,7 @@ pub async fn get_workspace_export_resources( let mut data = WorkspaceExport { yaak_version: mgr.package_info().version.clone().to_string(), yaak_schema: 2, - timestamp: chrono::Utc::now().naive_utc(), + timestamp: Utc::now().naive_utc(), resources: BatchUpsertResult { workspaces: Vec::new(), environments: Vec::new(), @@ -2197,3 +2198,21 @@ pub async fn get_workspace_export_resources( data } + +// Generate the created_at or updated_at timestamps for an upsert operation, depending on the ID +// provided. +fn timestamp_for_upsert(update_source: &UpdateSource, dt: NaiveDateTime) -> NaiveDateTime { + match update_source { + // Sync and import operations always preserve timestamps + UpdateSource::Sync | UpdateSource::Import => { + if dt.and_utc().timestamp() == 0 { + // Sometimes data won't have timestamps (partial data) + Utc::now().naive_utc() + } else { + dt + } + }, + // Other sources will always update to the latest time + _ => Utc::now().naive_utc(), + } +} diff --git a/src-tauri/yaak-sync/src/commands.rs b/src-tauri/yaak-sync/src/commands.rs index f51757fe..a3dba670 100644 --- a/src-tauri/yaak-sync/src/commands.rs +++ b/src-tauri/yaak-sync/src/commands.rs @@ -1,7 +1,7 @@ use crate::error::Result; use crate::sync::{ apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates, get_fs_candidates, - SyncOp, + FsCandidate, SyncOp, }; use crate::watch::{watch_directory, WatchEvent}; use chrono::Utc; @@ -23,16 +23,18 @@ pub async fn calculate( let fs_candidates = get_fs_candidates(sync_dir) .await? .into_iter() - // Strip out any non-workspace candidates + // Only keep items in the same workspace .filter(|fs| fs.model.workspace_id() == workspace_id) - .collect(); + .collect::>(); + // println!("\ndb_candidates: \n{}\n", serde_json::to_string_pretty(&db_candidates)?); + // println!("\nfs_candidates: \n{}\n", serde_json::to_string_pretty(&fs_candidates)?); Ok(compute_sync_ops(db_candidates, fs_candidates)) } #[command] pub async fn calculate_fs(dir: &Path) -> Result> { let db_candidates = Vec::new(); - let fs_candidates = get_fs_candidates(Path::new(&dir)).await?; + let fs_candidates = get_fs_candidates(dir).await?; Ok(compute_sync_ops(db_candidates, fs_candidates)) } diff --git a/src-tauri/yaak-sync/src/sync.rs b/src-tauri/yaak-sync/src/sync.rs index 4607b0a1..602f7f1e 100644 --- a/src-tauri/yaak-sync/src/sync.rs +++ b/src-tauri/yaak-sync/src/sync.rs @@ -76,7 +76,8 @@ impl Display for SyncOp { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub(crate) enum DbCandidate { Added(SyncModel), Modified(SyncModel, SyncState), diff --git a/src-web/commands/commands.tsx b/src-web/commands/commands.tsx index 8cfb1a62..e60c6e28 100644 --- a/src-web/commands/commands.tsx +++ b/src-web/commands/commands.tsx @@ -1,4 +1,4 @@ -import type { Folder, Workspace } from '@yaakapp-internal/models'; +import type { Folder } from '@yaakapp-internal/models'; import { applySync, calculateSync } from '@yaakapp-internal/sync'; import { Banner } from '../components/core/Banner'; import { InlineCode } from '../components/core/InlineCode'; @@ -10,21 +10,8 @@ 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>({ - mutationKey: ['create_workspace'], - mutationFn: (patch) => invokeCmd('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, @@ -66,8 +53,10 @@ export const syncWorkspace = createFastMutation< mutationFn: async ({ workspaceId, syncDir }) => { const ops = (await calculateSync(workspaceId, syncDir)) ?? []; if (ops.length === 0) { + console.log('Nothing to sync', workspaceId, syncDir, ops); return; } + console.log('syncing workspace', workspaceId, syncDir, ops); const dbOps = ops.filter((o) => o.type.startsWith('db')); diff --git a/src-web/commands/openWorkspace.tsx b/src-web/commands/openWorkspaceFromSyncDir.tsx similarity index 94% rename from src-web/commands/openWorkspace.tsx rename to src-web/commands/openWorkspaceFromSyncDir.tsx index b013c425..14ff16d7 100644 --- a/src-web/commands/openWorkspace.tsx +++ b/src-web/commands/openWorkspaceFromSyncDir.tsx @@ -4,7 +4,7 @@ import { createFastMutation } from '../hooks/useFastMutation'; import { showSimpleAlert } from '../lib/alert'; import { router } from '../lib/router'; -export const openWorkspace = createFastMutation({ +export const openWorkspaceFromSyncDir = createFastMutation({ mutationKey: [], mutationFn: async () => { const dir = await open({ diff --git a/src-web/commands/switchWorkspace.tsx b/src-web/commands/switchWorkspace.tsx new file mode 100644 index 00000000..5dd4850f --- /dev/null +++ b/src-web/commands/switchWorkspace.tsx @@ -0,0 +1,42 @@ +import { createFastMutation } from '../hooks/useFastMutation'; +import { getRecentCookieJars } from '../hooks/useRecentCookieJars'; +import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; +import { getRecentRequests } from '../hooks/useRecentRequests'; +import { router } from '../lib/router'; +import { invokeCmd } from '../lib/tauri'; + +export const switchWorkspace = createFastMutation({ + mutationKey: ['open_workspace'], + mutationFn: async ({ + workspaceId, + inNewWindow, + }: { + workspaceId: string; + inNewWindow: boolean; + }) => { + const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined; + const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined; + const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined; + const search = { + environment_id: environmentId, + cookie_jar_id: cookieJarId, + request_id: requestId, + }; + + if (inNewWindow) { + const location = router.buildLocation({ + to: '/workspaces/$workspaceId', + params: { workspaceId }, + search, + }); + await invokeCmd('cmd_new_main_window', { url: location.href }); + return; + } + + await router.navigate({ + to: '/workspaces/$workspaceId', + params: { workspaceId }, + search, + }); + }, +}); diff --git a/src-web/commands/upsertWorkspace.ts b/src-web/commands/upsertWorkspace.ts new file mode 100644 index 00000000..7132b70b --- /dev/null +++ b/src-web/commands/upsertWorkspace.ts @@ -0,0 +1,19 @@ +import type { Workspace } from '@yaakapp-internal/models'; +import { createFastMutation } from '../hooks/useFastMutation'; +import { trackEvent } from '../lib/analytics'; +import { invokeCmd } from '../lib/tauri'; + +export const upsertWorkspace = createFastMutation< + Workspace, + void, + Workspace | Partial> +>({ + mutationKey: ['upsert_workspace'], + mutationFn: (workspace) => invokeCmd('cmd_update_workspace', { workspace }), + onSuccess: async (workspace) => { + const isNew = workspace.createdAt == workspace.updatedAt; + + if (isNew) trackEvent('workspace', 'create'); + else trackEvent('workspace', 'update'); + }, +}); diff --git a/src-web/commands/upsertWorkspaceMeta.ts b/src-web/commands/upsertWorkspaceMeta.ts index 2f20692e..e98f00e8 100644 --- a/src-web/commands/upsertWorkspaceMeta.ts +++ b/src-web/commands/upsertWorkspaceMeta.ts @@ -7,7 +7,7 @@ import { invokeCmd } from '../lib/tauri'; export const upsertWorkspaceMeta = createFastMutation< WorkspaceMeta, unknown, - Partial + WorkspaceMeta | (Partial> & { workspaceId: string }) >({ mutationKey: ['update_workspace_meta'], mutationFn: async (patch) => { diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index ea847293..ff9b35d8 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -2,6 +2,8 @@ import classNames from 'classnames'; import { fuzzyFilter } from 'fuzzbunny'; import type { KeyboardEvent, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createFolder } from '../commands/commands'; +import { switchWorkspace } from '../commands/switchWorkspace'; import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveRequest } from '../hooks/useActiveRequest'; @@ -16,7 +18,6 @@ import type { HotkeyAction } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey'; import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; import { useOpenSettings } from '../hooks/useOpenSettings'; -import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; @@ -38,7 +39,6 @@ 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,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { const [, setSidebarHidden] = useSidebarHidden(); const { baseEnvironment } = useEnvironments(); const { mutate: openSettings } = useOpenSettings(); - const { mutate: switchWorkspace } = useSwitchWorkspace(); const { mutate: createHttpRequest } = useCreateHttpRequest(); const { mutate: createGrpcRequest } = useCreateGrpcRequest(); const { mutate: createEnvironment } = useCreateEnvironment(); @@ -315,7 +314,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { workspaceGroup.items.push({ key: `switch-workspace-${w.id}`, label: w.name, - onSelect: () => switchWorkspace({ workspaceId: w.id, inNewWindow: false }), + onSelect: () => switchWorkspace.mutate({ workspaceId: w.id, inNewWindow: false }), }); } @@ -327,7 +326,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { activeEnvironment?.id, setActiveEnvironmentId, sortedWorkspaces, - switchWorkspace, ]); const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]); diff --git a/src-web/components/CookieDropdown.tsx b/src-web/components/CookieDropdown.tsx index 4ec93c09..017bab4f 100644 --- a/src-web/components/CookieDropdown.tsx +++ b/src-web/components/CookieDropdown.tsx @@ -76,7 +76,7 @@ export const CookieDropdown = memo(function CookieDropdown() { key: 'delete', label: 'Delete', leftSlot: , - variant: 'danger', + color: 'danger', onSelect: () => deleteCookieJar.mutateAsync(), }, ] diff --git a/src-web/components/CreateWorkspaceDialog.tsx b/src-web/components/CreateWorkspaceDialog.tsx index bb953949..97d789bd 100644 --- a/src-web/components/CreateWorkspaceDialog.tsx +++ b/src-web/components/CreateWorkspaceDialog.tsx @@ -1,5 +1,8 @@ import { useState } from 'react'; -import { createWorkspace } from '../commands/commands'; +import { upsertWorkspace } from '../commands/upsertWorkspace'; +import { upsertWorkspaceMeta } from '../commands/upsertWorkspaceMeta'; +import { router } from '../lib/router'; +import { getWorkspaceMeta } from '../lib/store'; import { Button } from './core/Button'; import { PlainInput } from './core/PlainInput'; import { VStack } from './core/Stacks'; @@ -26,7 +29,20 @@ export function CreateWorkspaceDialog({ hide }: Props) { e.preventDefault(); const { enabled, value } = settingSyncDir ?? {}; if (enabled && !value) return; - await createWorkspace.mutateAsync({ name, settingSyncDir: value }); + const workspace = await upsertWorkspace.mutateAsync({ name }); + if (workspace == null) return; + + // Do getWorkspaceMeta instead of naively creating one because it might have + // been created already when the store refreshes the workspace meta after + const workspaceMeta = await getWorkspaceMeta(workspace.id); + upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir: value }); + + // Navigate to workspace + await router.navigate({ + to: '/workspaces/$workspaceId', + params: { workspaceId: workspace.id }, + }); + hide(); }} > diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index da7cd6a0..c1a0f475 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -278,7 +278,7 @@ function SidebarButton({ }, { key: 'delete-environment', - variant: 'danger', + color: 'danger', label: 'Delete', leftSlot: , onSelect: () => deleteEnvironment.mutate(), diff --git a/src-web/components/GraphQLEditor.tsx b/src-web/components/GraphQLEditor.tsx index 70127c79..a3784b97 100644 --- a/src-web/components/GraphQLEditor.tsx +++ b/src-web/components/GraphQLEditor.tsx @@ -79,7 +79,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr label: 'Clear', onSelect: clear, hidden: !schema, - variant: 'danger', + color: 'danger', leftSlot: , }, { type: 'separator', label: 'Setting' }, diff --git a/src-web/components/GrpcConnectionMessagesPane.tsx b/src-web/components/GrpcConnectionMessagesPane.tsx index 5ea7f7c7..82a54bb2 100644 --- a/src-web/components/GrpcConnectionMessagesPane.tsx +++ b/src-web/components/GrpcConnectionMessagesPane.tsx @@ -156,7 +156,9 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: ) : ( {Object.entries(activeEvent.metadata).map(([key, value]) => ( - + + {value} + ))} )} diff --git a/src-web/components/ResponseHeaders.tsx b/src-web/components/ResponseHeaders.tsx index de5f0ce3..463f8bfb 100644 --- a/src-web/components/ResponseHeaders.tsx +++ b/src-web/components/ResponseHeaders.tsx @@ -10,7 +10,9 @@ export function ResponseHeaders({ response }: Props) {
{response.headers.map((h, i) => ( - + + {h.value} + ))}
diff --git a/src-web/components/ResponseInfo.tsx b/src-web/components/ResponseInfo.tsx index 068912e7..ea17ae74 100644 --- a/src-web/components/ResponseInfo.tsx +++ b/src-web/components/ResponseInfo.tsx @@ -11,8 +11,12 @@ export function ResponseInfo({ response }: Props) { return (
- - + + {response.version} + + + {response.remoteAddr} +
} - value={ + > + {
{response.url}
} - /> + ); diff --git a/src-web/components/Settings/SettingsGeneral.tsx b/src-web/components/Settings/SettingsGeneral.tsx index ab309824..8c5852ec 100644 --- a/src-web/components/Settings/SettingsGeneral.tsx +++ b/src-web/components/Settings/SettingsGeneral.tsx @@ -1,10 +1,12 @@ +import { revealItemInDir } from '@tauri-apps/plugin-opener'; import React from 'react'; +import { upsertWorkspace } from '../../commands/upsertWorkspace'; import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { useAppInfo } from '../../hooks/useAppInfo'; import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; import { useSettings } from '../../hooks/useSettings'; import { useUpdateSettings } from '../../hooks/useUpdateSettings'; -import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace'; +import { revealInFinderText } from '../../lib/reveal'; import { Checkbox } from '../core/Checkbox'; import { Heading } from '../core/Heading'; import { IconButton } from '../core/IconButton'; @@ -16,7 +18,6 @@ import { VStack } from '../core/Stacks'; export function SettingsGeneral() { const workspace = useActiveWorkspace(); - const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null); const settings = useSettings(); const updateSettings = useUpdateSettings(); const appInfo = useAppInfo(); @@ -103,7 +104,9 @@ export function SettingsGeneral() { labelPosition="left" defaultValue={`${workspace.settingRequestTimeout}`} validate={(value) => parseInt(value) >= 0} - onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })} + onChange={(v) => + upsertWorkspace.mutate({ ...workspace, settingRequestTimeout: parseInt(v) || 0 }) + } type="number" /> @@ -112,7 +115,7 @@ export function SettingsGeneral() { title="Validate TLS Certificates" event="validate-certs" onChange={(settingValidateCertificates) => - updateWorkspace.mutate({ settingValidateCertificates }) + upsertWorkspace.mutate({ ...workspace, settingValidateCertificates }) } /> @@ -120,7 +123,12 @@ export function SettingsGeneral() { checked={workspace.settingFollowRedirects} title="Follow Redirects" event="follow-redirects" - onChange={(settingFollowRedirects) => updateWorkspace.mutate({ settingFollowRedirects })} + onChange={(settingFollowRedirects) => + upsertWorkspace.mutate({ + ...workspace, + settingFollowRedirects, + }) + } /> @@ -128,9 +136,33 @@ export function SettingsGeneral() { App Info - - - + {appInfo.version} + revealItemInDir(appInfo.appDataDir)} + /> + } + > + {appInfo.appDataDir} + + revealItemInDir(appInfo.appLogDir)} + /> + } + > + {appInfo.appLogDir} + ); diff --git a/src-web/components/SidebarItemContextMenu.tsx b/src-web/components/SidebarItemContextMenu.tsx index b4e64a04..5a0c1a02 100644 --- a/src-web/components/SidebarItemContextMenu.tsx +++ b/src-web/components/SidebarItemContextMenu.tsx @@ -72,7 +72,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) { { key: 'delete-folder', label: 'Delete', - variant: 'danger', + color: 'danger', leftSlot: , onSelect: () => deleteFolder.mutate(), }, @@ -132,7 +132,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) { }, { key: 'delete-request', - variant: 'danger', + color: 'danger', label: 'Delete', hotKeyAction: 'http_request.delete', hotKeyLabelOnly: true, diff --git a/src-web/components/SwitchWorkspaceDialog.tsx b/src-web/components/SwitchWorkspaceDialog.tsx index d493e4cd..aa7b384f 100644 --- a/src-web/components/SwitchWorkspaceDialog.tsx +++ b/src-web/components/SwitchWorkspaceDialog.tsx @@ -1,8 +1,8 @@ +import type { Workspace } from '@yaakapp-internal/models'; import { useState } from 'react'; -import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace'; +import { switchWorkspace } from '../commands/switchWorkspace'; import { useSettings } from '../hooks/useSettings'; import { useUpdateSettings } from '../hooks/useUpdateSettings'; -import type { Workspace } from '@yaakapp-internal/models'; import { Button } from './core/Button'; import { Checkbox } from './core/Checkbox'; import { Icon } from './core/Icon'; @@ -15,7 +15,6 @@ interface Props { } export function SwitchWorkspaceDialog({ hide, workspace }: Props) { - const switchWorkspace = useSwitchWorkspace(); const settings = useSettings(); const updateSettings = useUpdateSettings(); const [remember, setRemember] = useState(false); diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index 9dc479f7..5d25d0bf 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -1,13 +1,17 @@ +import { revealItemInDir } from '@tauri-apps/plugin-opener'; import classNames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; -import {openWorkspace} from "../commands/openWorkspace"; +import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir'; +import { switchWorkspace } from '../commands/switchWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory'; -import { useSwitchWorkspace } from '../hooks/useSwitchWorkspace'; -import { useSettings } from '../hooks/useSettings'; +import { settingsAtom } from '../hooks/useSettings'; +import { useWorkspaceMeta } from '../hooks/useWorkspaceMeta'; import { useWorkspaces } from '../hooks/useWorkspaces'; import { showDialog } from '../lib/dialog'; +import { jotaiStore } from '../lib/jotai'; +import { revealInFinderText } from '../lib/reveal'; import { getWorkspace } from '../lib/store'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; @@ -25,12 +29,10 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ ...buttonProps }: Props) { const workspaces = useWorkspaces(); - const activeWorkspace = useActiveWorkspace(); + const workspace = useActiveWorkspace(); const createWorkspace = useCreateWorkspace(); + const workspaceMeta = useWorkspaceMeta(); const { mutate: deleteSendHistory } = useDeleteSendHistory(); - const settings = useSettings(); - const switchWorkspace = useSwitchWorkspace(); - const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null; const orderedWorkspaces = useMemo( () => [...workspaces].sort((a, b) => (a.name.localeCompare(b.name) > 0 ? 1 : -1)), @@ -45,7 +47,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ key: w.id, label: w.name, value: w.id, - leftSlot: w.id === activeWorkspace?.id ? : , + leftSlot: w.id === workspace?.id ? : , })); const extraItems: DropdownItem[] = [ @@ -60,14 +62,25 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ title: 'Workspace Settings', size: 'md', render: ({ hide }) => ( - + ), }); }, }, + { + key: 'reveal-workspace-sync-dir', + label: revealInFinderText, + hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null, + leftSlot: , + onSelect: async () => { + if (workspaceMeta?.settingSyncDir == null) return; + await revealItemInDir(workspaceMeta.settingSyncDir); + }, + }, { key: 'delete-responses', label: 'Clear Send History', + color: 'warning', leftSlot: , onSelect: deleteSendHistory, }, @@ -82,52 +95,50 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ key: 'open-workspace', label: 'Open Workspace', leftSlot: , - onSelect: openWorkspace.mutate, + onSelect: openWorkspaceFromSyncDir.mutate, }, ]; return { workspaceItems, extraItems }; - }, [orderedWorkspaces, activeWorkspace?.id, deleteSendHistory, createWorkspace]); + }, [orderedWorkspaces, deleteSendHistory, createWorkspace, workspaceMeta, workspace?.id]); - const handleChange = useCallback( - async (workspaceId: string | null) => { - if (workspaceId == null) return; + const handleChangeWorkspace = useCallback(async (workspaceId: string | null) => { + if (workspaceId == null) return; - if (typeof openWorkspaceNewWindow === 'boolean') { - switchWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow }); - return; - } + const settings = jotaiStore.get(settingsAtom); + if (typeof settings.openWorkspaceNewWindow === 'boolean') { + switchWorkspace.mutate({ workspaceId, inNewWindow: settings.openWorkspaceNewWindow }); + return; + } - const workspace = await getWorkspace(workspaceId); - if (workspace == null) return; + const workspace = await getWorkspace(workspaceId); + if (workspace == null) return; - showDialog({ - id: 'switch-workspace', - size: 'sm', - title: 'Switch Workspace', - render: ({ hide }) => , - }); - }, - [switchWorkspace, openWorkspaceNewWindow], - ); + showDialog({ + id: 'switch-workspace', + size: 'sm', + title: 'Switch Workspace', + render: ({ hide }) => , + }); + }, []); return ( ); diff --git a/src-web/components/WorkspaceSettingsDialog.tsx b/src-web/components/WorkspaceSettingsDialog.tsx index 8dd22966..4b83f437 100644 --- a/src-web/components/WorkspaceSettingsDialog.tsx +++ b/src-web/components/WorkspaceSettingsDialog.tsx @@ -1,6 +1,6 @@ +import { upsertWorkspace } from '../commands/upsertWorkspace'; 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'; @@ -21,7 +21,6 @@ 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) { @@ -44,7 +43,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { updateWorkspace({ name })} + onChange={(name) => upsertWorkspace.mutate({ ...workspace, name })} stateKey={`name.${workspace.id}`} /> @@ -54,7 +53,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { className="min-h-[10rem] max-h-[25rem] border border-border px-2" defaultValue={workspace.description} stateKey={`description.${workspace.id}`} - onChange={(description) => updateWorkspace({ description })} + onChange={(description) => upsertWorkspace.mutate({ ...workspace, description })} heightMode="auto" /> @@ -62,7 +61,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { { - upsertWorkspaceMeta.mutate({ settingSyncDir }); + upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir }); }} /> diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index 4463eee9..29a30235 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -20,7 +20,7 @@ import React, { useRef, useState, } from 'react'; -import {useClickAway, useKey, useWindowSize} from 'react-use'; +import { useClickAway, useKey, useWindowSize } from 'react-use'; import type { HotkeyAction } from '../../hooks/useHotKey'; import { useHotKey } from '../../hooks/useHotKey'; import { useStateWithDeps } from '../../hooks/useStateWithDeps'; @@ -45,7 +45,7 @@ export type DropdownItemDefault = { keepOpen?: boolean; hotKeyAction?: HotkeyAction; hotKeyLabelOnly?: boolean; - variant?: 'default' | 'danger' | 'notify'; + color?: 'default' | 'danger' | 'info' | 'warning' | 'notice'; disabled?: boolean; hidden?: boolean; leftSlot?: ReactNode; @@ -558,8 +558,10 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men 'h-xs', // More compact 'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap', 'focus:bg-surface-highlight focus:text rounded', - item.variant === 'danger' && '!text-danger', - item.variant === 'notify' && '!text-primary', + item.color === 'danger' && '!text-danger', + item.color === 'warning' && '!text-warning', + item.color === 'notice' && '!text-notice', + item.color === 'info' && '!text-info', )} {...props} > diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 7e21a408..b02095e3 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -42,9 +42,10 @@ const icons = { filter: lucide.FilterIcon, flask: lucide.FlaskConicalIcon, folder: lucide.FolderIcon, - folder_sync: lucide.FolderSyncIcon, folder_input: lucide.FolderInputIcon, + folder_open: lucide.FolderOpenIcon, folder_output: lucide.FolderOutputIcon, + folder_sync: lucide.FolderSyncIcon, git_branch: lucide.GitBranchIcon, git_commit: lucide.GitCommitIcon, git_commit_vertical: lucide.GitCommitVerticalIcon, diff --git a/src-web/components/core/KeyValueRow.tsx b/src-web/components/core/KeyValueRow.tsx index 0ae9ab85..9302dd9b 100644 --- a/src-web/components/core/KeyValueRow.tsx +++ b/src-web/components/core/KeyValueRow.tsx @@ -22,14 +22,18 @@ export function KeyValueRows({ children }: Props) { interface KeyValueRowProps { label: ReactNode; - value: ReactNode; + children: ReactNode; + rightSlot?: ReactNode; + leftSlot?: ReactNode; labelClassName?: string; labelColor?: 'secondary' | 'primary' | 'info'; } export function KeyValueRow({ label, - value, + children, + rightSlot, + leftSlot, labelColor = 'secondary', labelClassName, }: KeyValueRowProps) { @@ -47,7 +51,11 @@ export function KeyValueRow({ {label} -
{value}
+
+ {leftSlot ?? } + {children} + {rightSlot ?
{rightSlot}
: } +
); diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index 947168bc..8d602384 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -344,7 +344,7 @@ function PairEditorRow({ key: 'delete', label: 'Delete', onSelect: handleDelete, - variant: 'danger', + color: 'danger', }, ], [handleDelete], diff --git a/src-web/hooks/useSwitchWorkspace.ts b/src-web/hooks/useSwitchWorkspace.ts deleted file mode 100644 index cd7b909b..00000000 --- a/src-web/hooks/useSwitchWorkspace.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { router } from '../lib/router'; -import { invokeCmd } from '../lib/tauri'; -import { useFastMutation } from './useFastMutation'; -import { getRecentCookieJars } from './useRecentCookieJars'; -import { getRecentEnvironments } from './useRecentEnvironments'; -import { getRecentRequests } from './useRecentRequests'; - -export function useSwitchWorkspace() { - return useFastMutation({ - mutationKey: ['open_workspace'], - mutationFn: async ({ - workspaceId, - inNewWindow, - }: { - workspaceId: string; - inNewWindow: boolean; - }) => { - const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined; - const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined; - const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined; - const search = { - environment_id: environmentId, - cookie_jar_id: cookieJarId, - request_id: requestId, - }; - - if (inNewWindow) { - const location = router.buildLocation({ - to: '/workspaces/$workspaceId', - params: { workspaceId }, - search, - }); - await invokeCmd('cmd_new_main_window', { url: location.href }); - return; - } - - await router.navigate({ - to: '/workspaces/$workspaceId', - params: { workspaceId }, - search, - }); - }, - }); -} diff --git a/src-web/hooks/useSyncWorkspaceChildModels.ts b/src-web/hooks/useSyncWorkspaceChildModels.ts index 028af856..4eef77ef 100644 --- a/src-web/hooks/useSyncWorkspaceChildModels.ts +++ b/src-web/hooks/useSyncWorkspaceChildModels.ts @@ -1,6 +1,6 @@ -import type { WorkspaceMeta } from '@yaakapp-internal/models'; import { useEffect } from 'react'; import { jotaiStore } from '../lib/jotai'; +import { getWorkspaceMeta } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { activeWorkspaceIdAtom, getActiveWorkspaceId } from './useActiveWorkspace'; import { cookieJarsAtom } from './useCookieJars'; @@ -26,10 +26,9 @@ async function sync() { jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values')); const workspaceId = getActiveWorkspaceId(); + if (workspaceId == null) return; + const args = { workspaceId }; - if (workspaceId == null) { - return; - } // Set the things we need first, first jotaiStore.set(httpRequestsAtom, await invokeCmd('cmd_list_http_requests', args)); @@ -43,5 +42,5 @@ async function sync() { jotaiStore.set(environmentsAtom, await invokeCmd('cmd_list_environments', args)); // Single models - jotaiStore.set(workspaceMetaAtom, await invokeCmd('cmd_get_workspace_meta', args)); + jotaiStore.set(workspaceMetaAtom, await getWorkspaceMeta(workspaceId)); } diff --git a/src-web/hooks/useUpdateWorkspace.ts b/src-web/hooks/useUpdateWorkspace.ts deleted file mode 100644 index 496960d9..00000000 --- a/src-web/hooks/useUpdateWorkspace.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Workspace } from '@yaakapp-internal/models'; -import { getWorkspace } from '../lib/store'; -import { invokeCmd } from '../lib/tauri'; -import { useFastMutation } from './useFastMutation'; - -export function useUpdateWorkspace(id: string | null) { - return useFastMutation | ((w: Workspace) => Workspace)>({ - mutationKey: ['update_workspace', id], - mutationFn: async (v) => { - const workspace = await getWorkspace(id); - if (workspace == null) { - throw new Error("Can't update a null workspace"); - } - - const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; - return invokeCmd('cmd_update_workspace', { workspace: newWorkspace }); - }, - }); -} diff --git a/src-web/hooks/useWorkspaceMeta.ts b/src-web/hooks/useWorkspaceMeta.ts index 4a85fc46..57c34b40 100644 --- a/src-web/hooks/useWorkspaceMeta.ts +++ b/src-web/hooks/useWorkspaceMeta.ts @@ -1,13 +1,8 @@ import type { WorkspaceMeta } from '@yaakapp-internal/models'; import { atom, useAtomValue } from 'jotai'; -export const workspaceMetaAtom = atom(); +export const workspaceMetaAtom = atom(null); export function useWorkspaceMeta() { - const workspaceMeta = useAtomValue(workspaceMetaAtom); - if (!workspaceMeta) { - throw new Error('WorkspaceMeta not found'); - } - - return workspaceMeta; + return useAtomValue(workspaceMetaAtom); } diff --git a/src-web/lib/reveal.ts b/src-web/lib/reveal.ts new file mode 100644 index 00000000..7ca8b15c --- /dev/null +++ b/src-web/lib/reveal.ts @@ -0,0 +1,9 @@ +import { type } from '@tauri-apps/plugin-os'; + +const os = type(); +export const revealInFinderText = + os === 'macos' + ? 'Reveal in Finder' + : os === 'windows' + ? 'Show in Explorer' + : 'Show in File Manager'; diff --git a/src-web/lib/store.ts b/src-web/lib/store.ts index e5639072..93c321e6 100644 --- a/src-web/lib/store.ts +++ b/src-web/lib/store.ts @@ -7,6 +7,7 @@ import type { Plugin, Settings, Workspace, + WorkspaceMeta, } from '@yaakapp-internal/models'; import { invokeCmd } from './tauri'; @@ -59,6 +60,10 @@ export async function getWorkspace(id: string | null): Promise return workspace; } +export async function getWorkspaceMeta(workspaceId: string) { + return invokeCmd('cmd_get_workspace_meta', { workspaceId }); +} + export async function listWorkspaces(): Promise { const workspaces: Workspace[] = (await invokeCmd('cmd_list_workspaces')) ?? []; return workspaces;