Refactor desktop app into separate client and proxy apps

This commit is contained in:
Gregory Schier
2026-03-06 09:23:19 -08:00
parent e26705f016
commit 6915778c06
613 changed files with 1356 additions and 812 deletions

View File

@@ -0,0 +1,170 @@
import { createWorkspaceModel, type Folder, modelTypeLabel } from '@yaakapp-internal/models';
import { applySync, calculateSync } from '@yaakapp-internal/sync';
import { Banner } from '../components/core/Banner';
import { Button } from '../components/core/Button';
import { InlineCode } from '../components/core/InlineCode';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
TruncatedWideTableCell,
} from '../components/core/Table';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { pluralizeCount } from '../lib/pluralize';
import { showPrompt } from '../lib/prompt';
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
export const createFolder = createFastMutation<
string | null,
void,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error("Cannot create folder when there's no active workspace");
}
if (!patch.name) {
const name = await showPrompt({
id: 'new-folder',
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
confirmText: 'Create',
placeholder: 'Name',
});
if (name == null) return null;
patch.name = name;
}
patch.sortPriority = patch.sortPriority || -Date.now();
const id = await createWorkspaceModel({ model: 'folder', workspaceId, ...patch });
return id;
},
});
export const syncWorkspace = createFastMutation<
void,
void,
{ workspaceId: string; syncDir: string; force?: boolean }
>({
mutationKey: [],
mutationFn: async ({ workspaceId, syncDir, force }) => {
const ops = (await calculateSync(workspaceId, syncDir)) ?? [];
if (ops.length === 0) {
console.log('Nothing to sync', workspaceId, syncDir);
return;
}
console.log('Syncing workspace', workspaceId, syncDir, ops);
const dbOps = ops.filter((o) => o.type.startsWith('db'));
if (dbOps.length === 0) {
await applySync(workspaceId, syncDir, ops);
return;
}
const isDeletingWorkspace = ops.some(
(o) => o.type === 'dbDelete' && o.model.model === 'workspace',
);
console.log('Directory changes detected', { dbOps, ops });
if (force) {
await applySync(workspaceId, syncDir, ops);
return;
}
showDialog({
id: 'commit-sync',
title: 'Changes Detected',
size: 'md',
render: ({ hide }) => (
<form
className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3"
onSubmit={async (e) => {
e.preventDefault();
await applySync(workspaceId, syncDir, ops);
hide();
}}
>
{isDeletingWorkspace ? (
<Banner color="danger">
🚨 <strong>Changes contain a workspace deletion!</strong>
</Banner>
) : (
<span />
)}
<p>
{pluralizeCount('file', dbOps.length)} in the directory{' '}
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
</p>
<Table scrollable className="my-4">
<TableHead>
<TableRow>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Operation</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{dbOps.map((op, i) => {
let name: string;
let label: string;
let color: string;
let model: string;
if (op.type === 'dbCreate') {
label = 'create';
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-success';
model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbUpdate') {
label = 'update';
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-info';
model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbDelete') {
label = 'delete';
name = resolvedModelNameWithFolders(op.model);
color = 'text-danger';
model = modelTypeLabel(op.model);
} else {
return null;
}
return (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<TableRow key={i}>
<TableCell className="text-text-subtle">{model}</TableCell>
<TruncatedWideTableCell>{name}</TruncatedWideTableCell>
<TableCell className="text-right">
<InlineCode className={color}>{label}</InlineCode>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<footer className="py-3 flex flex-row-reverse items-center gap-3">
<Button type="submit" color="primary">
Apply Changes
</Button>
<Button onClick={hide} color="secondary">
Cancel
</Button>
</footer>
</form>
),
});
},
});

View File

@@ -0,0 +1,51 @@
import type { Environment } from '@yaakapp-internal/models';
import { CreateEnvironmentDialog } from '../components/CreateEnvironmentDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
export const createSubEnvironmentAndActivate = createFastMutation<
string | null,
unknown,
Environment | null
>({
mutationKey: ['create_environment'],
mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {
throw new Error('No base environment passed');
}
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error('Cannot create environment when no active workspace');
}
return new Promise<string | null>((resolve) => {
showDialog({
id: 'new-environment',
title: 'New Environment',
description: 'Create multiple environments with different sets of variables',
size: 'sm',
onClose: () => resolve(null),
render: ({ hide }) => (
<CreateEnvironmentDialog
workspaceId={workspaceId}
hide={hide}
onCreate={(id: string) => {
resolve(id);
}}
/>
),
});
});
},
onSuccess: async (environmentId) => {
if (environmentId == null) {
return; // Was not created
}
setWorkspaceSearchParams({ environment_id: environmentId });
},
});

View File

@@ -0,0 +1,8 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from '@yaakapp-internal/ws';
import { createFastMutation } from '../hooks/useFastMutation';
export const deleteWebsocketConnections = createFastMutation({
mutationKey: ['delete_websocket_connections'],
mutationFn: async (request: WebsocketRequest) => cmdDeleteWebsocketConnections(request.id),
});

View File

@@ -0,0 +1,35 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { pluralizeCount } from '../lib/pluralize';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
export const moveToWorkspace = createFastMutation({
mutationKey: ['move_workspace'],
mutationFn: async (requests: (HttpRequest | GrpcRequest | WebsocketRequest)[]) => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return;
if (requests.length === 0) return;
const title =
requests.length === 1
? 'Move Request'
: `Move ${pluralizeCount('Request', requests.length)}`;
showDialog({
id: 'change-workspace',
title,
size: 'sm',
render: ({ hide }) => (
<MoveToWorkspaceDialog
onDone={hide}
requests={requests}
activeWorkspaceId={activeWorkspaceId}
/>
),
});
},
});

View File

@@ -0,0 +1,17 @@
import { getModel } from '@yaakapp-internal/models';
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
import { showDialog } from '../lib/dialog';
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
const folder = getModel('folder', folderId);
if (folder == null) return;
showDialog({
id: 'folder-settings',
title: null,
size: 'lg',
className: 'h-[50rem]',
noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
});
}

