Filesystem Sync (#142)

This commit is contained in:
Gregory Schier
2025-01-03 20:41:00 -08:00
committed by GitHub
parent 6ad27c4458
commit 31440eea76
159 changed files with 4296 additions and 1016 deletions

View File

@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCommands } from '../hooks/useCommands';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
@@ -53,21 +54,22 @@ type CommandPaletteItem = {
const MAX_PER_GROUP = 8;
export function CommandPalette({ onClose }: { onClose: () => void }) {
export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions();
const workspaces = useWorkspaces();
const { subEnvironments } = useEnvironments();
const createWorkspace = useCreateWorkspace();
const recentEnvironments = useRecentEnvironments();
const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests();
const activeRequest = useActiveRequest();
const [recentRequests] = useRecentRequests();
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { createFolder } = useCommands();
const [activeCookieJar] = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
@@ -91,13 +93,18 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
{
key: 'app.create',
label: 'Create Workspace',
onSelect: createWorkspace.mutate,
onSelect: createWorkspace,
},
{
key: 'http_request.create',
label: 'Create HTTP Request',
onSelect: () => createHttpRequest.mutate({}),
},
{
key: 'folder.create',
label: 'Create Folder',
onSelect: () => createFolder.mutate({}),
},
{
key: 'cookies.show',
label: 'Show Cookies',
@@ -183,9 +190,10 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
activeRequest,
baseEnvironment,
createEnvironment,
createFolder,
createGrpcRequest,
createHttpRequest,
createWorkspace.mutate,
createWorkspace,
deleteRequest.mutate,
dialog,
httpRequestActions,
@@ -230,9 +238,14 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
}, [subEnvironments, recentEnvironments]);
const sortedWorkspaces = useMemo(() => {
const r = [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces.indexOf(a.id);
const bRecentIndex = recentWorkspaces.indexOf(b.id);
if (recentWorkspaces == null) {
// Should never happen
return workspaces;
}
const r = [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces?.indexOf(a.id);
const bRecentIndex = recentWorkspaces?.indexOf(b.id);
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
@@ -309,7 +322,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
for (const w of sortedWorkspaces) {
workspaceGroup.items.push({
key: `switch-workspace-${w.id}`,
label: w.id + ' - ' + w.name,
label: w.name,
onSelect: () => openWorkspace.mutate({ workspaceId: w.id, inNewWindow: false }),
});
}

View File

@@ -0,0 +1,54 @@
import { useState } from 'react';
import { useCommands } from '../hooks/useCommands';
import { Button } from './core/Button';
import { PlainInput } from './core/PlainInput';
import { VStack } from './core/Stacks';
import { MarkdownEditor } from './MarkdownEditor';
import { SelectFile } from './SelectFile';
interface Props {
hide: () => void;
}
export function CreateWorkspaceDialog({ hide }: Props) {
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [settingSyncDir, setSettingSyncDir] = useState<string | null>(null);
const { createWorkspace } = useCommands();
return (
<VStack
as="form"
space={3}
alignItems="start"
className="pb-3 max-h-[50vh]"
onSubmit={async (e) => {
e.preventDefault();
await createWorkspace.mutateAsync({ name, description, settingSyncDir });
hide();
}}
>
<PlainInput label="Workspace Name" defaultValue={name} onChange={setName} />
<MarkdownEditor
name="workspace-description"
placeholder="Workspace description"
className="min-h-[10rem] max-h-[25rem] border border-border px-2"
defaultValue={description}
stateKey={null}
onChange={setDescription}
heightMode="auto"
/>
<div>
<SelectFile
directory
noun="Sync Directory"
filePath={settingSyncDir}
onChange={({ filePath }) => setSettingSyncDir(filePath)}
/>
</div>
<Button type="submit">Create Workspace</Button>
</VStack>
);
}

View File

@@ -1,6 +1,9 @@
import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId } from '../hooks/useActiveCookieJar';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins';
import {
useEnsureActiveCookieJar,
useSubscribeActiveCookieJarId,
} from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
@@ -13,10 +16,10 @@ import { useHotKey } from '../hooks/useHotKey';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { usePrompt } from '../hooks/usePrompt';
import {useRecentCookieJars, useSubscribeRecentCookieJars} from '../hooks/useRecentCookieJars';
import {useRecentEnvironments, useSubscribeRecentEnvironments} from '../hooks/useRecentEnvironments';
import { useSubscribeRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useSubscribeRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
import {useRecentWorkspaces, useSubscribeRecentWorkspaces} from '../hooks/useRecentWorkspaces';
import { useSubscribeRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
@@ -42,10 +45,6 @@ export function GlobalHooks() {
useSubscribeRecentEnvironments();
useSubscribeRecentCookieJars();
// Include here so they always update, even if no component references them
useRecentWorkspaces();
useRecentEnvironments();
useRecentCookieJars();
useSyncWorkspaceChildModels();
useSubscribeTemplateFunctions();

View File

@@ -120,7 +120,7 @@ export function GrpcConnectionLayout({ style }: Props) {
) : messages.length >= 0 ? (
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />
) : (
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.focus', 'urlBar.focus']} />
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.focus', 'url_bar.focus']} />
)}
</div>
)

View File

@@ -64,7 +64,7 @@ export function MarkdownEditor({
defaultValue.length === 0 ? (
<p className="text-text-subtle">No description</p>
) : (
<Prose className="max-w-xl">
<Prose className="max-w-xl overflow-y-auto max-h-full">
<Markdown
remarkPlugins={[remarkGfm]}
components={{

View File

@@ -64,17 +64,17 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
</InlineCode>
</>
),
action: (
action: ({ hide }) => (
<Button
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={async () => {
toast.hide('workspace-moved');
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: selectedWorkspaceId },
});
hide();
}}
>
Switch to Workspace

View File

@@ -12,7 +12,7 @@ export function RedirectToLatestWorkspace() {
const navigate = useNavigate();
useEffect(() => {
if (workspaces.length === 0) {
if (workspaces.length === 0 || recentWorkspaces == null) {
console.log('No workspaces found to redirect to. Skipping.');
return;
}

View File

@@ -105,7 +105,7 @@ export const ResponsePane = memo(function ResponsePane({
>
{activeResponse == null ? (
<HotKeyList
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'urlBar.focus']}
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'url_bar.focus']}
/>
) : (
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">

View File

@@ -6,7 +6,7 @@ import { Button } from './core/Button';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
type Props = ButtonProps & {
type Props = Omit<ButtonProps, 'type'> & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null;
directory?: boolean;

View File

@@ -7,7 +7,7 @@ import type {
TemplateFunctionHttpRequestArg,
TemplateFunctionSelectArg,
TemplateFunctionTextArg,
} from '@yaakapp-internal/plugin';
} from '@yaakapp-internal/plugins';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';

View File

@@ -1,9 +1,9 @@
import type { ShowToastRequest } from '@yaakapp-internal/plugin';
import type { ShowToastRequest } from '@yaakapp-internal/plugins';
import { AnimatePresence } from 'framer-motion';
import React, {type ReactNode, useContext, useMemo, useRef, useState} from 'react';
import React, { type ReactNode, useContext, useMemo, useRef, useState } from 'react';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { generateId } from '../lib/generateId';
import {Toast, type ToastProps} from './core/Toast';
import { Toast, type ToastProps } from './core/Toast';
import { Portal } from './Portal';
import { ToastContext } from './ToastContext';
@@ -29,7 +29,6 @@ export interface Actions {
hide: (id: string) => void;
}
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<ToastState['toasts']>([]);
const timeoutRef = useRef<NodeJS.Timeout>();

View File

@@ -49,7 +49,7 @@ export const UrlBar = memo(function UrlBar({
const inputRef = useRef<EditorView>(null);
const [isFocused, setIsFocused] = useState<boolean>(false);
useHotKey('urlBar.focus', () => {
useHotKey('url_bar.focus', () => {
const head = inputRef.current?.state.doc.length ?? 0;
inputRef.current?.dispatch({
selection: { anchor: 0, head },

View File

@@ -1,21 +1,28 @@
import { applySync, calculateSync } from '@yaakapp-internal/sync';
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useConfirm } from '../hooks/useConfirm';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
import { useDialog } from '../hooks/useDialog';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useSettings } from '../hooks/useSettings';
import { useToast } from '../hooks/useToast';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { pluralizeCount } from '../lib/pluralize';
import { getWorkspace } from '../lib/store';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
import { VStack } from './core/Stacks';
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
import { WorkspaceSettingsDialog } from './WorkpaceSettingsDialog';
import { WorkspaceSettingsDialog } from './WorkspaceSettingsDialog';
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
@@ -25,10 +32,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}: Props) {
const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null;
const { mutate: createWorkspace } = useCreateWorkspace();
const createWorkspace = useCreateWorkspace();
const { mutate: deleteSendHistory } = useDeleteSendHistory();
const dialog = useDialog();
const confirm = useConfirm();
const toast = useToast();
const settings = useSettings();
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
@@ -46,7 +54,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
key: w.id,
label: w.name,
value: w.id,
leftSlot: w.id === activeWorkspaceId ? <Icon icon="check" /> : <Icon icon="empty" />,
leftSlot: w.id === activeWorkspace?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
}));
const extraItems: DropdownItem[] = [
@@ -54,6 +62,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
key: 'workspace-settings',
label: 'Workspace Settings',
leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show',
onSelect: async () => {
dialog.show({
id: 'workspace-settings',
@@ -63,6 +72,96 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
});
},
},
{
key: 'sync',
label: 'Sync Workspace',
leftSlot: <Icon icon="folder_sync" />,
hidden: !activeWorkspace?.settingSyncDir,
onSelect: async () => {
if (activeWorkspace == null) return;
const ops = await calculateSync(activeWorkspace);
if (ops.length === 0) {
toast.show({
id: 'no-sync-changes',
message: 'No changes detected for sync',
});
return;
}
const dbChanges = ops.filter((o) => o.type.startsWith('db'));
if (dbChanges.length === 0) {
await applySync(activeWorkspace, ops);
toast.show({
id: 'applied-sync-changes',
message: `Applied ${pluralizeCount('change', ops.length)}`,
});
return;
}
const confirmed = await confirm({
id: 'commit-sync',
title: 'Filesystem Changes Detected',
confirmText: 'Apply Changes',
description: (
<VStack space={3}>
<p>
Some files in the directory have changed. Do you want to apply the updates to your
workspace?
</p>
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th className="py-1 text-left">Name</th>
<th className="py-1 text-right pl-4">Operation</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">
{dbChanges.map((op, i) => {
let name = '';
let label = '';
let color = '';
if (op.type === 'dbCreate') {
label = 'create';
name = fallbackRequestName(op.fs.model);
color = 'text-success';
} else if (op.type === 'dbUpdate') {
label = 'update';
name = fallbackRequestName(op.fs.model);
color = 'text-info';
} else if (op.type === 'dbDelete') {
label = 'delete';
name = fallbackRequestName(op.model);
color = 'text-danger';
} else {
return null;
}
return (
<tr key={i} className="text-text">
<td className="py-1">{name}</td>
<td className="py-1 pl-4 text-right">
<InlineCode className={color}>{label}</InlineCode>
</td>
</tr>
);
})}
</tbody>
</table>
</VStack>
),
});
if (confirmed) {
await applySync(activeWorkspace, ops);
toast.show({
id: 'applied-confirmed-sync-changes',
message: `Applied ${pluralizeCount('change', ops.length)}`,
});
}
},
},
{
key: 'delete-responses',
label: 'Clear Send History',
@@ -80,12 +179,13 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
return { workspaceItems, extraItems };
}, [
activeWorkspace?.id,
activeWorkspaceId,
createWorkspace,
deleteSendHistory,
dialog,
orderedWorkspaces,
activeWorkspace,
deleteSendHistory,
createWorkspace,
dialog,
confirm,
toast,
]);
const handleChange = useCallback(
@@ -115,7 +215,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
items={workspaceItems}
extraItems={extraItems}
onChange={handleChange}
value={activeWorkspaceId}
value={activeWorkspace?.id ?? null}
>
<Button
size="sm"

View File

@@ -5,15 +5,16 @@ import { Button } from './core/Button';
import { PlainInput } from './core/PlainInput';
import { VStack } from './core/Stacks';
import { MarkdownEditor } from './MarkdownEditor';
import { SelectFile } from './SelectFile';
interface Props {
workspaceId: string | null;
}
export function WorkspaceSettingsDialog({ workspaceId }: Props) {
const updateWorkspace = useUpdateWorkspace(workspaceId ?? null);
const workspaces = useWorkspaces();
const workspace = workspaces.find((w) => w.id === workspaceId);
const { mutate: updateWorkspace } = useUpdateWorkspace(workspaceId ?? null);
const { mutate: deleteWorkspace } = useDeleteWorkspace();
if (workspace == null) return null;
@@ -23,21 +24,27 @@ export function WorkspaceSettingsDialog({ workspaceId }: Props) {
<PlainInput
label="Workspace Name"
defaultValue={workspace.name}
onChange={(name) => updateWorkspace.mutate({ name })}
onChange={(name) => updateWorkspace({ name })}
/>
<MarkdownEditor
name="workspace-description"
placeholder="Workspace description"
className="min-h-[10rem] border border-border px-2"
className="min-h-[10rem] max-h-[25rem] border border-border px-2"
defaultValue={workspace.description}
stateKey={`description.${workspace.id}`}
onChange={(description) => updateWorkspace.mutate({ description })}
onChange={(description) => updateWorkspace({ description })}
heightMode="auto"
/>
<VStack space={3} className="mt-3" alignItems="start">
<Button onClick={() => deleteWorkspace()} color="danger" variant="border">
<SelectFile
directory
noun="Sync Directory"
filePath={workspace.settingSyncDir}
onChange={({ filePath: settingSyncDir }) => updateWorkspace({ settingSyncDir })}
/>
<Button onClick={() => deleteWorkspace()} color="danger" variant="border" size="sm">
Delete Workspace
</Button>
</VStack>

View File

@@ -4,7 +4,7 @@ import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import type { EnvironmentVariable } from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugin';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { EditorView } from 'codemirror';
import type { MutableRefObject, ReactNode } from 'react';

View File

@@ -41,6 +41,8 @@ const icons = {
file_code: lucide.FileCodeIcon,
filter: lucide.FilterIcon,
flask: lucide.FlaskConicalIcon,
folder: lucide.FolderIcon,
folder_sync: lucide.FolderSyncIcon,
folder_input: lucide.FolderInputIcon,
folder_output: lucide.FolderOutputIcon,
git_branch: lucide.GitBranchIcon,

View File

@@ -1,4 +1,4 @@
import type { ShowToastRequest } from '@yaakapp-internal/plugin';
import type { ShowToastRequest } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
@@ -15,7 +15,7 @@ export interface ToastProps {
onClose: () => void;
className?: string;
timeout: number | null;
action?: ReactNode;
action?: (args: { hide: () => void }) => ReactNode;
icon?: ShowToastRequest['icon'];
color?: ShowToastRequest['color'];
}
@@ -59,15 +59,15 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
`x-theme-toast x-theme-toast--${color}`,
'pointer-events-auto overflow-hidden',
'relative pointer-events-auto bg-surface text-text rounded-lg',
'border border-border shadow-lg max-w-[30rem]',
'border border-border shadow-lg w-[25rem]',
'grid grid-cols-[1fr_auto]',
)}
>
<div className="px-3 py-3 flex items-center gap-2">
{toastIcon && <Icon icon={toastIcon} className="text-text-subtle" />}
<VStack space={2}>
<div className="px-3 py-3 flex items-start gap-2 w-full">
{toastIcon && <Icon icon={toastIcon} className="mt-1 text-text-subtle" />}
<VStack space={2} className="w-full">
<div>{children}</div>
{action}
{action?.({ hide: onClose })}
</VStack>
</div>

View File

@@ -1,6 +1,6 @@
// Listen for settings changes, the re-compute theme
import { listen } from '@tauri-apps/api/event';
import type { ModelPayload } from './hooks/useSyncModelStores';
import type { ModelPayload } from '@yaakapp-internal/models';
import { getSettings } from './lib/store';
function setFontSizeOnDocument(fontSize: number) {

View File

@@ -1,4 +1,4 @@
import type { PromptTextRequest } from '@yaakapp-internal/plugin';
import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import { Button } from '../components/core/Button';

View File

@@ -0,0 +1,84 @@
import { useNavigate } from '@tanstack/react-router';
import type { Folder, Workspace } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { trackEvent } from '../lib/analytics';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { createFastMutation } from './useFastMutation';
import { foldersAtom } from './useFolders';
import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores';
import { useToast } from './useToast';
import { workspacesAtom } from './useWorkspaces';
function makeCommands({
navigate,
prompt,
}: {
navigate: ReturnType<typeof useNavigate>;
prompt: ReturnType<typeof usePrompt>;
toast: ReturnType<typeof useToast>;
}) {
return {
createWorkspace: createFastMutation<Workspace, void, Partial<Workspace>>({
mutationKey: ['create_workspace'],
mutationFn: (patch) => invokeCmd<Workspace>('cmd_update_workspace', { workspace: patch }),
onSuccess: async (workspace) => {
// Optimistic update
jotaiStore.set(workspacesAtom, updateModelList(workspace));
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: workspace.id },
});
},
onSettled: () => trackEvent('workspace', 'create'),
}),
createFolder: createFastMutation<
Folder | null,
void,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
const workspaceId = getActiveWorkspaceId();
if (workspaceId == null) {
throw new Error("Cannot create folder when there's no active workspace");
}
if (!patch.name) {
const name = await prompt({
id: 'new-folder',
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
confirmText: 'Create',
placeholder: 'Name',
});
if (name == null) return null;
patch.name = name;
}
patch.sortPriority = patch.sortPriority || -Date.now();
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
},
onSuccess: async (folder) => {
if (folder == null) return;
// Optimistic update
jotaiStore.set(foldersAtom, updateModelList(folder));
},
onSettled: () => trackEvent('folder', 'create'),
}),
} as const;
}
export function useCommands() {
const navigate = useNavigate();
const toast = useToast();
const prompt = usePrompt();
return useMemo(() => makeCommands({ navigate, toast, prompt }), [navigate, prompt, toast]);
}

View File

@@ -4,7 +4,7 @@ import { Icon } from '../components/core/Icon';
import { generateId } from '../lib/generateId';
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
import { getActiveRequest } from './useActiveRequest';
import { useCreateFolder } from './useCreateFolder';
import {useCommands} from "./useCommands";
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
import { useCreateHttpRequest } from './useCreateHttpRequest';
@@ -19,7 +19,7 @@ export function useCreateDropdownItems({
} = {}): () => DropdownItem[] {
const { mutate: createHttpRequest } = useCreateHttpRequest();
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const { mutate: createFolder } = useCreateFolder();
const { createFolder } = useCommands();
return useCallback((): DropdownItem[] => {
const folderId =
@@ -62,7 +62,7 @@ export function useCreateDropdownItems({
key: 'create-folder',
label: 'Folder',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createFolder({ folderId }),
onSelect: () => createFolder.mutate({ folderId }),
},
]) as DropdownItem[]),
];

View File

@@ -15,7 +15,6 @@ export function useCreateEnvironment() {
const setEnvironments = useSetAtom(environmentsAtom);
return useFastMutation<Environment | null, unknown, Environment | null>({
toastyError: true,
mutationKey: ['create_environment'],
mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {

View File

@@ -1,52 +0,0 @@
import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { foldersAtom } from './useFolders';
import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores';
export function useCreateFolder() {
const prompt = usePrompt();
const setFolders = useSetAtom(foldersAtom);
return useFastMutation<
Folder | null,
unknown,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
const workspaceId = getActiveWorkspaceId();
if (workspaceId == null) {
throw new Error("Cannot create folder when there's no active workspace");
}
if (!patch.name) {
const name = await prompt({
id: 'new-folder',
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
confirmText: 'Create',
placeholder: 'Name',
});
if (name == null) return null;
patch.name = name;
}
patch.sortPriority = patch.sortPriority || -Date.now();
return await invokeCmd('cmd_create_folder', { workspaceId, ...patch });
},
onSuccess: (folder) => {
if (folder == null) return;
// Optimistic update
setFolders(updateModelList(folder));
},
onSettled: () => trackEvent('folder', 'create'),
});
}

View File

@@ -1,43 +0,0 @@
import { useNavigate } from '@tanstack/react-router';
import type { Workspace } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores';
import { workspacesAtom } from './useWorkspaces';
export function useCreateWorkspace() {
const prompt = usePrompt();
const setWorkspaces = useSetAtom(workspacesAtom);
const navigate = useNavigate();
return useFastMutation<Workspace | null, void, void>({
mutationKey: ['create_workspace'],
mutationFn: async () => {
const name = await prompt({
id: 'new-workspace',
label: 'Name',
defaultValue: 'My Workspace',
title: 'New Workspace',
placeholder: 'My Workspace',
confirmText: 'Create',
});
if (name == null) {
return null;
}
return invokeCmd<Workspace>('cmd_create_workspace', { name });
},
onSuccess: async (workspace) => {
if (workspace == null) return;
// Optimistic update
setWorkspaces(updateModelList(workspace));
navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: workspace.id },
});
},
});
}

View File

@@ -0,0 +1,16 @@
import { useCallback } from 'react';
import { CreateWorkspaceDialog } from '../components/CreateWorkspaceDialog';
import { useDialog } from './useDialog';
export function useCreateWorkspace() {
const dialog = useDialog();
return useCallback(() => {
dialog.show({
id: 'create-workspace',
title: 'Create Workspace',
size: 'md',
render: ({ hide }) => <CreateWorkspaceDialog hide={hide} />,
});
}, [dialog]);
}

View File

@@ -1,59 +1,58 @@
import type { MutationKey } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useToast } from './useToast';
import { useMemo } from 'react';
export function useFastMutation<TData = unknown, TError = unknown, TVariables = void>({
mutationKey,
mutationFn,
onSuccess,
onError,
onSettled,
toastyError,
}: {
interface MutationOptions<TData, TError, TVariables> {
mutationKey: MutationKey;
mutationFn: (vars: TVariables) => Promise<TData>;
onSettled?: () => void;
onError?: (err: TError) => void;
onSuccess?: (data: TData) => void;
toastyError?: boolean;
}) {
const toast = useToast();
const mutateAsync = useCallback(
async (variables: TVariables) => {
try {
const data = await mutationFn(variables);
onSuccess?.(data);
return data;
} catch (err: unknown) {
const e = err as TError;
console.log('Fast mutation error', mutationKey, e);
onError?.(e);
if (toastyError) {
toast.show({
id: 'error-' + mutationKey.join('.'),
color: 'danger',
timeout: 8000,
message: String(e),
});
}
} finally {
onSettled?.();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
mutationKey,
);
const mutate = useCallback(
(variables: TVariables) => {
setTimeout(() => mutateAsync(variables));
},
[mutateAsync],
);
return {
mutate,
mutateAsync,
};
}
type CallbackMutationOptions<TData, TError, TVariables> = Omit<
MutationOptions<TData, TError, TVariables>,
'mutationKey' | 'mutationFn'
>;
export function createFastMutation<TData = unknown, TError = unknown, TVariables = void>(
defaultArgs: MutationOptions<TData, TError, TVariables>,
) {
const mutateAsync = async (
variables: TVariables,
args?: CallbackMutationOptions<TData, TError, TVariables>,
) => {
const { mutationKey, mutationFn, onSuccess, onError, onSettled } = {
...defaultArgs,
...args,
};
try {
const data = await mutationFn(variables);
onSuccess?.(data);
return data;
} catch (err: unknown) {
const e = err as TError;
console.log('Fast mutation error', mutationKey, e);
onError?.(e);
} finally {
onSettled?.();
}
};
const mutate = (
variables: TVariables,
args?: CallbackMutationOptions<TData, TError, TVariables>,
) => {
setTimeout(() => mutateAsync(variables, args));
};
return { mutateAsync, mutate };
}
export function useFastMutation<TData = unknown, TError = unknown, TVariables = void>(
defaultArgs: MutationOptions<TData, TError, TVariables>,
) {
return useMemo(() => {
return createFastMutation(defaultArgs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, defaultArgs.mutationKey);
}

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { FilterResponse } from '@yaakapp-internal/plugin';
import type { FilterResponse } from '@yaakapp-internal/plugins';
import { invokeCmd } from '../lib/tauri';
export function useFilterResponse({

View File

@@ -7,9 +7,13 @@ import { useOsInfo } from './useOsInfo';
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
export type HotkeyAction =
| 'app.zoom_in'
| 'app.zoom_out'
| 'app.zoom_reset'
| 'command_palette.toggle'
| 'environmentEditor.toggle'
| 'hotkeys.showHelp'
| 'grpc_request.send'
| 'hotkeys.showHelp'
| 'http_request.create'
| 'http_request.duplicate'
| 'http_request.send'
@@ -18,13 +22,14 @@ export type HotkeyAction =
| 'request_switcher.toggle'
| 'settings.show'
| 'sidebar.focus'
| 'urlBar.focus'
| 'command_palette.toggle'
| 'app.zoom_in'
| 'app.zoom_out'
| 'app.zoom_reset';
| 'url_bar.focus'
| 'workspace_settings.show';
const hotkeys: Record<HotkeyAction, string[]> = {
'app.zoom_in': ['CmdCtrl+='],
'app.zoom_out': ['CmdCtrl+-'],
'app.zoom_reset': ['CmdCtrl+0'],
'command_palette.toggle': ['CmdCtrl+k'],
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
@@ -36,14 +41,15 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'request_switcher.toggle': ['CmdCtrl+p'],
'settings.show': ['CmdCtrl+,'],
'sidebar.focus': ['CmdCtrl+b'],
'urlBar.focus': ['CmdCtrl+l'],
'command_palette.toggle': ['CmdCtrl+k'],
'app.zoom_in': ['CmdCtrl+='],
'app.zoom_out': ['CmdCtrl+-'],
'app.zoom_reset': ['CmdCtrl+0'],
'url_bar.focus': ['CmdCtrl+l'],
'workspace_settings.show': ['CmdCtrl+Shift+,'],
};
const hotkeyLabels: Record<HotkeyAction, string> = {
'app.zoom_in': 'Zoom In',
'app.zoom_out': 'Zoom Out',
'app.zoom_reset': 'Zoom to Actual Size',
'command_palette.toggle': 'Toggle Command Palette',
'environmentEditor.toggle': 'Edit Environments',
'grpc_request.send': 'Send Message',
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
@@ -55,11 +61,8 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'request_switcher.toggle': 'Toggle Request Switcher',
'settings.show': 'Open Settings',
'sidebar.focus': 'Focus or Toggle Sidebar',
'urlBar.focus': 'Focus URL',
'command_palette.toggle': 'Toggle Command Palette',
'app.zoom_in': 'Zoom In',
'app.zoom_out': 'Zoom Out',
'app.zoom_reset': 'Zoom to Actual Size',
'url_bar.focus': 'Focus URL',
'workspace_settings.show': 'Open Workspace Settings',
};
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
@@ -165,7 +168,7 @@ export function useHotKeyLabel(action: HotkeyAction): string {
export function useFormattedHotkey(action: HotkeyAction | null): string[] | null {
const osInfo = useOsInfo();
const trigger = action != null ? hotkeys[action]?.[0] ?? null : null;
const trigger = action != null ? (hotkeys[action]?.[0] ?? null) : null;
if (trigger == null || osInfo == null) {
return null;
}

View File

@@ -4,7 +4,7 @@ import type {
CallHttpRequestActionRequest,
GetHttpRequestActionsResponse,
HttpRequestAction,
} from '@yaakapp-internal/plugin';
} from '@yaakapp-internal/plugins';
import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins';
import { useMemo } from 'react';

View File

@@ -8,7 +8,7 @@ import { buildKeyValueKey, extractKeyValueOrFallback, setKeyValue } from '../lib
const DEFAULT_NAMESPACE = 'global';
export const keyValuesAtom = atom<KeyValue[]>([]);
export const keyValuesAtom = atom<KeyValue[] | null>(null);
export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE,
@@ -32,7 +32,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
const keyValues = useAtomValue(keyValuesAtom);
const keyValue =
keyValues?.find((kv) => buildKeyValueKey(kv.key) === buildKeyValueKey(key)) ?? null;
const value = extractKeyValueOrFallback(keyValue, fallback);
const value = keyValues == null ? null : extractKeyValueOrFallback(keyValue, fallback);
const isLoading = keyValues == null;
const { mutateAsync } = useMutation<void, unknown, T>({
@@ -43,7 +43,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
const set = useCallback(
async (valueOrUpdate: ((v: T) => T) | T) => {
if (typeof valueOrUpdate === 'function') {
const newV = valueOrUpdate(value);
const newV = valueOrUpdate(value ?? fallback);
if (newV === value) return;
await mutateAsync(newV);
} else {

View File

@@ -1,8 +1,8 @@
import { open } from '@tauri-apps/plugin-shell';
import { Button } from '../components/core/Button';
import { useToast } from './useToast';
import { invokeCmd } from '../lib/tauri';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { useToast } from './useToast';
export function useNotificationToast() {
const toast = useToast();
@@ -29,14 +29,14 @@ export function useNotificationToast() {
message: payload.message,
color: 'custom',
onClose: () => markRead(payload.id),
action:
action: ({ hide }) =>
actionLabel && actionUrl ? (
<Button
size="xs"
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={() => {
toast.hide(payload.id);
hide();
return open(actionUrl);
}}
>

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { BootResponse } from '@yaakapp-internal/plugin';
import type { BootResponse } from '@yaakapp-internal/plugins';
import { invokeCmd } from '../lib/tauri';
export function usePluginInfo(id: string) {

View File

@@ -11,13 +11,17 @@ const fallback: string[] = [];
export function useRecentWorkspaces() {
const workspaces = useWorkspaces();
const { value } = useKeyValue<string[]>({ key: kvKey(), namespace, fallback });
const { value, isLoading } = useKeyValue<string[]>({ key: kvKey(), namespace, fallback });
const onlyValidIds = useMemo(
() => value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
[value, workspaces],
);
console.log("HELLO", {isLoading, value})
if (isLoading) return null;
return onlyValidIds;
}
@@ -25,15 +29,18 @@ export function useSubscribeRecentWorkspaces() {
useEffect(() => {
return jotaiStore.sub(activeWorkspaceIdAtom, async () => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
console.log("AAA");
if (activeWorkspaceId == null) return;
const key = kvKey();
const recentIds = await getKeyValue<string[]>({ namespace, key, fallback });
console.log("BBB", recentIds, activeWorkspaceId);
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);
const value = [activeWorkspaceId, ...withoutActiveId];
console.log("SET ACTIVE WORCENT", value);
await setKeyValue({ namespace, key, value });
});
}, []);

View File

@@ -1,7 +1,7 @@
import deepEqual from '@gilbarbara/deep-equal';
import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel, KeyValue } from '@yaakapp-internal/models';
import type { AnyModel, KeyValue, ModelPayload } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { buildKeyValueKey } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
@@ -21,97 +21,93 @@ import { useRequestUpdateKey } from './useRequestUpdateKey';
import { settingsAtom } from './useSettings';
import { workspacesAtom } from './useWorkspaces';
export interface ModelPayload {
model: AnyModel;
windowLabel: string;
}
export function useSyncModelStores() {
const activeWorkspace = useActiveWorkspace();
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
const { model, windowLabel } = payload;
const queryKey =
model.model === 'grpc_event'
? grpcEventsQueryKey(model)
: model.model === 'key_value'
? keyValueQueryKey(model)
payload.model.model === 'grpc_event'
? grpcEventsQueryKey(payload.model)
: payload.model.model === 'key_value'
? keyValueQueryKey(payload.model)
: null;
// TODO: Move this logic to useRequestEditor() hook
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
wasUpdatedExternally(model.id);
if (
payload.model.model === 'http_request' &&
(payload.windowLabel !== getCurrentWebviewWindow().label || payload.updateSource !== 'window')
) {
wasUpdatedExternally(payload.model.id);
}
// Only sync models that belong to this workspace, if a workspace ID is present
if ('workspaceId' in model && model.workspaceId !== activeWorkspace?.id) {
if ('workspaceId' in payload.model && payload.model.workspaceId !== activeWorkspace?.id) {
return;
}
if (shouldIgnoreModel(model, windowLabel)) return;
if (shouldIgnoreModel(payload)) return;
if (model.model === 'workspace') {
jotaiStore.set(workspacesAtom, updateModelList(model));
} else if (model.model === 'plugin') {
jotaiStore.set(pluginsAtom, updateModelList(model));
} else if (model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, updateModelList(model));
} else if (model.model === 'folder') {
jotaiStore.set(foldersAtom, updateModelList(model));
} else if (model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, updateModelList(model));
} else if (model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, updateModelList(model));
} else if (model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, updateModelList(model));
} else if (model.model === 'environment') {
jotaiStore.set(environmentsAtom, updateModelList(model));
} else if (model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, updateModelList(model));
} else if (model.model === 'settings') {
jotaiStore.set(settingsAtom, model);
} else if (model.model === 'key_value') {
jotaiStore.set(keyValuesAtom, updateModelList(model));
if (payload.model.model === 'workspace') {
jotaiStore.set(workspacesAtom, updateModelList(payload.model));
} else if (payload.model.model === 'plugin') {
jotaiStore.set(pluginsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'folder') {
jotaiStore.set(foldersAtom, updateModelList(payload.model));
} else if (payload.model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, updateModelList(payload.model));
} else if (payload.model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'environment') {
jotaiStore.set(environmentsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'settings') {
jotaiStore.set(settingsAtom, payload.model);
} else if (payload.model.model === 'key_value') {
jotaiStore.set(keyValuesAtom, updateModelList(payload.model));
} else if (queryKey != null) {
// TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => {
if (Array.isArray(current)) {
return updateModelList(model)(current);
return updateModelList(payload.model)(current);
}
});
}
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
if (shouldIgnoreModel(payload)) return;
console.log('Delete model', payload);
if (model.model === 'workspace') {
jotaiStore.set(workspacesAtom, removeModelById(model));
} else if (model.model === 'plugin') {
jotaiStore.set(pluginsAtom, removeModelById(model));
} else if (model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, removeModelById(model));
} else if (model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, removeModelById(model));
} else if (model.model === 'folder') {
jotaiStore.set(foldersAtom, removeModelById(model));
} else if (model.model === 'environment') {
jotaiStore.set(environmentsAtom, removeModelById(model));
} else if (model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, removeModelById(model));
} else if (model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, removeModelById(model));
} else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), removeModelByKeyValue(model));
} else if (model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, removeModelById(model));
if (payload.model.model === 'workspace') {
jotaiStore.set(workspacesAtom, removeModelById(payload.model));
} else if (payload.model.model === 'plugin') {
jotaiStore.set(pluginsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, removeModelById(payload.model));
} else if (payload.model.model === 'folder') {
jotaiStore.set(foldersAtom, removeModelById(payload.model));
} else if (payload.model.model === 'environment') {
jotaiStore.set(environmentsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(payload.model), removeModelById(payload.model));
} else if (payload.model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload.model), removeModelByKv(payload.model));
} else if (payload.model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, removeModelById(payload.model));
}
});
}
@@ -120,7 +116,7 @@ export function updateModelList<T extends AnyModel>(model: T) {
// Mark these models as DESC instead of ASC
const pushToFront = model.model === 'http_response' || model.model === 'grpc_connection';
return (current: T[] | undefined): T[] => {
return (current: T[] | undefined | null): T[] => {
const index = current?.findIndex((v) => modelsEq(v, model)) ?? -1;
const existingModel = current?.[index];
if (existingModel && deepEqual(existingModel, model)) {
@@ -147,7 +143,7 @@ export function removeModelById<T extends { id: string }>(model: T) {
};
}
export function removeModelByKeyValue(model: KeyValue) {
export function removeModelByKv(model: KeyValue) {
return (prevEntries: KeyValue[] | undefined) =>
prevEntries?.filter(
(e) =>
@@ -159,13 +155,20 @@ export function removeModelByKeyValue(model: KeyValue) {
) ?? [];
}
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
function shouldIgnoreModel({ model, windowLabel, updateSource }: ModelPayload) {
// Never ignore same-window updates
if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates
return false;
}
if (payload.model === 'key_value') {
return payload.namespace === 'no_sync';
// Never ignore updates from non-user sources
if (updateSource !== 'window') {
return false;
}
if (model.model === 'key_value') {
return model.namespace === 'no_sync';
}
return false;
};
}

View File

@@ -19,16 +19,23 @@ export function useSyncWorkspaceChildModels() {
}
async function sync() {
// Doesn't need a workspace ID, so sync it right away
jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values'));
const workspaceId = getActiveWorkspaceId();
const args = { workspaceId };
if (workspaceId == null) {
return;
}
console.log('Syncing model stores', args);
// Set the things we need first, first
jotaiStore.set(httpRequestsAtom, await invokeCmd('cmd_list_http_requests', args));
jotaiStore.set(grpcRequestsAtom, await invokeCmd('cmd_list_grpc_requests', args));
jotaiStore.set(foldersAtom, await invokeCmd('cmd_list_folders', args));
// Then, set the rest
jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values', args));
jotaiStore.set(cookieJarsAtom, await invokeCmd('cmd_list_cookie_jars', args));
jotaiStore.set(httpResponsesAtom, await invokeCmd('cmd_list_http_responses', args));
jotaiStore.set(grpcConnectionsAtom, await invokeCmd('cmd_list_grpc_connections', args));

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugin';
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugins';
import { atom, useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index';
import { useMemo, useState } from 'react';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { CommandPalette } from '../components/CommandPalette';
import { CommandPaletteDialog } from '../components/CommandPaletteDialog';
import { useDialog } from './useDialog';
export function useToggleCommandPalette() {
@@ -13,7 +13,7 @@ export function useToggleCommandPalette() {
vAlign: 'top',
noPadding: true,
noScroll: true,
render: ({ hide }) => <CommandPalette onClose={hide} />,
render: ({ hide }) => <CommandPaletteDialog onClose={hide} />,
});
}, [dialog]);

View File

@@ -13,7 +13,6 @@ export function useUpdateEnvironment(id: string | null) {
unknown,
Partial<Environment> | ((r: Environment) => Environment)
>({
toastyError: true,
mutationKey: ['update_environment', id],
mutationFn: async (v) => {
const environment = await getEnvironment(id);

View File

@@ -1,17 +1,17 @@
import type {AnyModel, GrpcRequest, HttpRequest} from '@yaakapp-internal/models';
import type { AnyModel, GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
export function fallbackRequestName(r: HttpRequest | GrpcRequest | AnyModel | null): string {
if (r == null) return '';
if (r.model !== 'grpc_request' && r.model !== 'http_request') {
return 'name' in r ? r.name : '';
}
// Return name if it has one
if ('name' in r && r.name) {
return r.name;
}
if (r.model !== 'http_request' && r.model !== 'grpc_request') {
return 'No Name';
}
// Replace variable syntax with variable name
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, '$1');
if (withoutVariables.trim() === '') {

View File

@@ -7,10 +7,8 @@ type TauriCmd =
| 'cmd_create_cookie_jar'
| 'cmd_create_environment'
| 'cmd_template_tokens_to_string'
| 'cmd_create_folder'
| 'cmd_create_grpc_request'
| 'cmd_create_http_request'
| 'cmd_create_workspace'
| 'cmd_curl_to_request'
| 'cmd_delete_all_grpc_connections'
| 'cmd_delete_all_http_responses'

View File

@@ -61,7 +61,8 @@
"slugify": "^1.6.6",
"uuid": "^10.0.0",
"whatwg-mimetype": "^4.0.0",
"xml-formatter": "^3.6.3"
"xml-formatter": "^3.6.3",
"yaml": "^2.6.1"
},
"devDependencies": {
"@lezer/generator": "^1.7.1",

View File

@@ -1,6 +1,6 @@
import { emit, listen } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { ModelPayload } from './hooks/useSyncModelStores';
import type { ModelPayload } from '@yaakapp-internal/models';
import { getSettings } from './lib/store';
import type { Appearance } from './lib/theme/appearance';
import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance';