Fix workspace creation, reveal sync dir, and don't update timestamps on sync/import

This commit is contained in:
Gregory Schier
2025-01-09 07:50:23 -08:00
parent 0a7257c55a
commit f694456ddc
33 changed files with 312 additions and 219 deletions

View File

@@ -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",

View File

@@ -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"]}}
{"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"]}}

View File

@@ -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<R: Runtime>(
])
.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<R: Runtime>(
WorkspaceIden::SettingRequestTimeout,
WorkspaceIden::SettingValidateCertificates,
])
.values([(WorkspaceIden::UpdatedAt, CurrentTimestamp.into())])
.to_owned(),
)
.returning_all()
@@ -333,8 +334,8 @@ pub async fn upsert_workspace_meta<R: Runtime>(
.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<R: Runtime>(
])
.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<R: Runtime>(
])
.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<R: Runtime>(
])
.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<R: Runtime>(
])
.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<R: Runtime>(
])
.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<R: Runtime>(
])
.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<R: Runtime>(
pub async fn upsert_folder<R: Runtime>(
window: &WebviewWindow<R>,
r: Folder,
folder: Folder,
update_source: &UpdateSource,
) -> Result<Folder> {
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::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1303,13 +1304,13 @@ pub async fn upsert_folder<R: Runtime>(
])
.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<R: Runtime>(
pub async fn upsert_http_request<R: Runtime>(
window: &WebviewWindow<R>,
r: HttpRequest,
request: HttpRequest,
update_source: &UpdateSource,
) -> Result<HttpRequest> {
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::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1453,21 +1454,21 @@ pub async fn upsert_http_request<R: Runtime>(
])
.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<R: Runtime>(
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<R: Runtime>(
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(),
}
}

View File

@@ -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<R: Runtime>(
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::<Vec<FsCandidate>>();
// 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<Vec<SyncOp>> {
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))
}

View File

@@ -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),

View File

@@ -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<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,
@@ -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'));

View File

@@ -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({

View File

@@ -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<void>('cmd_new_main_window', { url: location.href });
return;
}
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search,
});
},
});

View File

@@ -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<Omit<Workspace, 'id'>>
>({
mutationKey: ['upsert_workspace'],
mutationFn: (workspace) => invokeCmd<Workspace>('cmd_update_workspace', { workspace }),
onSuccess: async (workspace) => {
const isNew = workspace.createdAt == workspace.updatedAt;
if (isNew) trackEvent('workspace', 'create');
else trackEvent('workspace', 'update');
},
});

View File

@@ -7,7 +7,7 @@ import { invokeCmd } from '../lib/tauri';
export const upsertWorkspaceMeta = createFastMutation<
WorkspaceMeta,
unknown,
Partial<WorkspaceMeta>
WorkspaceMeta | (Partial<Omit<WorkspaceMeta, 'id'>> & { workspaceId: string })
>({
mutationKey: ['update_workspace_meta'],
mutationFn: async (patch) => {

View File

@@ -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]);

View File

@@ -76,7 +76,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
key: 'delete',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
variant: 'danger',
color: 'danger',
onSelect: () => deleteCookieJar.mutateAsync(),
},
]

View File

@@ -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();
}}
>

View File

@@ -278,7 +278,7 @@ function SidebarButton({
},
{
key: 'delete-environment',
variant: 'danger',
color: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />,
onSelect: () => deleteEnvironment.mutate(),

View File

@@ -79,7 +79,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
label: 'Clear',
onSelect: clear,
hidden: !schema,
variant: 'danger',
color: 'danger',
leftSlot: <Icon icon="trash" />,
},
{ type: 'separator', label: 'Setting' },

View File

@@ -156,7 +156,9 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
) : (
<KeyValueRows>
{Object.entries(activeEvent.metadata).map(([key, value]) => (
<KeyValueRow key={key} label={key} value={value} />
<KeyValueRow key={key} label={key}>
{value}
</KeyValueRow>
))}
</KeyValueRows>
)}

View File

@@ -10,7 +10,9 @@ export function ResponseHeaders({ response }: Props) {
<div className="overflow-auto h-full pb-4">
<KeyValueRows>
{response.headers.map((h, i) => (
<KeyValueRow labelColor="primary" key={i} label={h.name} value={h.value} />
<KeyValueRow labelColor="primary" key={i} label={h.name}>
{h.value}
</KeyValueRow>
))}
</KeyValueRows>
</div>

View File

@@ -11,8 +11,12 @@ export function ResponseInfo({ response }: Props) {
return (
<div className="overflow-auto h-full pb-4">
<KeyValueRows>
<KeyValueRow labelColor="info" label="Version" value={response.version} />
<KeyValueRow labelColor="info" label="Remote Address" value={response.remoteAddr} />
<KeyValueRow labelColor="info" label="Version">
{response.version}
</KeyValueRow>
<KeyValueRow labelColor="info" label="Remote Address">
{response.remoteAddr}
</KeyValueRow>
<KeyValueRow
labelColor="info"
label={
@@ -27,12 +31,13 @@ export function ResponseInfo({ response }: Props) {
/>
</div>
}
value={
>
{
<div className="flex">
<span className="select-text cursor-text">{response.url}</span>
</div>
}
/>
</KeyValueRow>
</KeyValueRows>
</div>
);

View File

@@ -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,
})
}
/>
</VStack>
@@ -128,9 +136,33 @@ export function SettingsGeneral() {
<Heading size={2}>App Info</Heading>
<KeyValueRows>
<KeyValueRow label="Version" value={appInfo.version} />
<KeyValueRow label="Data Directory" value={appInfo.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo.appLogDir} />
<KeyValueRow label="Version">{appInfo.version}</KeyValueRow>
<KeyValueRow
label="Data Directory"
rightSlot={
<IconButton
title={revealInFinderText}
icon="folder_open"
size="2xs"
onClick={() => revealItemInDir(appInfo.appDataDir)}
/>
}
>
{appInfo.appDataDir}
</KeyValueRow>
<KeyValueRow
label="Logs Directory"
rightSlot={
<IconButton
title={revealInFinderText}
icon="folder_open"
size="2xs"
onClick={() => revealItemInDir(appInfo.appLogDir)}
/>
}
>
{appInfo.appLogDir}
</KeyValueRow>
</KeyValueRows>
</VStack>
);

View File

@@ -72,7 +72,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
{
key: 'delete-folder',
label: 'Delete',
variant: 'danger',
color: 'danger',
leftSlot: <Icon icon="trash" />,
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,

View File

@@ -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<boolean>(false);

View File

@@ -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 ? <Icon icon="check" /> : <Icon icon="empty" />,
leftSlot: w.id === workspace?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
}));
const extraItems: DropdownItem[] = [
@@ -60,14 +62,25 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
title: 'Workspace Settings',
size: 'md',
render: ({ hide }) => (
<WorkspaceSettingsDialog workspaceId={activeWorkspace?.id ?? null} hide={hide} />
<WorkspaceSettingsDialog workspaceId={workspace?.id ?? null} hide={hide} />
),
});
},
},
{
key: 'reveal-workspace-sync-dir',
label: revealInFinderText,
hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null,
leftSlot: <Icon icon="folder_open" />,
onSelect: async () => {
if (workspaceMeta?.settingSyncDir == null) return;
await revealItemInDir(workspaceMeta.settingSyncDir);
},
},
{
key: 'delete-responses',
label: 'Clear Send History',
color: 'warning',
leftSlot: <Icon icon="history" />,
onSelect: deleteSendHistory,
},
@@ -82,52 +95,50 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
key: 'open-workspace',
label: 'Open Workspace',
leftSlot: <Icon icon="folder" />,
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 }) => <SwitchWorkspaceDialog workspace={workspace} hide={hide} />,
});
},
[switchWorkspace, openWorkspaceNewWindow],
);
showDialog({
id: 'switch-workspace',
size: 'sm',
title: 'Switch Workspace',
render: ({ hide }) => <SwitchWorkspaceDialog workspace={workspace} hide={hide} />,
});
}, []);
return (
<RadioDropdown
items={workspaceItems}
extraItems={extraItems}
onChange={handleChange}
value={activeWorkspace?.id ?? null}
onChange={handleChangeWorkspace}
value={workspace?.id ?? null}
>
<Button
size="sm"
className={classNames(
className,
'text !px-2 truncate',
activeWorkspace === null && 'italic opacity-disabled',
workspace === null && 'italic opacity-disabled',
)}
{...buttonProps}
>
{activeWorkspace?.name ?? 'Workspace'}
{workspace?.name ?? 'Workspace'}
</Button>
</RadioDropdown>
);

View File

@@ -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) {
<Input
label="Workspace Name"
defaultValue={workspace.name}
onChange={(name) => 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) {
<SyncToFilesystemSetting
value={workspaceMeta.settingSyncDir}
onChange={({ value: settingSyncDir }) => {
upsertWorkspaceMeta.mutate({ settingSyncDir });
upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir });
}}
/>
<Separator />

View File

@@ -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}
>

View File

@@ -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,

View File

@@ -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({
<span className="select-text cursor-text">{label}</span>
</td>
<td className="select-none py-0.5 break-all align-top max-w-[15rem]">
<div className="select-text cursor-text max-h-[5rem] overflow-y-auto">{value}</div>
<div className="select-text cursor-text max-h-[5rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
{leftSlot ?? <span aria-hidden />}
{children}
{rightSlot ? <div className="ml-1.5">{rightSlot}</div> : <span aria-hidden />}
</div>
</td>
</>
);

View File

@@ -344,7 +344,7 @@ function PairEditorRow({
key: 'delete',
label: 'Delete',
onSelect: handleDelete,
variant: 'danger',
color: 'danger',
},
],
[handleDelete],

View File

@@ -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,
});
},
});
}

View File

@@ -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<WorkspaceMeta>('cmd_get_workspace_meta', args));
jotaiStore.set(workspaceMetaAtom, await getWorkspaceMeta(workspaceId));
}

View File

@@ -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<Workspace, unknown, Partial<Workspace> | ((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 });
},
});
}

View File

@@ -1,13 +1,8 @@
import type { WorkspaceMeta } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
export const workspaceMetaAtom = atom<WorkspaceMeta>();
export const workspaceMetaAtom = atom<WorkspaceMeta | null>(null);
export function useWorkspaceMeta() {
const workspaceMeta = useAtomValue(workspaceMetaAtom);
if (!workspaceMeta) {
throw new Error('WorkspaceMeta not found');
}
return workspaceMeta;
return useAtomValue(workspaceMetaAtom);
}

9
src-web/lib/reveal.ts Normal file
View File

@@ -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';

View File

@@ -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<Workspace | null>
return workspace;
}
export async function getWorkspaceMeta(workspaceId: string) {
return invokeCmd<WorkspaceMeta>('cmd_get_workspace_meta', { workspaceId });
}
export async function listWorkspaces(): Promise<Workspace[]> {
const workspaces: Workspace[] = (await invokeCmd('cmd_list_workspaces')) ?? [];
return workspaces;