View File

@@ -0,0 +1,30 @@
import type { SettingsTab } from '../components/Settings/Settings';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
// Allow tab with optional subtab (e.g., "plugins:installed")
type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null;
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({
mutationKey: ['open_settings'],
mutationFn: async (tab) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
const location = router.buildLocation({
to: '/workspaces/$workspaceId/settings',
params: { workspaceId },
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
});
await invokeCmd('cmd_new_child_window', {
url: location.href,
label: 'settings',
title: 'Yaak Settings',
innerSize: [750, 600],
});
},
});

View File

@@ -0,0 +1,27 @@
import { applySync, calculateSyncFsOnly } from '@yaakapp-internal/sync';
import { createFastMutation } from '../hooks/useFastMutation';
import { showSimpleAlert } from '../lib/alert';
import { router } from '../lib/router';
export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
mutationKey: [],
mutationFn: async (dir) => {
const ops = await calculateSyncFsOnly(dir);
const workspace = ops
.map((o) => (o.type === 'dbCreate' && o.fs.model.type === 'workspace' ? o.fs.model : null))
.filter((m) => m)[0];
if (workspace == null) {
showSimpleAlert('Failed to Open', 'No workspace found in directory');
return;
}
await applySync(workspace.id, dir, ops);
router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: workspace.id },
});
},
});

View File

@@ -0,0 +1,19 @@
import type { WorkspaceSettingsTab } from '../components/WorkspaceSettingsDialog';
import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
showDialog({
id: 'workspace-settings',
size: 'md',
className: 'h-[calc(100vh-5rem)] !max-h-[40rem]',
noPadding: true,
render: ({ hide }) => (
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
),
});
}

View File

@@ -0,0 +1,43 @@
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<
void,
unknown,
{
workspaceId: string;
inNewWindow: boolean;
}
>({
mutationKey: ['open_workspace'],
mutationFn: async ({ workspaceId, inNewWindow }) => {
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,
});
},
});