Run oxfmt across repo, add format script and docs

Add .oxfmtignore to skip generated bindings and wasm-pack output.
Add npm format script, update DEVELOPMENT.md for Vite+ toolchain,
and format all non-generated files with oxfmt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-13 10:15:49 -07:00
parent 45262edfbd
commit b4a1c418bb
664 changed files with 13638 additions and 13492 deletions

View File

@@ -1,8 +1,8 @@
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 { 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,
@@ -11,21 +11,21 @@ import {
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';
} 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'>>
Partial<Pick<Folder, "name" | "sortPriority" | "folderId">>
>({
mutationKey: ['create_folder'],
mutationKey: ["create_folder"],
mutationFn: async (patch) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
@@ -34,12 +34,12 @@ export const createFolder = createFastMutation<
if (!patch.name) {
const name = await showPrompt({
id: 'new-folder',
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
confirmText: 'Create',
placeholder: 'Name',
id: "new-folder",
label: "Name",
defaultValue: "Folder",
title: "New Folder",
confirmText: "Create",
placeholder: "Name",
});
if (name == null) return null;
@@ -47,7 +47,7 @@ export const createFolder = createFastMutation<
}
patch.sortPriority = patch.sortPriority || -Date.now();
const id = await createWorkspaceModel({ model: 'folder', workspaceId, ...patch });
const id = await createWorkspaceModel({ model: "folder", workspaceId, ...patch });
return id;
},
});
@@ -61,12 +61,12 @@ export const syncWorkspace = createFastMutation<
mutationFn: async ({ workspaceId, syncDir, force }) => {
const ops = (await calculateSync(workspaceId, syncDir)) ?? [];
if (ops.length === 0) {
console.log('Nothing to sync', workspaceId, syncDir);
console.log("Nothing to sync", workspaceId, syncDir);
return;
}
console.log('Syncing workspace', workspaceId, syncDir, ops);
console.log("Syncing workspace", workspaceId, syncDir, ops);
const dbOps = ops.filter((o) => o.type.startsWith('db'));
const dbOps = ops.filter((o) => o.type.startsWith("db"));
if (dbOps.length === 0) {
await applySync(workspaceId, syncDir, ops);
@@ -74,10 +74,10 @@ export const syncWorkspace = createFastMutation<
}
const isDeletingWorkspace = ops.some(
(o) => o.type === 'dbDelete' && o.model.model === 'workspace',
(o) => o.type === "dbDelete" && o.model.model === "workspace",
);
console.log('Directory changes detected', { dbOps, ops });
console.log("Directory changes detected", { dbOps, ops });
if (force) {
await applySync(workspaceId, syncDir, ops);
@@ -85,9 +85,9 @@ export const syncWorkspace = createFastMutation<
}
showDialog({
id: 'commit-sync',
title: 'Changes Detected',
size: 'md',
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"
@@ -105,8 +105,8 @@ export const syncWorkspace = createFastMutation<
<span />
)}
<p>
{pluralizeCount('file', dbOps.length)} in the directory{' '}
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
{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>
@@ -123,20 +123,20 @@ export const syncWorkspace = createFastMutation<
let color: string;
let model: string;
if (op.type === 'dbCreate') {
label = 'create';
if (op.type === "dbCreate") {
label = "create";
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-success';
color = "text-success";
model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbUpdate') {
label = 'update';
} else if (op.type === "dbUpdate") {
label = "update";
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-info';
color = "text-info";
model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbDelete') {
label = 'delete';
} else if (op.type === "dbDelete") {
label = "delete";
name = resolvedModelNameWithFolders(op.model);
color = 'text-danger';
color = "text-danger";
model = modelTypeLabel(op.model);
} else {
return null;

View File

@@ -1,33 +1,33 @@
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';
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'],
mutationKey: ["create_environment"],
mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {
throw new Error('No base environment passed');
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');
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',
id: "new-environment",
title: "New Environment",
description: "Create multiple environments with different sets of variables",
size: "sm",
onClose: () => resolve(null),
render: ({ hide }) => (
<CreateEnvironmentDialog

View File

@@ -1,8 +1,8 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from '@yaakapp-internal/ws';
import { createFastMutation } from '../hooks/useFastMutation';
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'],
mutationKey: ["delete_websocket_connections"],
mutationFn: async (request: WebsocketRequest) => cmdDeleteWebsocketConnections(request.id),
});

View File

@@ -1,28 +1,26 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
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';
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'],
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)}`;
requests.length === 1 ? "Move Request" : `Move ${pluralizeCount("Request", requests.length)}`;
showDialog({
id: 'change-workspace',
id: "change-workspace",
title,
size: 'sm',
size: "sm",
render: ({ hide }) => (
<MoveToWorkspaceDialog
onDone={hide}

View File

@@ -1,16 +1,16 @@
import { getModel } from '@yaakapp-internal/models';
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
import { showDialog } from '../lib/dialog';
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);
const folder = getModel("folder", folderId);
if (folder == null) return;
showDialog({
id: 'folder-settings',
id: "folder-settings",
title: null,
size: 'lg',
className: 'h-[50rem]',
size: "lg",
className: "h-[50rem]",
noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
});

View File

@@ -1,29 +1,29 @@
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';
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'],
mutationKey: ["open_settings"],
mutationFn: async (tab) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
const location = router.buildLocation({
to: '/workspaces/$workspaceId/settings',
to: "/workspaces/$workspaceId/settings",
params: { workspaceId },
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
});
await invokeCmd('cmd_new_child_window', {
await invokeCmd("cmd_new_child_window", {
url: location.href,
label: 'settings',
title: 'Yaak Settings',
label: "settings",
title: "Yaak Settings",
innerSize: [750, 600],
});
},

View File

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

View File

@@ -1,16 +1,16 @@
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';
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]',
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

@@ -1,9 +1,9 @@
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';
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,
@@ -13,7 +13,7 @@ export const switchWorkspace = createFastMutation<
inNewWindow: boolean;
}
>({
mutationKey: ['open_workspace'],
mutationKey: ["open_workspace"],
mutationFn: async ({ workspaceId, inNewWindow }) => {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
@@ -26,16 +26,16 @@ export const switchWorkspace = createFastMutation<
if (inNewWindow) {
const location = router.buildLocation({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId },
search,
});
await invokeCmd<void>('cmd_new_main_window', { url: location.href });
await invokeCmd<void>("cmd_new_main_window", { url: location.href });
return;
}
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId },
search,
});

View File

@@ -1,17 +1,17 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import mime from 'mime';
import { useKeyValue } from '../hooks/useKeyValue';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { HStack, VStack } from './core/Stacks';
import { SelectFile } from './SelectFile';
import type { HttpRequest } from "@yaakapp-internal/models";
import mime from "mime";
import { useKeyValue } from "../hooks/useKeyValue";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import { HStack, VStack } from "./core/Stacks";
import { SelectFile } from "./SelectFile";
type Props = {
requestId: string;
contentType: string | null;
body: HttpRequest['body'];
onChange: (body: HttpRequest['body']) => void;
body: HttpRequest["body"];
onChange: (body: HttpRequest["body"]) => void;
onChangeContentType: (contentType: string | null) => void;
};
@@ -23,8 +23,8 @@ export function BinaryFileEditor({
requestId,
}: Props) {
const ignoreContentType = useKeyValue<boolean>({
namespace: 'global',
key: ['ignore_content_type', requestId],
namespace: "global",
key: ["ignore_content_type", requestId],
fallback: false,
});
@@ -33,8 +33,8 @@ export function BinaryFileEditor({
onChange({ filePath: filePath ?? undefined });
};
const filePath = typeof body.filePath === 'string' ? body.filePath : null;
const mimeType = mime.getType(filePath ?? '') ?? 'application/octet-stream';
const filePath = typeof body.filePath === "string" ? body.filePath : null;
const mimeType = mime.getType(filePath ?? "") ?? "application/octet-stream";
return (
<VStack space={2}>

View File

@@ -1,12 +1,12 @@
import type { ReactNode } from 'react';
import { appInfo } from '../lib/appInfo';
import type { ReactNode } from "react";
import { appInfo } from "../lib/appInfo";
interface Props {
children: ReactNode;
feature: 'updater' | 'license';
feature: "updater" | "license";
}
const featureMap: Record<Props['feature'], boolean> = {
const featureMap: Record<Props["feature"], boolean> = {
updater: appInfo.featureUpdater,
license: appInfo.featureLicense,
};

View File

@@ -1,16 +1,16 @@
import { open } from '@tauri-apps/plugin-dialog';
import { gitClone } from '@yaakapp-internal/git';
import { useState } from 'react';
import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir';
import { appInfo } from '../lib/appInfo';
import { showErrorToast } from '../lib/toast';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { IconButton } from './core/IconButton';
import { PlainInput } from './core/PlainInput';
import { VStack } from './core/Stacks';
import { promptCredentials } from './git/credentials';
import { open } from "@tauri-apps/plugin-dialog";
import { gitClone } from "@yaakapp-internal/git";
import { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { appInfo } from "../lib/appInfo";
import { showErrorToast } from "../lib/toast";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { IconButton } from "./core/IconButton";
import { PlainInput } from "./core/PlainInput";
import { VStack } from "./core/Stacks";
import { promptCredentials } from "./git/credentials";
interface Props {
hide: () => void;
@@ -18,15 +18,15 @@ interface Props {
// Detect path separator from an existing path (defaults to /)
function getPathSeparator(path: string): string {
return path.includes('\\') ? '\\' : '/';
return path.includes("\\") ? "\\" : "/";
}
export function CloneGitRepositoryDialog({ hide }: Props) {
const [url, setUrl] = useState<string>('');
const [url, setUrl] = useState<string>("");
const [baseDirectory, setBaseDirectory] = useState<string>(appInfo.defaultProjectDir);
const [directoryOverride, setDirectoryOverride] = useState<string | null>(null);
const [hasSubdirectory, setHasSubdirectory] = useState(false);
const [subdirectory, setSubdirectory] = useState<string>('');
const [subdirectory, setSubdirectory] = useState<string>("");
const [isCloning, setIsCloning] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -39,7 +39,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
const handleSelectDirectory = async () => {
const dir = await open({
title: 'Select Directory',
title: "Select Directory",
directory: true,
multiple: false,
});
@@ -59,9 +59,9 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
try {
const result = await gitClone(url, directory, promptCredentials);
if (result.type === 'needs_credentials') {
if (result.type === "needs_credentials") {
setError(
result.error ?? 'Authentication failed. Please check your credentials and try again.',
result.error ?? "Authentication failed. Please check your credentials and try again.",
);
return;
}
@@ -73,8 +73,8 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
} catch (err) {
setError(String(err));
showErrorToast({
id: 'git-clone-error',
title: 'Clone Failed',
id: "git-clone-error",
title: "Clone Failed",
message: String(err),
});
} finally {
@@ -137,7 +137,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
disabled={!url || !directory || isCloning}
isLoading={isCloning}
>
{isCloning ? 'Cloning...' : 'Clone Repository'}
{isCloning ? "Cloning..." : "Clone Repository"}
</Button>
</VStack>
);
@@ -157,5 +157,5 @@ function extractRepoName(url: string): string {
if (sshMatch?.[1]) {
return sshMatch[1];
}
return '';
return "";
}

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import classNames from "classnames";
import type { CSSProperties } from "react";
interface Props {
color: string | null;
@@ -11,7 +11,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
const style: CSSProperties = { backgroundColor: color ?? undefined };
const finalClassName = classNames(
className,
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0',
"inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0",
);
if (onClick) {
@@ -20,7 +20,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
type="button"
onClick={onClick}
style={style}
className={classNames(finalClassName, 'hover:border-text')}
className={classNames(finalClassName, "hover:border-text")}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import { useAtomValue } from 'jotai';
import { workspacesAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { fuzzyFilter } from "fuzzbunny";
import { useAtomValue } from "jotai";
import {
Fragment,
type KeyboardEvent,
@@ -10,48 +10,48 @@ import {
useMemo,
useRef,
useState,
} from 'react';
import { createFolder } from '../commands/commands';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { openSettings } from '../commands/openSettings';
import { switchWorkspace } from '../commands/switchWorkspace';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { useAllRequests } from '../hooks/useAllRequests';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useGrpcRequestActions } from '../hooks/useGrpcRequestActions';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { appInfo } from '../lib/appInfo';
import { copyToClipboard } from '../lib/copy';
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { showDialog } from '../lib/dialog';
import { editEnvironment } from '../lib/editEnvironment';
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
} from "react";
import { createFolder } from "../commands/commands";
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
import { openSettings } from "../commands/openSettings";
import { switchWorkspace } from "../commands/switchWorkspace";
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
import { useActiveRequest } from "../hooks/useActiveRequest";
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { useAllRequests } from "../hooks/useAllRequests";
import { useCreateWorkspace } from "../hooks/useCreateWorkspace";
import { useDebouncedState } from "../hooks/useDebouncedState";
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useGrpcRequestActions } from "../hooks/useGrpcRequestActions";
import type { HotkeyAction } from "../hooks/useHotKey";
import { useHttpRequestActions } from "../hooks/useHttpRequestActions";
import { useRecentEnvironments } from "../hooks/useRecentEnvironments";
import { useRecentRequests } from "../hooks/useRecentRequests";
import { useRecentWorkspaces } from "../hooks/useRecentWorkspaces";
import { useScrollIntoView } from "../hooks/useScrollIntoView";
import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { useSidebarHidden } from "../hooks/useSidebarHidden";
import { appInfo } from "../lib/appInfo";
import { copyToClipboard } from "../lib/copy";
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { showDialog } from "../lib/dialog";
import { editEnvironment } from "../lib/editEnvironment";
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
import {
resolvedModelNameWithFolders,
resolvedModelNameWithFoldersArray,
} from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
import { Hotkey } from './core/Hotkey';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { PlainInput } from './core/PlainInput';
} from "../lib/resolvedModelName";
import { router } from "../lib/router";
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import { CookieDialog } from "./CookieDialog";
import { Button } from "./core/Button";
import { Heading } from "./core/Heading";
import { Hotkey } from "./core/Hotkey";
import { HttpMethodTag } from "./core/HttpMethodTag";
import { Icon } from "./core/Icon";
import { PlainInput } from "./core/PlainInput";
interface CommandPaletteGroup {
key: string;
@@ -68,7 +68,7 @@ type CommandPaletteItem = {
const MAX_PER_GROUP = 8;
export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150);
const [command, setCommand] = useDebouncedState<string>("", 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const activeEnvironment = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions();
@@ -96,79 +96,79 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const commands: CommandPaletteItem[] = [
{
key: 'settings.open',
label: 'Open Settings',
action: 'settings.show',
key: "settings.open",
label: "Open Settings",
action: "settings.show",
onSelect: () => openSettings.mutate(null),
},
{
key: 'app.create',
label: 'Create Workspace',
key: "app.create",
label: "Create Workspace",
onSelect: createWorkspace,
},
{
key: 'model.create',
label: 'Create HTTP Request',
onSelect: () => createRequestAndNavigate({ model: 'http_request', workspaceId }),
key: "model.create",
label: "Create HTTP Request",
onSelect: () => createRequestAndNavigate({ model: "http_request", workspaceId }),
},
{
key: 'grpc_request.create',
label: 'Create GRPC Request',
onSelect: () => createRequestAndNavigate({ model: 'grpc_request', workspaceId }),
key: "grpc_request.create",
label: "Create GRPC Request",
onSelect: () => createRequestAndNavigate({ model: "grpc_request", workspaceId }),
},
{
key: 'websocket_request.create',
label: 'Create Websocket Request',
onSelect: () => createRequestAndNavigate({ model: 'websocket_request', workspaceId }),
key: "websocket_request.create",
label: "Create Websocket Request",
onSelect: () => createRequestAndNavigate({ model: "websocket_request", workspaceId }),
},
{
key: 'folder.create',
label: 'Create Folder',
key: "folder.create",
label: "Create Folder",
onSelect: () => createFolder.mutate({}),
},
{
key: 'cookies.show',
label: 'Show Cookies',
key: "cookies.show",
label: "Show Cookies",
onSelect: async () => {
showDialog({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
id: "cookies",
title: "Manage Cookies",
size: "full",
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
});
},
},
{
key: 'environment.edit',
label: 'Edit Environment',
action: 'environment_editor.toggle',
key: "environment.edit",
label: "Edit Environment",
action: "environment_editor.toggle",
onSelect: () => editEnvironment(activeEnvironment),
},
{
key: 'environment.create',
label: 'Create Environment',
key: "environment.create",
label: "Create Environment",
onSelect: () => createSubEnvironmentAndActivate.mutate(baseEnvironment),
},
{
key: 'sidebar.toggle',
label: 'Toggle Sidebar',
action: 'sidebar.focus',
key: "sidebar.toggle",
label: "Toggle Sidebar",
action: "sidebar.focus",
onSelect: () => setSidebarHidden((h) => !h),
},
];
if (activeRequest?.model === 'http_request') {
if (activeRequest?.model === "http_request") {
commands.push({
key: 'request.send',
action: 'request.send',
label: 'Send Request',
key: "request.send",
action: "request.send",
label: "Send Request",
onSelect: () => sendRequest(activeRequest.id),
});
if (appInfo.cliVersion != null) {
commands.push({
key: 'request.copy_cli_send',
key: "request.copy_cli_send",
searchText: `copy cli send yaak request send ${activeRequest.id}`,
label: 'Copy CLI Send Command',
label: "Copy CLI Send Command",
onSelect: () => copyToClipboard(`yaak request send ${activeRequest.id}`),
});
}
@@ -181,7 +181,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
});
}
if (activeRequest?.model === 'grpc_request') {
if (activeRequest?.model === "grpc_request") {
grpcRequestActions.forEach((a, i) => {
commands.push({
key: `grpc_request_action.${i}`,
@@ -193,21 +193,21 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (activeRequest != null) {
commands.push({
key: 'http_request.rename',
label: 'Rename Request',
key: "http_request.rename",
label: "Rename Request",
onSelect: () => renameModelWithPrompt(activeRequest),
});
commands.push({
key: 'sidebar.selected.delete',
label: 'Delete Request',
key: "sidebar.selected.delete",
label: "Delete Request",
onSelect: () => deleteModelWithConfirm(activeRequest),
});
}
return commands.sort((a, b) =>
('searchText' in a ? a.searchText : a.label).localeCompare(
'searchText' in b ? b.searchText : b.label,
("searchText" in a ? a.searchText : a.label).localeCompare(
"searchText" in b ? b.searchText : b.label,
),
);
}, [
@@ -284,14 +284,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const groups = useMemo<CommandPaletteGroup[]>(() => {
const actionsGroup: CommandPaletteGroup = {
key: 'actions',
label: 'Actions',
key: "actions",
label: "Actions",
items: workspaceCommands,
};
const requestGroup: CommandPaletteGroup = {
key: 'requests',
label: 'Switch Request',
key: "requests",
label: "Switch Request",
items: [],
};
@@ -305,14 +305,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
{resolvedModelNameWithFoldersArray(r).map((name, i, all) => (
<Fragment key={name}>
{i !== 0 && <Icon icon="chevron_right" className="opacity-80" />}
<div className={classNames(i < all.length - 1 && 'truncate')}>{name}</div>
<div className={classNames(i < all.length - 1 && "truncate")}>{name}</div>
</Fragment>
))}
</div>
),
onSelect: async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: r.workspaceId },
search: (prev) => ({ ...prev, request_id: r.id }),
});
@@ -321,8 +321,8 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}
const environmentGroup: CommandPaletteGroup = {
key: 'environments',
label: 'Switch Environment',
key: "environments",
label: "Switch Environment",
items: [],
};
@@ -338,8 +338,8 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}
const workspaceGroup: CommandPaletteGroup = {
key: 'workspaces',
label: 'Switch Workspace',
key: "workspaces",
label: "Switch Workspace",
items: [],
};
@@ -367,10 +367,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
? fuzzyFilter(
allItems.map((i) => ({
...i,
filterBy: 'searchText' in i ? i.searchText : i.label,
filterBy: "searchText" in i ? i.searchText : i.label,
})),
command,
{ fields: ['filterBy'] },
{ fields: ["filterBy"] },
)
.sort((a, b) => b.score - a.score)
.map((v) => v.item)
@@ -408,13 +408,13 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
setSelectedItemKey(next?.key ?? null);
} else if (e.key === 'ArrowUp' || (e.ctrlKey && e.key === 'k')) {
} else if (e.key === "ArrowUp" || (e.ctrlKey && e.key === "k")) {
const prev = filteredAllItems[index - 1] ?? filteredAllItems[filteredAllItems.length - 1];
setSelectedItemKey(prev?.key ?? null);
} else if (e.key === 'Enter') {
} else if (e.key === "Enter") {
const selected = filteredAllItems[index];
setSelectedItemKey(selected?.key ?? null);
if (selected) {
@@ -491,10 +491,10 @@ function CommandPaletteItem({
color="custom"
justify="start"
className={classNames(
'w-full h-sm flex items-center rounded px-1.5',
'hover:text-text',
active && 'bg-surface-highlight',
!active && 'text-text-subtle',
"w-full h-sm flex items-center rounded px-1.5",
"hover:text-text",
active && "bg-surface-highlight",
!active && "text-text-subtle",
)}
>
<span className="truncate">{children}</span>

View File

@@ -1,14 +1,14 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import type { ReactNode } from 'react';
import { useToggle } from '../hooks/useToggle';
import { showConfirm } from '../lib/confirm';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks';
import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import type { ReactNode } from "react";
import { useToggle } from "../hooks/useToggle";
import { showConfirm } from "../lib/confirm";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import { Link } from "./core/Link";
import { SizeTag } from "./core/SizeTag";
import { HStack } from "./core/Stacks";
interface Props {
children: ReactNode;
@@ -31,17 +31,17 @@ export function ConfirmLargeRequestBody({ children, request }: Props) {
return (
<Banner color="primary" className="flex flex-col gap-3">
<p>
Rendering content over{' '}
Rendering content over{" "}
<InlineCode>
<SizeTag contentLength={tooLargeBytes} />
</InlineCode>{' '}
</InlineCode>{" "}
may impact performance.
</p>
<p>
See{' '}
See{" "}
<Link href="https://feedback.yaak.app/en/help/articles/1198684-working-with-large-values">
Working With Large Values
</Link>{' '}
</Link>{" "}
for tips.
</p>
<HStack wrap space={2}>
@@ -55,13 +55,13 @@ export function ConfirmLargeRequestBody({ children, request }: Props) {
onClick={async () => {
const confirm = await showConfirm({
id: `delete-body-${request.id}`,
confirmText: 'Delete Body',
title: 'Delete Body Text',
description: 'Are you sure you want to delete the request body text?',
color: 'danger',
confirmText: "Delete Body",
title: "Delete Body Text",
description: "Are you sure you want to delete the request body text?",
color: "danger",
});
if (confirm) {
await patchModel(request, { body: { ...request.body, text: '' } });
await patchModel(request, { body: { ...request.body, text: "" } });
}
}}
>

View File

@@ -1,16 +1,16 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { type ReactNode, useMemo } from 'react';
import { useSaveResponse } from '../hooks/useSaveResponse';
import { useToggle } from '../hooks/useToggle';
import { isProbablyTextContentType } from '../lib/contentType';
import { getContentTypeFromHeaders } from '../lib/model_util';
import { getResponseBodyText } from '../lib/responseBody';
import { CopyButton } from './CopyButton';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks';
import type { HttpResponse } from "@yaakapp-internal/models";
import { type ReactNode, useMemo } from "react";
import { useSaveResponse } from "../hooks/useSaveResponse";
import { useToggle } from "../hooks/useToggle";
import { isProbablyTextContentType } from "../lib/contentType";
import { getContentTypeFromHeaders } from "../lib/model_util";
import { getResponseBodyText } from "../lib/responseBody";
import { CopyButton } from "./CopyButton";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import { SizeTag } from "./core/SizeTag";
import { HStack } from "./core/Stacks";
interface Props {
children: ReactNode;
@@ -33,10 +33,10 @@ export function ConfirmLargeResponse({ children, response }: Props) {
return (
<Banner color="primary" className="flex flex-col gap-3">
<p>
Showing responses over{' '}
Showing responses over{" "}
<InlineCode>
<SizeTag contentLength={LARGE_BYTES} />
</InlineCode>{' '}
</InlineCode>{" "}
may impact performance
</p>
<HStack wrap space={2}>

View File

@@ -1,15 +1,15 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { type ReactNode, useMemo } from 'react';
import { getRequestBodyText as getHttpResponseRequestBodyText } from '../hooks/useHttpRequestBody';
import { useToggle } from '../hooks/useToggle';
import { isProbablyTextContentType } from '../lib/contentType';
import { getContentTypeFromHeaders } from '../lib/model_util';
import { CopyButton } from './CopyButton';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks';
import type { HttpResponse } from "@yaakapp-internal/models";
import { type ReactNode, useMemo } from "react";
import { getRequestBodyText as getHttpResponseRequestBodyText } from "../hooks/useHttpRequestBody";
import { useToggle } from "../hooks/useToggle";
import { isProbablyTextContentType } from "../lib/contentType";
import { getContentTypeFromHeaders } from "../lib/model_util";
import { CopyButton } from "./CopyButton";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import { SizeTag } from "./core/SizeTag";
import { HStack } from "./core/Stacks";
interface Props {
children: ReactNode;
@@ -31,10 +31,10 @@ export function ConfirmLargeResponseRequest({ children, response }: Props) {
return (
<Banner color="primary" className="flex flex-col gap-3">
<p>
Showing content over{' '}
Showing content over{" "}
<InlineCode>
<SizeTag contentLength={LARGE_BYTES} />
</InlineCode>{' '}
</InlineCode>{" "}
may impact performance
</p>
<HStack wrap space={2}>
@@ -46,7 +46,7 @@ export function ConfirmLargeResponseRequest({ children, response }: Props) {
color="secondary"
variant="border"
size="xs"
text={() => getHttpResponseRequestBodyText(response).then((d) => d?.bodyText ?? '')}
text={() => getHttpResponseRequestBodyText(response).then((d) => d?.bodyText ?? "")}
/>
)}
</HStack>

View File

@@ -1,10 +1,10 @@
import type { Cookie } from '@yaakapp-internal/models';
import { cookieJarsAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { cookieDomain } from '../lib/model_util';
import { Banner } from './core/Banner';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import type { Cookie } from "@yaakapp-internal/models";
import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { cookieDomain } from "../lib/model_util";
import { Banner } from "./core/Banner";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
interface Props {
cookieJarId: string | null;

View File

@@ -1,17 +1,17 @@
import { cookieJarsAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { memo, useMemo } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { showDialog } from '../lib/dialog';
import { showPrompt } from '../lib/prompt';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { CookieDialog } from './CookieDialog';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { memo, useMemo } from "react";
import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { showDialog } from "../lib/dialog";
import { showPrompt } from "../lib/prompt";
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import { CookieDialog } from "./CookieDialog";
import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
export const CookieDropdown = memo(function CookieDropdown() {
const activeCookieJar = useActiveCookieJar();
@@ -23,44 +23,44 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(cookieJars ?? []).map((j) => ({
key: j.id,
label: j.name,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? "check" : "empty"} />,
onSelect: () => {
setWorkspaceSearchParams({ cookie_jar_id: j.id });
},
})),
...(((cookieJars ?? []).length > 0 && activeCookieJar != null
? [
{ type: 'separator', label: activeCookieJar.name },
{ type: "separator", label: activeCookieJar.name },
{
key: 'manage',
label: 'Manage Cookies',
key: "manage",
label: "Manage Cookies",
leftSlot: <Icon icon="cookie" />,
onSelect: () => {
if (activeCookieJar == null) return;
showDialog({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
id: "cookies",
title: "Manage Cookies",
size: "full",
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
});
},
},
{
key: 'rename',
label: 'Rename',
key: "rename",
label: "Rename",
leftSlot: <Icon icon="pencil" />,
onSelect: async () => {
const name = await showPrompt({
id: 'rename-cookie-jar',
title: 'Rename Cookie Jar',
id: "rename-cookie-jar",
title: "Rename Cookie Jar",
description: (
<>
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
</>
),
label: 'Name',
confirmText: 'Save',
placeholder: 'New name',
label: "Name",
confirmText: "Save",
placeholder: "New name",
defaultValue: activeCookieJar?.name,
});
if (name == null) return;
@@ -70,9 +70,9 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(((cookieJars ?? []).length > 1 // Never delete the last one
? [
{
label: 'Delete',
label: "Delete",
leftSlot: <Icon icon="trash" />,
color: 'danger',
color: "danger",
onSelect: async () => {
await deleteModelWithConfirm(activeCookieJar);
},
@@ -81,10 +81,10 @@ export const CookieDropdown = memo(function CookieDropdown() {
: []) as DropdownItem[]),
]
: []) as DropdownItem[]),
{ type: 'separator' },
{ type: "separator" },
{
key: 'create-cookie-jar',
label: 'New Cookie Jar',
key: "create-cookie-jar",
label: "New Cookie Jar",
leftSlot: <Icon icon="plus" />,
onSelect: () => createCookieJar.mutate(),
},

View File

@@ -1,10 +1,10 @@
import { useTimedBoolean } from '../hooks/useTimedBoolean';
import { copyToClipboard } from '../lib/copy';
import { showToast } from '../lib/toast';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import { useTimedBoolean } from "../hooks/useTimedBoolean";
import { copyToClipboard } from "../lib/copy";
import { showToast } from "../lib/toast";
import type { ButtonProps } from "./core/Button";
import { Button } from "./core/Button";
interface Props extends Omit<ButtonProps, 'onClick'> {
interface Props extends Omit<ButtonProps, "onClick"> {
text: string | (() => Promise<string | null>);
}
@@ -14,12 +14,12 @@ export function CopyButton({ text, ...props }: Props) {
<Button
{...props}
onClick={async () => {
const content = typeof text === 'function' ? await text() : text;
const content = typeof text === "function" ? await text() : text;
if (content == null) {
showToast({
id: 'failed-to-copy',
color: 'danger',
message: 'Failed to copy',
id: "failed-to-copy",
color: "danger",
message: "Failed to copy",
});
} else {
copyToClipboard(content, { disableToast: true });
@@ -27,7 +27,7 @@ export function CopyButton({ text, ...props }: Props) {
}
}}
>
{copied ? 'Copied' : 'Copy'}
{copied ? "Copied" : "Copy"}
</Button>
);
}

View File

@@ -1,10 +1,10 @@
import { useTimedBoolean } from '../hooks/useTimedBoolean';
import { copyToClipboard } from '../lib/copy';
import { showToast } from '../lib/toast';
import type { IconButtonProps } from './core/IconButton';
import { IconButton } from './core/IconButton';
import { useTimedBoolean } from "../hooks/useTimedBoolean";
import { copyToClipboard } from "../lib/copy";
import { showToast } from "../lib/toast";
import type { IconButtonProps } from "./core/IconButton";
import { IconButton } from "./core/IconButton";
interface Props extends Omit<IconButtonProps, 'onClick' | 'icon'> {
interface Props extends Omit<IconButtonProps, "onClick" | "icon"> {
text: string | (() => Promise<string | null>);
}
@@ -13,15 +13,15 @@ export function CopyIconButton({ text, ...props }: Props) {
return (
<IconButton
{...props}
icon={copied ? 'check' : 'copy'}
icon={copied ? "check" : "copy"}
showConfirm
onClick={async () => {
const content = typeof text === 'function' ? await text() : text;
const content = typeof text === "function" ? await text() : text;
if (content == null) {
showToast({
id: 'failed-to-copy',
color: 'danger',
message: 'Failed to copy',
id: "failed-to-copy",
color: "danger",
message: "Failed to copy",
});
} else {
copyToClipboard(content, { disableToast: true });

View File

@@ -1,8 +1,8 @@
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import type { DropdownProps } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { useCreateDropdownItems } from "../hooks/useCreateDropdownItems";
import type { DropdownProps } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
interface Props extends Omit<DropdownProps, 'items'> {
interface Props extends Omit<DropdownProps, "items"> {
hideFolder?: boolean;
}
@@ -10,7 +10,7 @@ export function CreateDropdown({ hideFolder, children, ...props }: Props) {
const getItems = useCreateDropdownItems({
hideFolder,
hideIcons: true,
folderId: 'active-folder',
folderId: "active-folder",
});
return (

View File

@@ -1,12 +1,12 @@
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { useState } from 'react';
import { useToggle } from '../hooks/useToggle';
import { ColorIndicator } from './ColorIndicator';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { ColorPickerWithThemeColors } from './core/ColorPicker';
import { Label } from './core/Label';
import { PlainInput } from './core/PlainInput';
import { createWorkspaceModel } from "@yaakapp-internal/models";
import { useState } from "react";
import { useToggle } from "../hooks/useToggle";
import { ColorIndicator } from "./ColorIndicator";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { ColorPickerWithThemeColors } from "./core/ColorPicker";
import { Label } from "./core/Label";
import { PlainInput } from "./core/PlainInput";
interface Props {
onCreate: (id: string) => void;
@@ -15,7 +15,7 @@ interface Props {
}
export function CreateEnvironmentDialog({ workspaceId, hide, onCreate }: Props) {
const [name, setName] = useState<string>('');
const [name, setName] = useState<string>("");
const [color, setColor] = useState<string | null>(null);
const [sharable, toggleSharable] = useToggle(false);
return (
@@ -24,13 +24,13 @@ export function CreateEnvironmentDialog({ workspaceId, hide, onCreate }: Props)
onSubmit={async (e) => {
e.preventDefault();
const id = await createWorkspaceModel({
model: 'environment',
model: "environment",
name,
color,
variables: [],
public: sharable,
workspaceId,
parentModel: 'environment',
parentModel: "environment",
});
hide();
onCreate(id);

View File

@@ -1,26 +1,26 @@
import { gitMutations } from '@yaakapp-internal/git';
import type { WorkspaceMeta } from '@yaakapp-internal/models';
import { createGlobalModel, updateModel } from '@yaakapp-internal/models';
import { useState } from 'react';
import { router } from '../lib/router';
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
import { invokeCmd } from '../lib/tauri';
import { showErrorToast } from '../lib/toast';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { Label } from './core/Label';
import { PlainInput } from './core/PlainInput';
import { VStack } from './core/Stacks';
import { EncryptionHelp } from './EncryptionHelp';
import { gitCallbacks } from './git/callbacks';
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
import { gitMutations } from "@yaakapp-internal/git";
import type { WorkspaceMeta } from "@yaakapp-internal/models";
import { createGlobalModel, updateModel } from "@yaakapp-internal/models";
import { useState } from "react";
import { router } from "../lib/router";
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
import { invokeCmd } from "../lib/tauri";
import { showErrorToast } from "../lib/toast";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { Label } from "./core/Label";
import { PlainInput } from "./core/PlainInput";
import { VStack } from "./core/Stacks";
import { EncryptionHelp } from "./EncryptionHelp";
import { gitCallbacks } from "./git/callbacks";
import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
interface Props {
hide: () => void;
}
export function CreateWorkspaceDialog({ hide }: Props) {
const [name, setName] = useState<string>('');
const [name, setName] = useState<string>("");
const [syncConfig, setSyncConfig] = useState<{
filePath: string | null;
initGit?: boolean;
@@ -34,12 +34,12 @@ export function CreateWorkspaceDialog({ hide }: Props) {
className="pb-3"
onSubmit={async (e) => {
e.preventDefault();
const workspaceId = await createGlobalModel({ model: 'workspace', name });
const workspaceId = await createGlobalModel({ model: "workspace", name });
if (workspaceId == 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 invokeCmd<WorkspaceMeta>('cmd_get_workspace_meta', {
const workspaceMeta = await invokeCmd<WorkspaceMeta>("cmd_get_workspace_meta", {
workspaceId,
});
await updateModel({
@@ -52,8 +52,8 @@ export function CreateWorkspaceDialog({ hide }: Props) {
.init.mutateAsync()
.catch((err) => {
showErrorToast({
id: 'git-init-error',
title: 'Error initializing Git',
id: "git-init-error",
title: "Error initializing Git",
message: String(err),
});
});
@@ -61,7 +61,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
// Navigate to workspace
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId },
});

View File

@@ -1,14 +1,14 @@
import { useAtomValue } from 'jotai';
import type { ComponentType } from 'react';
import { useCallback } from 'react';
import { dialogsAtom, hideDialog } from '../lib/dialog';
import { Dialog, type DialogProps } from './core/Dialog';
import { ErrorBoundary } from './ErrorBoundary';
import { useAtomValue } from "jotai";
import type { ComponentType } from "react";
import { useCallback } from "react";
import { dialogsAtom, hideDialog } from "../lib/dialog";
import { Dialog, type DialogProps } from "./core/Dialog";
import { ErrorBoundary } from "./ErrorBoundary";
export type DialogInstance = {
id: string;
render: ComponentType<{ hide: () => void }>;
} & Omit<DialogProps, 'open' | 'children'>;
} & Omit<DialogProps, "open" | "children">;
export function Dialogs() {
const dialogs = useAtomValue(dialogsAtom);

View File

@@ -1,13 +1,13 @@
import type { DnsOverride, Workspace } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { useCallback, useId, useMemo } from 'react';
import { fireAndForget } from '../lib/fireAndForget';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { IconButton } from './core/IconButton';
import { PlainInput } from './core/PlainInput';
import { HStack, VStack } from './core/Stacks';
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './core/Table';
import type { DnsOverride, Workspace } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import { useCallback, useId, useMemo } from "react";
import { fireAndForget } from "../lib/fireAndForget";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { IconButton } from "./core/IconButton";
import { PlainInput } from "./core/PlainInput";
import { HStack, VStack } from "./core/Stacks";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "./core/Table";
interface Props {
workspace: Workspace;
@@ -37,8 +37,8 @@ export function DnsOverridesEditor({ workspace }: Props) {
const handleAdd = useCallback(() => {
const newOverride: DnsOverride = {
hostname: '',
ipv4: [''],
hostname: "",
ipv4: [""],
ipv6: [],
enabled: true,
};
@@ -66,9 +66,9 @@ export function DnsOverridesEditor({ workspace }: Props) {
return (
<VStack space={3} className="pb-3">
<div className="text-text-subtle text-sm">
Override DNS resolution for specific hostnames. This works like{' '}
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code>{' '}
but only for requests made from this workspace.
Override DNS resolution for specific hostnames. This works like{" "}
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code> but
only for requests made from this workspace.
</div>
{overridesWithIds.length > 0 && (
@@ -111,15 +111,15 @@ interface DnsOverrideRowProps {
}
function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
const ipv4Value = override.ipv4.join(', ');
const ipv6Value = override.ipv6.join(', ');
const ipv4Value = override.ipv4.join(", ");
const ipv6Value = override.ipv6.join(", ");
return (
<TableRow>
<TableCell>
<Checkbox
hideLabel
title={override.enabled ? 'Disable override' : 'Enable override'}
title={override.enabled ? "Disable override" : "Enable override"}
checked={override.enabled ?? true}
onChange={(enabled) => onUpdate({ enabled })}
/>
@@ -144,7 +144,7 @@ function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
onChange={(value) =>
onUpdate({
ipv4: value
.split(',')
.split(",")
.map((s) => s.trim())
.filter(Boolean),
})
@@ -161,7 +161,7 @@ function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
onChange={(value) =>
onUpdate({
ipv6: value
.split(',')
.split(",")
.map((s) => s.trim())
.filter(Boolean),
})

View File

@@ -1,30 +1,30 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import classNames from "classnames";
import type { CSSProperties } from "react";
import { memo } from "react";
interface Props {
className?: string;
style?: CSSProperties;
orientation?: 'horizontal' | 'vertical';
orientation?: "horizontal" | "vertical";
}
export const DropMarker = memo(
function DropMarker({ className, style, orientation = 'horizontal' }: Props) {
function DropMarker({ className, style, orientation = "horizontal" }: Props) {
return (
<div
style={style}
className={classNames(
className,
'absolute pointer-events-none z-50',
orientation === 'horizontal' && 'w-full',
orientation === 'vertical' && 'w-0 top-0 bottom-0',
"absolute pointer-events-none z-50",
orientation === "horizontal" && "w-full",
orientation === "vertical" && "w-0 top-0 bottom-0",
)}
>
<div
className={classNames(
'absolute bg-primary rounded-full',
orientation === 'horizontal' && 'left-2 right-2 -bottom-[0.1rem] h-[0.2rem]',
orientation === 'vertical' && '-left-[0.1rem] top-0 bottom-0 w-[0.2rem]',
"absolute bg-primary rounded-full",
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
)}
/>
</div>

View File

@@ -1,5 +1,5 @@
import type { Folder, HttpRequest } from '@yaakapp-internal/models';
import { foldersAtom, httpRequestsAtom } from '@yaakapp-internal/models';
import type { Folder, HttpRequest } from "@yaakapp-internal/models";
import { foldersAtom, httpRequestsAtom } from "@yaakapp-internal/models";
import type {
FormInput,
FormInputCheckbox,
@@ -10,33 +10,33 @@ import type {
FormInputSelect,
FormInputText,
JsonPrimitive,
} from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useCallback, useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useRandomKey } from '../hooks/useRandomKey';
import { capitalize } from '../lib/capitalize';
import { showDialog } from '../lib/dialog';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox';
import { DetailsBanner } from './core/DetailsBanner';
import { Editor } from './core/Editor/LazyEditor';
import { IconButton } from './core/IconButton';
import type { InputProps } from './core/Input';
import { Input } from './core/Input';
import { Label } from './core/Label';
import type { Pair } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
import { SelectFile } from './SelectFile';
} from "@yaakapp-internal/plugins";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from "react";
import { useActiveRequest } from "../hooks/useActiveRequest";
import { useRandomKey } from "../hooks/useRandomKey";
import { capitalize } from "../lib/capitalize";
import { showDialog } from "../lib/dialog";
import { resolvedModelName } from "../lib/resolvedModelName";
import { Banner } from "./core/Banner";
import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner";
import { Editor } from "./core/Editor/LazyEditor";
import { IconButton } from "./core/IconButton";
import type { InputProps } from "./core/Input";
import { Input } from "./core/Input";
import { Label } from "./core/Label";
import type { Pair } from "./core/PairEditor";
import { PairEditor } from "./core/PairEditor";
import { PlainInput } from "./core/PlainInput";
import { Select } from "./core/Select";
import { VStack } from "./core/Stacks";
import { Markdown } from "./Markdown";
import { SelectFile } from "./SelectFile";
export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
const INPUT_SIZE = 'sm';
export const DYNAMIC_FORM_NULL_ARG = "__NULL__";
const INPUT_SIZE = "sm";
interface Props<T> {
inputs: FormInput[] | undefined | null;
@@ -75,7 +75,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables}
data={data}
className={classNames(className, 'pb-4')} // Pad the bottom to look nice
className={classNames(className, "pb-4")} // Pad the bottom to look nice
/>
);
}
@@ -89,8 +89,8 @@ function FormInputsStack<T extends Record<string, JsonPrimitive>>({
space={3}
className={classNames(
className,
'h-full overflow-auto',
'pr-1', // A bit of space between inputs and scrollbar
"h-full overflow-auto",
"pr-1", // A bit of space between inputs and scrollbar
)}
>
<FormInputs {...props} />
@@ -100,7 +100,7 @@ function FormInputsStack<T extends Record<string, JsonPrimitive>>({
type FormInputsProps<T> = Pick<
Props<T>,
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data'
"inputs" | "autocompleteFunctions" | "autocompleteVariables" | "stateKey" | "data"
> & {
setDataAttr: (name: string, value: JsonPrimitive) => void;
disabled?: boolean;
@@ -118,16 +118,16 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
return (
<>
{inputs?.map((input, i) => {
if ('hidden' in input && input.hidden) {
if ("hidden" in input && input.hidden) {
return null;
}
if ('disabled' in input && disabled != null) {
if ("disabled" in input && disabled != null) {
input.disabled = disabled;
}
switch (input.type) {
case 'select':
case "select":
return (
<SelectArg
key={i + stateKey}
@@ -140,7 +140,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
}
/>
);
case 'text':
case "text":
return (
<TextArg
key={i + stateKey}
@@ -150,11 +150,11 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "")
}
/>
);
case 'editor':
case "editor":
return (
<EditorArg
key={i + stateKey}
@@ -164,11 +164,11 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "")
}
/>
);
case 'checkbox':
case "checkbox":
return (
<CheckboxArg
key={i + stateKey}
@@ -177,7 +177,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
value={data[input.name] != null ? data[input.name] === true : false}
/>
);
case 'http_request':
case "http_request":
return (
<HttpRequestArg
key={i + stateKey}
@@ -186,7 +186,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG}
/>
);
case 'file':
case "file":
return (
<FileArg
key={i + stateKey}
@@ -197,7 +197,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
}
/>
);
case 'accordion':
case "accordion":
if (!hasVisibleInputs(input.inputs)) {
return null;
}
@@ -205,7 +205,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
<div key={i + stateKey}>
<DetailsBanner
summary={input.label}
className={classNames('!mb-auto', disabled && 'opacity-disabled')}
className={classNames("!mb-auto", disabled && "opacity-disabled")}
>
<div className="mt-3">
<FormInputsStack
@@ -221,7 +221,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
</DetailsBanner>
</div>
);
case 'h_stack':
case "h_stack":
if (!hasVisibleInputs(input.inputs)) {
return null;
}
@@ -238,7 +238,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
/>
</div>
);
case 'banner':
case "banner":
if (!hasVisibleInputs(input.inputs)) {
return null;
}
@@ -246,7 +246,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
<Banner
key={i + stateKey}
color={input.color}
className={classNames(disabled && 'opacity-disabled')}
className={classNames(disabled && "opacity-disabled")}
>
<FormInputsStack
data={data}
@@ -259,9 +259,9 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
/>
</Banner>
);
case 'markdown':
case "markdown":
return <Markdown key={i + stateKey}>{input.content}</Markdown>;
case 'key_value':
case "key_value":
return (
<KeyValueArg
key={i + stateKey}
@@ -269,7 +269,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
stateKey={stateKey}
onChange={(v) => setDataAttr(input.name, v)}
value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '[]')
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "[]")
}
/>
);
@@ -301,12 +301,12 @@ function TextArg({
onChange,
name: arg.name,
multiLine: arg.multiLine,
className: arg.multiLine ? 'min-h-[4rem]' : undefined,
className: arg.multiLine ? "min-h-[4rem]" : undefined,
defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value,
required: !arg.optional,
disabled: arg.disabled,
help: arg.description,
type: arg.password ? 'password' : 'text',
type: arg.password ? "password" : "text",
label: arg.label ?? arg.name,
size: INPUT_SIZE,
hideLabel: arg.hideLabel ?? arg.label == null,
@@ -358,9 +358,9 @@ function EditorArg({
</Label>
<div
className={classNames(
'border border-border rounded-md overflow-hidden px-2 py-1',
'focus-within:border-border-focus',
!arg.rows && 'max-h-[10rem]', // So it doesn't take up too much space
"border border-border rounded-md overflow-hidden px-2 py-1",
"focus-within:border-border-focus",
!arg.rows && "max-h-[10rem]", // So it doesn't take up too much space
)}
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
>
@@ -390,10 +390,10 @@ function EditorArg({
title="Pop out to large editor"
onClick={() => {
showDialog({
id: 'id',
size: 'full',
title: arg.readOnly ? 'View Value' : 'Edit Value',
className: '!max-w-[50rem] !max-h-[60rem]',
id: "id",
size: "full",
title: arg.readOnly ? "View Value" : "Edit Value",
className: "!max-w-[50rem] !max-h-[60rem]",
description: arg.label && (
<Label
htmlFor={id}
@@ -496,7 +496,7 @@ function HttpRequestArg({
}) {
const folders = useAtomValue(foldersAtom);
const httpRequests = useAtomValue(httpRequestsAtom);
const activeHttpRequest = useActiveRequest('http_request');
const activeHttpRequest = useActiveRequest("http_request");
useEffect(() => {
if (value === DYNAMIC_FORM_NULL_ARG && activeHttpRequest) {
@@ -513,13 +513,13 @@ function HttpRequestArg({
value={value}
disabled={arg.disabled}
options={httpRequests.map((r) => {
return {
label:
buildRequestBreadcrumbs(r, folders).join(' / ') +
(r.id === activeHttpRequest?.id ? ' (current)' : ''),
value: r.id,
};
})}
return {
label:
buildRequestBreadcrumbs(r, folders).join(" / ") +
(r.id === activeHttpRequest?.id ? " (current)" : ""),
value: r.id,
};
})}
/>
);
}
@@ -539,7 +539,7 @@ function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): strin
};
next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : resolvedModelName(a)));
return ancestors.map((a) => (a.model === "folder" ? a.name : resolvedModelName(a)));
}
function CheckboxArg({
@@ -616,7 +616,7 @@ function hasVisibleInputs(inputs: FormInput[] | undefined): boolean {
if (!inputs) return false;
for (const input of inputs) {
if ('inputs' in input && !hasVisibleInputs(input.inputs)) {
if ("inputs" in input && !hasVisibleInputs(input.inputs)) {
// Has children, but none are visible
return false;
}

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import classNames from "classnames";
import type { ReactNode } from "react";
interface Props {
children: ReactNode;
@@ -12,8 +12,8 @@ export function EmptyStateText({ children, className }: Props) {
<div
className={classNames(
className,
'rounded-lg border border-dashed border-border-subtle',
'h-full py-2 text-text-subtlest flex items-center justify-center italic',
"rounded-lg border border-dashed border-border-subtle",
"h-full py-2 text-text-subtlest flex items-center justify-center italic",
)}
>
{children}

View File

@@ -1,4 +1,4 @@
import { VStack } from './core/Stacks';
import { VStack } from "./core/Stacks";
export function EncryptionHelp() {
return (

View File

@@ -1,19 +1,19 @@
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { editEnvironment } from '../lib/editEnvironment';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import classNames from "classnames";
import { memo, useMemo } from "react";
import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { editEnvironment } from "../lib/editEnvironment";
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import type { ButtonProps } from "./core/Button";
import { Button } from "./core/Button";
import type { DropdownItem } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
type Props = {
className?: string;
} & Pick<ButtonProps, 'forDropdown' | 'leftSlot'>;
} & Pick<ButtonProps, "forDropdown" | "leftSlot">;
export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({
className,
@@ -41,11 +41,11 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
[activeEnvironment?.id],
),
...((subEnvironments.length > 0
? [{ type: 'separator', label: 'Environments' }]
? [{ type: "separator", label: "Environments" }]
: []) as DropdownItem[]),
{
label: 'Manage Environments',
hotKeyAction: 'environment_editor.toggle',
label: "Manage Environments",
hotKeyAction: "environment_editor.toggle",
leftSlot: <Icon icon="box" />,
onSelect: () => editEnvironment(activeEnvironment),
},
@@ -62,8 +62,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
size="sm"
className={classNames(
className,
'text !px-2 truncate',
!activeEnvironment && !hasBaseVars && 'text-text-subtlest italic',
"text !px-2 truncate",
!activeEnvironment && !hasBaseVars && "text-text-subtlest italic",
)}
// If no environments, the button simply opens the dialog.
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
@@ -71,7 +71,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
{...buttonProps}
>
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
{activeEnvironment?.name ?? (hasBaseVars ? "Environment" : "No Environment")}
</Button>
</Dropdown>
);

View File

@@ -1,6 +1,6 @@
import type { Environment } from '@yaakapp-internal/models';
import { showColorPicker } from '../lib/showColorPicker';
import { ColorIndicator } from './ColorIndicator';
import type { Environment } from "@yaakapp-internal/models";
import { showColorPicker } from "../lib/showColorPicker";
import { ColorIndicator } from "./ColorIndicator";
export function EnvironmentColorIndicator({
environment,

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { ColorIndicator } from './ColorIndicator';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { ColorPickerWithThemeColors } from './core/ColorPicker';
import { useState } from "react";
import { ColorIndicator } from "./ColorIndicator";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { ColorPickerWithThemeColors } from "./core/ColorPicker";
export function EnvironmentColorPicker({
color: defaultColor,

View File

@@ -1,33 +1,33 @@
import type { Environment, Workspace } from '@yaakapp-internal/models';
import { duplicateModel, patchModel } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import type { Environment, Workspace } from "@yaakapp-internal/models";
import { duplicateModel, patchModel } from "@yaakapp-internal/models";
import { atom, useAtomValue } from "jotai";
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import {
environmentsBreakdownAtom,
useEnvironmentsBreakdown,
} from '../hooks/useEnvironmentsBreakdown';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { fireAndForget } from '../lib/fireAndForget';
import { jotaiStore } from '../lib/jotai';
import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util';
import { resolvedModelName } from '../lib/resolvedModelName';
import { showColorPicker } from '../lib/showColorPicker';
import { Banner } from './core/Banner';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { IconTooltip } from './core/IconTooltip';
import { InlineCode } from './core/InlineCode';
import type { PairEditorHandle } from './core/PairEditor';
import { SplitLayout } from './core/SplitLayout';
import type { TreeNode } from './core/tree/common';
import type { TreeHandle, TreeProps } from './core/tree/Tree';
import { Tree } from './core/tree/Tree';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentEditor } from './EnvironmentEditor';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
} from "../hooks/useEnvironmentsBreakdown";
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { fireAndForget } from "../lib/fireAndForget";
import { jotaiStore } from "../lib/jotai";
import { isBaseEnvironment, isSubEnvironment } from "../lib/model_util";
import { resolvedModelName } from "../lib/resolvedModelName";
import { showColorPicker } from "../lib/showColorPicker";
import { Banner } from "./core/Banner";
import type { ContextMenuProps, DropdownItem } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { IconTooltip } from "./core/IconTooltip";
import { InlineCode } from "./core/InlineCode";
import type { PairEditorHandle } from "./core/PairEditor";
import { SplitLayout } from "./core/SplitLayout";
import type { TreeNode } from "./core/tree/common";
import type { TreeHandle, TreeProps } from "./core/tree/Tree";
import { Tree } from "./core/tree/Tree";
import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
import { EnvironmentEditor } from "./EnvironmentEditor";
import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
interface Props {
initialEnvironmentId: string | null;
@@ -108,7 +108,7 @@ function EnvironmentEditDialogSidebar({
selectedEnvironmentId: string | null;
setSelectedEnvironmentId: (id: string | null) => void;
}) {
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom) ?? '';
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom) ?? "";
const treeId = `environment.${activeWorkspaceId}.sidebar`;
const treeRef = useRef<TreeHandle>(null);
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
@@ -134,7 +134,7 @@ function EnvironmentEditDialogSidebar({
const enable = () => treeRef.current?.hasFocus() ?? false;
const actions = {
'sidebar.selected.rename': {
"sidebar.selected.rename": {
enable,
allowDefault: true,
priority: 100,
@@ -145,12 +145,12 @@ function EnvironmentEditDialogSidebar({
}
},
},
'sidebar.selected.delete': {
"sidebar.selected.delete": {
priority: 100,
enable,
cb: (items: TreeModel[]) => deleteModelWithConfirm(items),
},
'sidebar.selected.duplicate': {
"sidebar.selected.duplicate": {
priority: 100,
enable,
cb: async (items: TreeModel[]) => {
@@ -167,20 +167,20 @@ function EnvironmentEditDialogSidebar({
return actions;
}, [setSelectedEnvironmentId]);
const hotkeys = useMemo<TreeProps<TreeModel>['hotkeys']>(() => ({ actions }), [actions]);
const hotkeys = useMemo<TreeProps<TreeModel>["hotkeys"]>(() => ({ actions }), [actions]);
const getContextMenu = useCallback(
(items: TreeModel[]): ContextMenuProps['items'] => {
(items: TreeModel[]): ContextMenuProps["items"] => {
const environment = items[0];
const addEnvironmentItem: DropdownItem = {
label: 'Create Sub Environment',
label: "Create Sub Environment",
leftSlot: <Icon icon="plus" />,
onSelect: async () => {
await createSubEnvironment();
},
};
if (environment == null || environment.model !== 'environment') {
if (environment == null || environment.model !== "environment") {
return [addEnvironmentItem];
}
@@ -191,36 +191,36 @@ function EnvironmentEditDialogSidebar({
const menuItems: DropdownItem[] = [
{
label: 'Rename',
label: "Rename",
leftSlot: <Icon icon="pencil" />,
hidden: isBaseEnvironment(environment) || !singleEnvironment,
hotKeyAction: 'sidebar.selected.rename',
hotKeyAction: "sidebar.selected.rename",
hotKeyLabelOnly: true,
onSelect: async () => {
// Not sure why this is needed, but without it the
// edit input blurs immediately after opening.
requestAnimationFrame(() => {
fireAndForget(actions['sidebar.selected.rename'].cb(items));
fireAndForget(actions["sidebar.selected.rename"].cb(items));
});
},
},
{
label: 'Duplicate',
label: "Duplicate",
leftSlot: <Icon icon="copy" />,
hidden: isBaseEnvironment(environment),
hotKeyAction: 'sidebar.selected.duplicate',
hotKeyAction: "sidebar.selected.duplicate",
hotKeyLabelOnly: true,
onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
onSelect: () => actions["sidebar.selected.duplicate"].cb(items),
},
{
label: environment.color ? 'Change Color' : 'Assign Color',
label: environment.color ? "Change Color" : "Assign Color",
leftSlot: <Icon icon="palette" />,
hidden: isBaseEnvironment(environment) || !singleEnvironment,
onSelect: async () => showColorPicker(environment),
},
{
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
label: `Make ${environment.public ? "Private" : "Sharable"}`,
leftSlot: <Icon icon={environment.public ? "eye_closed" : "eye"} />,
rightSlot: <EnvironmentSharableTooltip />,
hidden: items.length > 1,
onSelect: async () => {
@@ -228,9 +228,9 @@ function EnvironmentEditDialogSidebar({
},
},
{
color: 'danger',
label: 'Delete',
hotKeyAction: 'sidebar.selected.delete',
color: "danger",
label: "Delete",
hotKeyAction: "sidebar.selected.delete",
hotKeyLabelOnly: true,
hidden: !canDeleteEnvironment,
leftSlot: <Icon icon="trash" />,
@@ -240,7 +240,7 @@ function EnvironmentEditDialogSidebar({
// Add sub environment to base environment
if (isBaseEnvironment(environment) && singleEnvironment) {
menuItems.push({ type: 'separator' });
menuItems.push({ type: "separator" });
menuItems.push(addEnvironmentItem);
}
@@ -358,7 +358,7 @@ function ItemLeftSlotInner({ item }: { item: TreeModel }) {
return baseEnvironments.length > 1 ? (
<Icon icon="alert_triangle" color="notice" />
) : (
item.model === 'environment' && item.color && <EnvironmentColorIndicator environment={item} />
item.model === "environment" && item.color && <EnvironmentColorIndicator environment={item} />
);
}
@@ -366,7 +366,7 @@ function ItemRightSlot({ item }: { item: TreeModel }) {
const { baseEnvironments } = useEnvironmentsBreakdown();
return (
<>
{item.model === 'environment' && baseEnvironments.length <= 1 && isBaseEnvironment(item) && (
{item.model === "environment" && baseEnvironments.length <= 1 && isBaseEnvironment(item) && (
<IconButton
size="sm"
color="custom"
@@ -384,7 +384,7 @@ function ItemRightSlot({ item }: { item: TreeModel }) {
function ItemInner({ item }: { item: TreeModel }) {
return (
<div className="grid grid-cols-[auto_minmax(0,1fr)] w-full items-center">
{item.model === 'environment' && item.public ? (
{item.model === "environment" && item.public ? (
<div className="mr-2 flex items-center">{sharableTooltip}</div>
) : (
<span aria-hidden />
@@ -402,9 +402,9 @@ async function createSubEnvironment() {
}
function getEditOptions(item: TreeModel) {
const options: ReturnType<NonNullable<TreeProps<TreeModel>['getEditOptions']>> = {
const options: ReturnType<NonNullable<TreeProps<TreeModel>["getEditOptions"]>> = {
defaultValue: item.name,
placeholder: 'Name',
placeholder: "Name",
async onChange(item, name) {
await patchModel(item, { name });
},

View File

@@ -1,27 +1,27 @@
import type { Environment } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useCallback, useMemo } from 'react';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
import { useKeyValue } from '../hooks/useKeyValue';
import { useRandomKey } from '../hooks/useRandomKey';
import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption';
import { isBaseEnvironment } from '../lib/model_util';
import type { Environment } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import classNames from "classnames";
import { useCallback, useMemo } from "react";
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useIsEncryptionEnabled } from "../hooks/useIsEncryptionEnabled";
import { useKeyValue } from "../hooks/useKeyValue";
import { useRandomKey } from "../hooks/useRandomKey";
import { analyzeTemplate, convertTemplateToSecure } from "../lib/encryption";
import { isBaseEnvironment } from "../lib/model_util";
import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption';
import { DismissibleBanner } from './core/DismissibleBanner';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Heading } from './core/Heading';
import type { PairEditorHandle, PairWithId } from './core/PairEditor';
import { ensurePairId } from './core/PairEditor.util';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { PillButton } from './core/PillButton';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
} from "../lib/setupOrConfigureEncryption";
import { DismissibleBanner } from "./core/DismissibleBanner";
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import { Heading } from "./core/Heading";
import type { PairEditorHandle, PairWithId } from "./core/PairEditor";
import { ensurePairId } from "./core/PairEditor.util";
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
import { PillButton } from "./core/PillButton";
import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
interface Props {
environment: Environment;
@@ -34,8 +34,8 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
const workspaceId = environment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({
namespace: 'global',
key: ['environmentValueVisibility', workspaceId],
namespace: "global",
key: ["environmentValueVisibility", workspaceId],
fallback: false,
});
const { allEnvironments } = useEnvironmentsBreakdown();
@@ -64,8 +64,8 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
}
options.push({
label: name,
type: 'constant',
detail: containingEnvs.map((e) => e.name).join(', '),
type: "constant",
detail: containingEnvs.map((e) => e.name).join(", "),
});
}
return { options };
@@ -73,14 +73,14 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet and is unusable
if (name === '') return true;
if (name === "") return true;
return name.match(/^[a-z_][a-z0-9_.-]*$/i) != null;
}, []);
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password';
const valueType = !isEncryptionEnabled && valueVisibility.value ? "text" : "password";
const allVariableAreEncrypted = useMemo(
() =>
environment.variables.every((v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure'),
environment.variables.every((v) => v.value === "" || analyzeTemplate(v.value) !== "insecure"),
[environment.variables],
);
@@ -88,7 +88,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
withEncryptionEnabled(async () => {
const encryptedVariables: PairWithId[] = [];
for (const variable of environment.variables) {
const value = variable.value ? await convertTemplateToSecure(variable.value) : '';
const value = variable.value ? await convertTemplateToSecure(variable.value) : "";
encryptedVariables.push(ensurePairId({ ...variable, value }));
}
await handleChange(encryptedVariables);
@@ -100,7 +100,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
<div
className={classNames(
className,
'h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3',
"h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3",
)}
>
<div className="flex flex-col gap-4">
@@ -123,7 +123,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
)
) : (
<PillButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
{valueVisibility.value ? "Hide Values" : "Show Values"}
</PillButton>
)}
<PillButton
@@ -133,7 +133,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
await patchModel(environment, { public: !environment.public });
}}
>
{environment.public ? 'Sharable' : 'Private'}
{environment.public ? "Sharable" : "Private"}
</PillButton>
</Heading>
{environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
@@ -143,9 +143,9 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
className="mr-3"
actions={[
{
label: 'Encrypt Variables',
label: "Encrypt Variables",
onClick: () => encryptEnvironment(environment),
color: 'success',
color: "success",
},
]}
>

View File

@@ -1,4 +1,4 @@
import { IconTooltip } from './core/IconTooltip';
import { IconTooltip } from "./core/IconTooltip";
export function EnvironmentSharableTooltip() {
return (

View File

@@ -1,10 +1,10 @@
import type { ErrorInfo, ReactNode } from 'react';
import { Component, useEffect } from 'react';
import { showDialog } from '../lib/dialog';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import RouteError from './RouteError';
import type { ErrorInfo, ReactNode } from "react";
import { Component, useEffect } from "react";
import { showDialog } from "../lib/dialog";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import RouteError from "./RouteError";
interface ErrorBoundaryProps {
name: string;
@@ -27,7 +27,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.warn('Error caught by ErrorBoundary:', error, info);
console.warn("Error caught by ErrorBoundary:", error, info);
}
render() {
@@ -44,7 +44,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
size="2xs"
onClick={() => {
showDialog({
id: 'error-boundary',
id: "error-boundary",
render: () => <RouteError error={this.state.error} />,
});
}}
@@ -61,7 +61,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
export function ErrorBoundaryTestThrow() {
useEffect(() => {
throw new Error('test error');
throw new Error("test error");
});
return <div>Hello</div>;

View File

@@ -1,17 +1,17 @@
import { save } from '@tauri-apps/plugin-dialog';
import type { Workspace } from '@yaakapp-internal/models';
import { workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo, useState } from 'react';
import slugify from 'slugify';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { pluralizeCount } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { DetailsBanner } from './core/DetailsBanner';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
import { save } from "@tauri-apps/plugin-dialog";
import type { Workspace } from "@yaakapp-internal/models";
import { workspacesAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useCallback, useMemo, useState } from "react";
import slugify from "slugify";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from "../lib/tauri";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner";
import { Link } from "./core/Link";
import { HStack, VStack } from "./core/Stacks";
interface Props {
onHide: () => void;
@@ -63,16 +63,16 @@ function ExportDataDialogContent({
const handleExport = useCallback(async () => {
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces';
const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
const exportPath = await save({
title: 'Export Data',
title: "Export Data",
defaultPath: `yaak.${slug}.json`,
});
if (exportPath == null) {
return;
}
await invokeCmd('cmd_export_data', {
await invokeCmd("cmd_export_data", {
workspaceIds: ids,
exportPath,
includePrivateEnvironments: includePrivateEnvironments,
@@ -92,7 +92,7 @@ function ExportDataDialogContent({
<tr>
<th className="w-6 min-w-0 py-2 text-left pl-1">
<Checkbox
checked={!allSelected && !noneSelected ? 'indeterminate' : allSelected}
checked={!allSelected && !noneSelected ? "indeterminate" : allSelected}
hideLabel
title="All workspaces"
onChange={handleToggleAll}
@@ -122,7 +122,7 @@ function ExportDataDialogContent({
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))
}
>
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
{w.name} {w.id === activeWorkspace.id ? "(current workspace)" : ""}
</td>
</tr>
))}
@@ -155,8 +155,8 @@ function ExportDataDialogContent({
disabled={noneSelected}
onClick={() => handleExport()}
>
Export{' '}
{pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
Export{" "}
{pluralizeCount("Workspace", numSelected, { omitSingle: true, noneWord: "Nothing" })}
</Button>
</HStack>
</footer>

View File

@@ -1,28 +1,28 @@
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useFolderActions } from '../hooks/useFolderActions';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { fireAndForget } from '../lib/fireAndForget';
import { showDialog } from '../lib/dialog';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { LoadingIcon } from './core/LoadingIcon';
import { Separator } from './core/Separator';
import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks';
import { HttpResponsePane } from './HttpResponsePane';
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { foldersAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties, ReactNode } from "react";
import { useCallback, useMemo } from "react";
import { allRequestsAtom } from "../hooks/useAllRequests";
import { useFolderActions } from "../hooks/useFolderActions";
import { useLatestHttpResponse } from "../hooks/useLatestHttpResponse";
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { fireAndForget } from "../lib/fireAndForget";
import { showDialog } from "../lib/dialog";
import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from "../lib/router";
import { Button } from "./core/Button";
import { Heading } from "./core/Heading";
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
import { HttpStatusTag } from "./core/HttpStatusTag";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { LoadingIcon } from "./core/LoadingIcon";
import { Separator } from "./core/Separator";
import { SizeTag } from "./core/SizeTag";
import { HStack } from "./core/Stacks";
import { HttpResponsePane } from "./HttpResponsePane";
interface Props {
folder: Folder;
@@ -34,7 +34,7 @@ export function FolderLayout({ folder, style }: Props) {
const requests = useAtomValue(allRequestsAtom);
const folderActions = useFolderActions();
const sendAllAction = useMemo(
() => folderActions.find((a) => a.label === 'Send All'),
() => folderActions.find((a) => a.label === "Send All"),
[folderActions],
);
@@ -79,13 +79,13 @@ export function FolderLayout({ folder, style }: Props) {
function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | WebsocketRequest }) {
let card: ReactNode;
if (child.model === 'folder') {
if (child.model === "folder") {
card = <FolderCard folder={child} />;
} else if (child.model === 'http_request') {
} else if (child.model === "http_request") {
card = <HttpRequestCard request={child} />;
} else if (child.model === 'grpc_request') {
} else if (child.model === "grpc_request") {
card = <RequestCard request={child} />;
} else if (child.model === 'websocket_request') {
} else if (child.model === "websocket_request") {
card = <RequestCard request={child} />;
} else {
card = <div>Unknown model</div>;
@@ -93,7 +93,7 @@ function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | Webs
const navigate = useCallback(async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: child.workspaceId },
search: (prev) => ({ ...prev, request_id: child.id }),
});
@@ -102,12 +102,12 @@ function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | Webs
return (
<div
className={classNames(
'rounded-lg bg-surface-highlight p-3 pt-1 border border-border',
'flex flex-col gap-3',
"rounded-lg bg-surface-highlight p-3 pt-1 border border-border",
"flex flex-col gap-3",
)}
>
<HStack space={2}>
{child.model === 'folder' && <Icon icon="folder" size="lg" />}
{child.model === "folder" && <Icon icon="folder" size="lg" />}
<Heading className="truncate" level={2}>
{resolvedModelName(child)}
</Heading>
@@ -144,7 +144,7 @@ function FolderCard({ folder }: { folder: Folder }) {
color="primary"
onClick={async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: folder.workspaceId },
search: (prev) => {
return { ...prev, request_id: null, folder_id: folder.id };
@@ -178,10 +178,10 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
onClick={(e) => {
e.stopPropagation();
showDialog({
id: 'response-preview',
title: 'Response Preview',
size: 'md',
className: 'h-full',
id: "response-preview",
title: "Response Preview",
size: "md",
className: "h-full",
render: () => {
return <HttpResponsePane activeRequestId={request.id} />;
},
@@ -192,12 +192,12 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
space={2}
alignItems="center"
className={classNames(
'cursor-default select-none',
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars',
'font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full',
"cursor-default select-none",
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
"font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full",
)}
>
{latestResponse.state !== 'closed' && <LoadingIcon size="sm" />}
{latestResponse.state !== "closed" && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={latestResponse} />
<span>&bull;</span>
<HttpResponseDurationTag response={latestResponse} />

View File

@@ -1,42 +1,38 @@
import {
createWorkspaceModel,
foldersAtom,
patchModel,
} from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { Fragment, useMemo } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useModelAncestors } from '../hooks/useModelAncestors';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { hideDialog } from '../lib/dialog';
import { CopyIconButton } from './CopyIconButton';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { Input } from './core/Input';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { EnvironmentEditor } from './EnvironmentEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { createWorkspaceModel, foldersAtom, patchModel } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { Fragment, useMemo } from "react";
import { useAuthTab } from "../hooks/useAuthTab";
import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useHeadersTab } from "../hooks/useHeadersTab";
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { useModelAncestors } from "../hooks/useModelAncestors";
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { hideDialog } from "../lib/dialog";
import { CopyIconButton } from "./CopyIconButton";
import { Button } from "./core/Button";
import { CountBadge } from "./core/CountBadge";
import { Icon } from "./core/Icon";
import { InlineCode } from "./core/InlineCode";
import { Input } from "./core/Input";
import { Link } from "./core/Link";
import { HStack, VStack } from "./core/Stacks";
import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { EmptyStateText } from "./EmptyStateText";
import { EnvironmentEditor } from "./EnvironmentEditor";
import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { MarkdownEditor } from "./MarkdownEditor";
interface Props {
folderId: string | null;
tab?: FolderSettingsTab;
}
const TAB_AUTH = 'auth';
const TAB_HEADERS = 'headers';
const TAB_VARIABLES = 'variables';
const TAB_GENERAL = 'general';
const TAB_AUTH = "auth";
const TAB_HEADERS = "headers";
const TAB_VARIABLES = "variables";
const TAB_GENERAL = "general";
export type FolderSettingsTab =
| typeof TAB_AUTH
@@ -54,7 +50,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
const inheritedHeaders = useInheritedHeaders(folder);
const environments = useEnvironmentsBreakdown();
const folderEnvironment = environments.allEnvironments.find(
(e) => e.parentModel === 'folder' && e.parentId === folderId,
(e) => e.parentModel === "folder" && e.parentId === folderId,
);
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
@@ -64,13 +60,13 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
return [
{
value: TAB_GENERAL,
label: 'General',
label: "General",
},
...headersTab,
...authTab,
{
value: TAB_VARIABLES,
label: 'Variables',
label: "Variables",
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
},
];
@@ -86,11 +82,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
{breadcrumbs.map((item, index) => (
<Fragment key={item.id}>
{index > 0 && (
<Icon
icon="chevron_right"
size="lg"
className="opacity-50 flex-shrink-0"
/>
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" />
)}
<span className="text-text-subtle truncate min-w-0" title={item.name}>
{item.name}
@@ -100,10 +92,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
{breadcrumbs.length > 0 && (
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" />
)}
<span
className="whitespace-nowrap"
title={folder.name}
>
<span className="whitespace-nowrap" title={folder.name}>
{folder.name}
</span>
</div>
@@ -141,7 +130,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
onClick={async () => {
const didDelete = await deleteModelWithConfirm(folder);
if (didDelete) {
hideDialog('folder-settings');
hideDialog("folder-settings");
}
}}
color="danger"
@@ -177,10 +166,10 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
<EmptyStateText>
<VStack alignItems="center" space={1.5}>
<p>
Override{' '}
Override{" "}
<Link href="https://yaak.app/docs/using-yaak/environments-and-variables">
Variables
</Link>{' '}
</Link>{" "}
for requests within this folder.
</p>
<Button
@@ -189,10 +178,10 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
onClick={async () => {
await createWorkspaceModel({
workspaceId: folder.workspaceId,
parentModel: 'folder',
parentModel: "folder",
parentId: folder.id,
model: 'environment',
name: 'Folder Environment',
model: "environment",
name: "Folder Environment",
});
}}
>

View File

@@ -1,12 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { useCallback, useMemo } from 'react';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import type { HttpRequest } from "@yaakapp-internal/models";
import { useCallback, useMemo } from "react";
import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairEditor } from "./core/PairEditor";
type Props = {
forceUpdateKey: string;
request: HttpRequest;
onChange: (body: HttpRequest['body']) => void;
onChange: (body: HttpRequest["body"]) => void;
};
export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props) {
@@ -24,7 +24,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
[request.body.form],
);
const handleChange = useCallback<PairEditorProps['onChange']>(
const handleChange = useCallback<PairEditorProps["onChange"]>(
(pairs) =>
onChange({
form: pairs.map((p) => ({

View File

@@ -1,12 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { useCallback, useMemo } from 'react';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import type { HttpRequest } from "@yaakapp-internal/models";
import { useCallback, useMemo } from "react";
import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
type Props = {
forceUpdateKey: string;
request: HttpRequest;
onChange: (headers: HttpRequest['body']) => void;
onChange: (headers: HttpRequest["body"]) => void;
};
export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Props) {
@@ -14,14 +14,14 @@ export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Prop
() =>
(Array.isArray(request.body.form) ? request.body.form : []).map((p) => ({
enabled: !!p.enabled,
name: p.name || '',
value: p.value || '',
name: p.name || "",
value: p.value || "",
id: p.id,
})),
[request.body.form],
);
const handleChange = useCallback<PairEditorProps['onChange']>(
const handleChange = useCallback<PairEditorProps["onChange"]>(
(pairs) =>
onChange({ form: pairs.map((p) => ({ enabled: p.enabled, name: p.name, value: p.value })) }),
[onChange],

View File

@@ -1,14 +1,14 @@
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey';
import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { jotaiStore } from '../lib/jotai';
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
import { activeRequestAtom } from "../hooks/useActiveRequest";
import { useSubscribeActiveWorkspaceId } from "../hooks/useActiveWorkspace";
import { useActiveWorkspaceChangedToast } from "../hooks/useActiveWorkspaceChangedToast";
import { useHotKey, useSubscribeHotKeys } from "../hooks/useHotKey";
import { useSubscribeHttpAuthentication } from "../hooks/useHttpAuthentication";
import { useSyncFontSizeSetting } from "../hooks/useSyncFontSizeSetting";
import { useSyncWorkspaceChildModels } from "../hooks/useSyncWorkspaceChildModels";
import { useSyncZoomSetting } from "../hooks/useSyncZoomSetting";
import { useSubscribeTemplateFunctions } from "../hooks/useTemplateFunctions";
import { jotaiStore } from "../lib/jotai";
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
export function GlobalHooks() {
useSyncZoomSetting();
@@ -25,7 +25,7 @@ export function GlobalHooks() {
useSubscribeHotKeys();
useHotKey(
'request.rename',
"request.rename",
async () => {
const model = jotaiStore.get(activeRequestAtom);
if (model == null) return;

View File

@@ -1,18 +1,18 @@
import { patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles';
import { activeGrpcConnectionAtom, useGrpcEvents } from '../hooks/usePinnedGrpcConnection';
import { workspaceLayoutAtom } from '../lib/atoms';
import { Banner } from './core/Banner';
import { HotkeyList } from './core/HotkeyList';
import { SplitLayout } from './core/SplitLayout';
import { GrpcRequestPane } from './GrpcRequestPane';
import { GrpcResponsePane } from './GrpcResponsePane';
import { patchModel } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties } from "react";
import { useEffect, useMemo } from "react";
import { useActiveRequest } from "../hooks/useActiveRequest";
import { useGrpc } from "../hooks/useGrpc";
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
import { activeGrpcConnectionAtom, useGrpcEvents } from "../hooks/usePinnedGrpcConnection";
import { workspaceLayoutAtom } from "../lib/atoms";
import { Banner } from "./core/Banner";
import { HotkeyList } from "./core/HotkeyList";
import { SplitLayout } from "./core/SplitLayout";
import { GrpcRequestPane } from "./GrpcRequestPane";
import { GrpcResponsePane } from "./GrpcResponsePane";
interface Props {
style: CSSProperties;
@@ -22,7 +22,7 @@ const emptyArray: string[] = [];
export function GrpcConnectionLayout({ style }: Props) {
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeRequest = useActiveRequest('grpc_request');
const activeRequest = useActiveRequest("grpc_request");
const activeConnection = useAtomValue(activeGrpcConnectionAtom);
const grpcEvents = useGrpcEvents(activeConnection?.id ?? null);
const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null);
@@ -59,18 +59,18 @@ export function GrpcConnectionLayout({ style }: Props) {
}, [activeRequest, services]);
const methodType:
| 'unary'
| 'server_streaming'
| 'client_streaming'
| 'streaming'
| 'no-schema'
| 'no-method' = useMemo(() => {
if (services == null) return 'no-schema';
if (activeMethod == null) return 'no-method';
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming';
if (activeMethod.clientStreaming) return 'client_streaming';
if (activeMethod.serverStreaming) return 'server_streaming';
return 'unary';
| "unary"
| "server_streaming"
| "client_streaming"
| "streaming"
| "no-schema"
| "no-method" = useMemo(() => {
if (services == null) return "no-schema";
if (activeMethod == null) return "no-method";
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return "streaming";
if (activeMethod.clientStreaming) return "client_streaming";
if (activeMethod.serverStreaming) return "server_streaming";
return "unary";
}, [activeMethod, services]);
if (activeRequest == null) {
@@ -104,10 +104,10 @@ export function GrpcConnectionLayout({ style }: Props) {
<div
style={style}
className={classNames(
'x-theme-responsePane',
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
'bg-surface rounded-md border border-border-subtle',
'shadow relative',
"x-theme-responsePane",
"max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1",
"bg-surface rounded-md border border-border-subtle",
"shadow relative",
)}
>
{grpc.go.error ? (
@@ -117,7 +117,7 @@ export function GrpcConnectionLayout({ style }: Props) {
) : grpcEvents.length >= 0 ? (
<GrpcResponsePane activeRequest={activeRequest} methodType={methodType} />
) : (
<HotkeyList hotkeys={['request.send', 'sidebar.focus', 'url_bar.focus']} />
<HotkeyList hotkeys={["request.send", "sidebar.focus", "url_bar.focus"]} />
)}
</div>
)

View File

@@ -1,29 +1,29 @@
import { jsoncLanguage } from '@shopify/lang-jsonc';
import { linter } from '@codemirror/lint';
import type { EditorView } from '@codemirror/view';
import type { GrpcRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { jsoncLanguage } from "@shopify/lang-jsonc";
import { linter } from "@codemirror/lint";
import type { EditorView } from "@codemirror/view";
import type { GrpcRequest } from "@yaakapp-internal/models";
import classNames from "classnames";
import {
handleRefresh,
jsonCompletion,
jsonSchemaLinter,
stateExtensions,
updateSchema,
} from 'codemirror-json-schema';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { pluralizeCount } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';
import { GrpcProtoSelectionDialog } from './GrpcProtoSelectionDialog';
} from "codemirror-json-schema";
import { useCallback, useEffect, useMemo, useState } from "react";
import type { ReflectResponseService } from "../hooks/useGrpc";
import { showAlert } from "../lib/alert";
import { showDialog } from "../lib/dialog";
import { pluralizeCount } from "../lib/pluralize";
import { Button } from "./core/Button";
import type { EditorProps } from "./core/Editor/Editor";
import { Editor } from "./core/Editor/LazyEditor";
import { FormattedError } from "./core/FormattedError";
import { InlineCode } from "./core/InlineCode";
import { VStack } from "./core/Stacks";
import { GrpcProtoSelectionDialog } from "./GrpcProtoSelectionDialog";
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className' | 'forceUpdateKey'> & {
type Props = Pick<EditorProps, "heightMode" | "onChange" | "className" | "forceUpdateKey"> & {
services: ReflectResponseService[] | null;
reflectionError?: string;
reflectionLoading?: boolean;
@@ -57,9 +57,9 @@ export function GrpcEditor({
const s = services.find((s) => s.name === request.service);
if (s == null) {
console.log('Failed to find service', { service: request.service, services });
console.log("Failed to find service", { service: request.service, services });
showAlert({
id: 'grpc-find-service-error',
id: "grpc-find-service-error",
title: "Couldn't Find Service",
body: (
<>
@@ -72,13 +72,13 @@ export function GrpcEditor({
const schema = s.methods.find((m) => m.name === request.method)?.schema;
if (request.method != null && schema == null) {
console.log('Failed to find method', { method: request.method, methods: s?.methods });
console.log("Failed to find method", { method: request.method, methods: s?.methods });
showAlert({
id: 'grpc-find-schema-error',
id: "grpc-find-schema-error",
title: "Couldn't Find Method",
body: (
<>
Failed to find method <InlineCode>{request.method}</InlineCode> for{' '}
Failed to find method <InlineCode>{request.method}</InlineCode> for{" "}
<InlineCode>{request.service}</InlineCode> in schema
</>
),
@@ -94,12 +94,12 @@ export function GrpcEditor({
updateSchema(editorView, JSON.parse(schema));
} catch (err) {
showAlert({
id: 'grpc-parse-schema-error',
title: 'Failed to Parse Schema',
id: "grpc-parse-schema-error",
title: "Failed to Parse Schema",
body: (
<VStack space={4}>
<p>
For service <InlineCode>{request.service}</InlineCode> and method{' '}
For service <InlineCode>{request.service}</InlineCode> and method{" "}
<InlineCode>{request.method}</InlineCode>
</p>
<FormattedError>{String(err)}</FormattedError>
@@ -128,39 +128,39 @@ export function GrpcEditor({
const actions = useMemo(
() => [
<div key="reflection" className={classNames(services == null && '!opacity-100')}>
<div key="reflection" className={classNames(services == null && "!opacity-100")}>
<Button
size="xs"
color={
reflectionLoading
? 'secondary'
? "secondary"
: reflectionUnavailable
? 'info'
? "info"
: reflectionError
? 'danger'
: 'secondary'
? "danger"
: "secondary"
}
isLoading={reflectionLoading}
onClick={() => {
showDialog({
title: 'Configure Schema',
size: 'md',
id: 'reflection-failed',
title: "Configure Schema",
size: "md",
id: "reflection-failed",
render: ({ hide }) => <GrpcProtoSelectionDialog onDone={hide} />,
});
}}
>
{reflectionLoading
? 'Inspecting Schema'
? "Inspecting Schema"
: reflectionUnavailable
? 'Select Proto Files'
? "Select Proto Files"
: reflectionError
? 'Server Error'
? "Server Error"
: protoFiles.length > 0
? pluralizeCount('File', protoFiles.length)
? pluralizeCount("File", protoFiles.length)
: services != null && protoFiles.length === 0
? 'Schema Detected'
: 'Select Schema'}
? "Schema Detected"
: "Select Schema"}
</Button>
</div>,
],

View File

@@ -1,16 +1,16 @@
import { open } from '@tauri-apps/plugin-dialog';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles';
import { pluralizeCount } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
import { open } from "@tauri-apps/plugin-dialog";
import type { GrpcRequest } from "@yaakapp-internal/models";
import { useActiveRequest } from "../hooks/useActiveRequest";
import { useGrpc } from "../hooks/useGrpc";
import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
import { pluralizeCount } from "../lib/pluralize";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
import { Link } from "./core/Link";
import { HStack, VStack } from "./core/Stacks";
interface Props {
onDone: () => void;
@@ -18,7 +18,7 @@ interface Props {
export function GrpcProtoSelectionDialog(props: Props) {
const request = useActiveRequest();
if (request?.model !== 'grpc_request') return null;
if (request?.model !== "grpc_request") return null;
return <GrpcProtoSelectionDialogWithRequest request={request} {...props} />;
}
@@ -49,9 +49,9 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
variant="border"
onClick={async () => {
const selected = await open({
title: 'Select Proto Files',
title: "Select Proto Files",
multiple: true,
filters: [{ name: 'Proto Files', extensions: ['proto'] }],
filters: [{ name: "Proto Files", extensions: ["proto"] }],
});
if (selected == null) return;
@@ -67,7 +67,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
color="primary"
onClick={async () => {
const selected = await open({
title: 'Select Proto Directory',
title: "Select Proto Directory",
directory: true,
});
if (selected == null) return;
@@ -92,7 +92,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
{reflectError && (
<Banner color="warning">
<h1 className="font-bold">
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode>
Reflection failed on URL <InlineCode>{request.url || "n/a"}</InlineCode>
</h1>
<p>{reflectError.trim()}</p>
</Banner>
@@ -100,16 +100,16 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
{!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Found services{' '}
Found services{" "}
{services?.slice(0, 5).map((s, i) => {
return (
<span key={s.name + s.methods.map((m) => m.name).join(',')}>
<span key={s.name + s.methods.map((m) => m.name).join(",")}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
{i === services.length - 1 ? "" : i === services.length - 2 ? " and " : ", "}
</span>
);
})}
{services?.length > 5 && pluralizeCount('other', services?.length - 5)}
{services?.length > 5 && pluralizeCount("other", services?.length - 5)}
</p>
</Banner>
)}
@@ -119,13 +119,13 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
Server reflection found services
{services?.map((s, i) => {
return (
<span key={s.name + s.methods.map((m) => m.name).join(',')}>
<span key={s.name + s.methods.map((m) => m.name).join(",")}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
{i === services.length - 1 ? "" : i === services.length - 2 ? " and " : ", "}
</span>
);
})}
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{' '}
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{" "}
files.
</p>
</Banner>
@@ -142,16 +142,16 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
</thead>
<tbody className="divide-y divide-surface-highlight">
{protoFiles.map((f, i) => {
const parts = f.split('/');
const parts = f.split("/");
return (
// oxlint-disable-next-line react/no-array-index-key
<tr key={f + i} className="group">
<td>
<Icon icon={f.endsWith('.proto') ? 'file_code' : 'folder_code'} />
<Icon icon={f.endsWith(".proto") ? "file_code" : "folder_code"} />
</td>
<td className="pl-1 font-mono text-sm" title={f}>
{parts.length > 3 && '.../'}
{parts.slice(-3).join('/')}
{parts.length > 3 && ".../"}
{parts.slice(-3).join("/")}
</td>
<td className="w-0 py-0.5">
<IconButton
@@ -173,10 +173,10 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
)}
{reflectionUnimplemented && protoFiles.length === 0 && (
<Banner>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '}
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{" "}
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
Server Reflection
</Link>{' '}
</Link>{" "}
. Please manually add the <InlineCode>.proto</InlineCode> file to get started.
</Banner>
)}

View File

@@ -1,28 +1,28 @@
import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useAuthTab } from '../hooks/useAuthTab';
import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { PlainInput } from './core/PlainInput';
import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { GrpcEditor } from './GrpcEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { UrlBar } from './UrlBar';
import { type GrpcRequest, type HttpRequestHeader, patchModel } from "@yaakapp-internal/models";
import classNames from "classnames";
import type { CSSProperties } from "react";
import { useCallback, useMemo, useRef } from "react";
import { useAuthTab } from "../hooks/useAuthTab";
import { useContainerSize } from "../hooks/useContainerQuery";
import type { ReflectResponseService } from "../hooks/useGrpc";
import { useHeadersTab } from "../hooks/useHeadersTab";
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
import { resolvedModelName } from "../lib/resolvedModelName";
import { Button } from "./core/Button";
import { CountBadge } from "./core/CountBadge";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { PlainInput } from "./core/PlainInput";
import { RadioDropdown } from "./core/RadioDropdown";
import { HStack, VStack } from "./core/Stacks";
import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { GrpcEditor } from "./GrpcEditor";
import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { MarkdownEditor } from "./MarkdownEditor";
import { UrlBar } from "./UrlBar";
interface Props {
style?: CSSProperties;
@@ -32,12 +32,12 @@ interface Props {
reflectionError?: string;
reflectionLoading?: boolean;
methodType:
| 'unary'
| 'client_streaming'
| 'server_streaming'
| 'streaming'
| 'no-schema'
| 'no-method';
| "unary"
| "client_streaming"
| "server_streaming"
| "streaming"
| "no-schema"
| "no-method";
isStreaming: boolean;
onCommit: () => void;
onCancel: () => void;
@@ -46,10 +46,10 @@ interface Props {
services: ReflectResponseService[] | null;
}
const TAB_MESSAGE = 'message';
const TAB_METADATA = 'metadata';
const TAB_AUTH = 'auth';
const TAB_DESCRIPTION = 'description';
const TAB_MESSAGE = "message";
const TAB_METADATA = "metadata";
const TAB_AUTH = "auth";
const TAB_DESCRIPTION = "description";
export function GrpcRequestPane({
style,
@@ -66,7 +66,7 @@ export function GrpcRequestPane({
onSend,
}: Props) {
const authTab = useAuthTab(TAB_AUTH, activeRequest);
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
const inheritedHeaders = useInheritedHeaders(activeRequest);
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
@@ -87,18 +87,18 @@ export function GrpcRequestPane({
const options =
services?.flatMap((s) =>
s.methods.map((m) => ({
label: `${s.name.split('.').pop() ?? s.name}/${m.name}`,
label: `${s.name.split(".").pop() ?? s.name}/${m.name}`,
value: `${s.name}/${m.name}`,
})),
) ?? [];
const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`;
const value = `${activeRequest?.service ?? ""}/${activeRequest?.method ?? ""}`;
return { value, options };
}, [activeRequest?.method, activeRequest?.service, services]);
const handleChangeService = useCallback(
async (v: string) => {
const [serviceName, methodName] = v.split('/', 2);
if (serviceName == null || methodName == null) throw new Error('Should never happen');
const [serviceName, methodName] = v.split("/", 2);
if (serviceName == null || methodName == null) throw new Error("Should never happen");
await patchModel(activeRequest, {
service: serviceName,
method: methodName,
@@ -112,9 +112,9 @@ export function GrpcRequestPane({
if (activeRequest.service == null || activeRequest.method == null) {
alert({
id: 'grpc-invalid-service-method',
title: 'Error',
body: 'Service or method not selected',
id: "grpc-invalid-service-method",
title: "Error",
body: "Service or method not selected",
});
}
onGo();
@@ -127,12 +127,12 @@ export function GrpcRequestPane({
const tabs: TabItem[] = useMemo(
() => [
{ value: TAB_MESSAGE, label: 'Message' },
{ value: TAB_MESSAGE, label: "Message" },
...metadataTab,
...authTab,
{
value: TAB_DESCRIPTION,
label: 'Info',
label: "Info",
rightSlot: activeRequest.description && <CountBadge count={true} />,
},
],
@@ -154,14 +154,14 @@ export function GrpcRequestPane({
<div
ref={urlContainerEl}
className={classNames(
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
paneWidth === 0 && 'opacity-0',
paneWidth > 0 && paneWidth < 400 && '!grid-cols-1',
"grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
paneWidth === 0 && "opacity-0",
paneWidth > 0 && paneWidth < 400 && "!grid-cols-1",
)}
>
<UrlBar
key={forceUpdateKey}
url={activeRequest.url ?? ''}
url={activeRequest.url ?? ""}
submitIcon={null}
forceUpdateKey={forceUpdateKey}
placeholder="localhost:50051"
@@ -178,13 +178,13 @@ export function GrpcRequestPane({
items={select.options.map((o) => ({
label: o.label,
value: o.value,
type: 'default',
type: "default",
shortLabel: o.label,
}))}
itemsAfter={[
{
label: 'Refresh',
type: 'default',
label: "Refresh",
type: "default",
leftSlot: <Icon size="sm" icon="refresh" />,
},
]}
@@ -195,14 +195,14 @@ export function GrpcRequestPane({
rightSlot={<Icon size="sm" icon="chevron_down" />}
disabled={isStreaming || services == null}
className={classNames(
'font-mono text-editor min-w-[5rem] !ring-0',
paneWidth < 400 && 'flex-1',
"font-mono text-editor min-w-[5rem] !ring-0",
paneWidth < 400 && "flex-1",
)}
>
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
{select.options.find((o) => o.value === select.value)?.label ?? "No Schema"}
</Button>
</RadioDropdown>
{methodType === 'client_streaming' || methodType === 'streaming' ? (
{methodType === "client_streaming" || methodType === "streaming" ? (
<>
{isStreaming && (
<>
@@ -225,26 +225,26 @@ export function GrpcRequestPane({
<IconButton
size="sm"
variant="border"
title={isStreaming ? 'Connect' : 'Send'}
title={isStreaming ? "Connect" : "Send"}
hotkeyAction="request.send"
onClick={isStreaming ? handleSend : handleConnect}
icon={isStreaming ? 'send_horizontal' : 'arrow_up_down'}
icon={isStreaming ? "send_horizontal" : "arrow_up_down"}
/>
</>
) : (
<IconButton
size="sm"
variant="border"
title={methodType === 'unary' ? 'Send' : 'Connect'}
title={methodType === "unary" ? "Send" : "Connect"}
hotkeyAction="request.send"
onClick={isStreaming ? onCancel : handleConnect}
disabled={methodType === 'no-schema' || methodType === 'no-method'}
disabled={methodType === "no-schema" || methodType === "no-method"}
icon={
isStreaming
? 'x'
: methodType.includes('streaming')
? 'arrow_up_down'
: 'send_horizontal'
? "x"
: methodType.includes("streaming")
? "arrow_up_down"
: "send_horizontal"
}
/>
)}

View File

@@ -1,38 +1,38 @@
import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { GrpcEvent, GrpcRequest } from "@yaakapp-internal/models";
import { useAtomValue, useSetAtom } from "jotai";
import type { CSSProperties } from "react";
import { useEffect, useMemo, useState } from "react";
import {
activeGrpcConnectionAtom,
activeGrpcConnections,
pinnedGrpcConnectionIdAtom,
useGrpcEvents,
} from '../hooks/usePinnedGrpcConnection';
import { useStateWithDeps } from '../hooks/useStateWithDeps';
import { Button } from './core/Button';
import { Editor } from './core/Editor/LazyEditor';
import { EventDetailHeader, EventViewer } from './core/EventViewer';
import { EventViewerRow } from './core/EventViewerRow';
import { HotkeyList } from './core/HotkeyList';
import { Icon, type IconProps } from './core/Icon';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { LoadingIcon } from './core/LoadingIcon';
import { HStack, VStack } from './core/Stacks';
import { EmptyStateText } from './EmptyStateText';
import { ErrorBoundary } from './ErrorBoundary';
import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown';
} from "../hooks/usePinnedGrpcConnection";
import { useStateWithDeps } from "../hooks/useStateWithDeps";
import { Button } from "./core/Button";
import { Editor } from "./core/Editor/LazyEditor";
import { EventDetailHeader, EventViewer } from "./core/EventViewer";
import { EventViewerRow } from "./core/EventViewerRow";
import { HotkeyList } from "./core/HotkeyList";
import { Icon, type IconProps } from "./core/Icon";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import { LoadingIcon } from "./core/LoadingIcon";
import { HStack, VStack } from "./core/Stacks";
import { EmptyStateText } from "./EmptyStateText";
import { ErrorBoundary } from "./ErrorBoundary";
import { RecentGrpcConnectionsDropdown } from "./RecentGrpcConnectionsDropdown";
interface Props {
style?: CSSProperties;
className?: string;
activeRequest: GrpcRequest;
methodType:
| 'unary'
| 'client_streaming'
| 'server_streaming'
| 'streaming'
| 'no-schema'
| 'no-method';
| "unary"
| "client_streaming"
| "server_streaming"
| "streaming"
| "no-schema"
| "no-method";
}
export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
@@ -52,10 +52,10 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
// Set the active message to the first message received if unary
// oxlint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (events.length === 0 || activeEvent != null || methodType !== 'unary') {
if (events.length === 0 || activeEvent != null || methodType !== "unary") {
return;
}
const firstServerMessageIndex = events.findIndex((m) => m.eventType === 'server_message');
const firstServerMessageIndex = events.findIndex((m) => m.eventType === "server_message");
if (firstServerMessageIndex !== -1) {
setActiveEventIndex(firstServerMessageIndex);
}
@@ -63,7 +63,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
if (activeConnection == null) {
return (
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
<HotkeyList hotkeys={["request.send", "model.create", "sidebar.focus", "url_bar.focus"]} />
);
}
@@ -71,7 +71,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
<HStack className="pl-3 mb-1 font-mono text-sm text-text-subtle overflow-x-auto hide-scrollbars">
<HStack space={2}>
<span className="whitespace-nowrap">{events.length} Messages</span>
{activeConnection.state !== 'closed' && (
{activeConnection.state !== "closed" && (
<LoadingIcon size="sm" className="text-text-subtlest" />
)}
</HStack>
@@ -157,8 +157,8 @@ function GrpcEventDetail({
setShowingLarge: (v: boolean) => void;
onClose: () => void;
}) {
if (event.eventType === 'client_message' || event.eventType === 'server_message') {
const title = `Message ${event.eventType === 'client_message' ? 'Sent' : 'Received'}`;
if (event.eventType === "client_message" || event.eventType === "server_message") {
const title = `Message ${event.eventType === "client_message" ? "Sent" : "Received"}`;
return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
@@ -192,7 +192,7 @@ function GrpcEventDetail({
) : (
<Editor
language="json"
defaultValue={event.content ?? ''}
defaultValue={event.content ?? ""}
wrapLines={false}
readOnly={true}
stateKey={null}
@@ -214,7 +214,7 @@ function GrpcEventDetail({
<div className="py-2 h-full">
{Object.keys(event.metadata).length === 0 ? (
<EmptyStateText>
No {event.eventType === 'connection_end' ? 'trailers' : 'metadata'}
No {event.eventType === "connection_end" ? "trailers" : "metadata"}
</EmptyStateText>
) : (
<KeyValueRows>
@@ -231,20 +231,20 @@ function GrpcEventDetail({
}
function getEventDisplay(
eventType: GrpcEvent['eventType'],
status: GrpcEvent['status'],
): { icon: IconProps['icon']; color: IconProps['color']; title: string } {
if (eventType === 'server_message') {
return { icon: 'arrow_big_down_dash', color: 'info', title: 'Server message' };
eventType: GrpcEvent["eventType"],
status: GrpcEvent["status"],
): { icon: IconProps["icon"]; color: IconProps["color"]; title: string } {
if (eventType === "server_message") {
return { icon: "arrow_big_down_dash", color: "info", title: "Server message" };
}
if (eventType === 'client_message') {
return { icon: 'arrow_big_up_dash', color: 'primary', title: 'Client message' };
if (eventType === "client_message") {
return { icon: "arrow_big_up_dash", color: "primary", title: "Client message" };
}
if (eventType === 'error' || (status != null && status > 0)) {
return { icon: 'alert_triangle', color: 'danger', title: 'Error' };
if (eventType === "error" || (status != null && status > 0)) {
return { icon: "alert_triangle", color: "danger", title: "Error" };
}
if (eventType === 'connection_end') {
return { icon: 'check', color: 'success', title: 'Connection response' };
if (eventType === "connection_end") {
return { icon: "check", color: "success", title: "Connection response" };
}
return { icon: 'info', color: undefined, title: 'Event' };
return { icon: "info", color: undefined, title: "Event" };
}

View File

@@ -1,16 +1,16 @@
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
import { type } from "@tauri-apps/plugin-os";
import { settingsAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
import { useMemo } from "react";
import { useIsFullscreen } from "../hooks/useIsFullscreen";
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from "../lib/constants";
import { WindowControls } from "./WindowControls";
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
size: 'md' | 'lg';
size: "md" | "lg";
ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean;
hideControls?: boolean;
@@ -32,12 +32,12 @@ export function HeaderSize({
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (size === "md") s.minHeight = HEADER_SIZE_MD;
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
if (nativeTitlebar) {
// No style updates when using native titlebar
} else if (type() === 'macos') {
} else if (type() === "macos") {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 76 / settings.interfaceScale;
@@ -63,16 +63,16 @@ export function HeaderSize({
style={finalStyle}
className={classNames(
className,
'pt-[1px]', // Make up for bottom border
'select-none relative',
'w-full border-b border-border-subtle min-w-0',
"pt-[1px]", // Make up for bottom border
"select-none relative",
"w-full border-b border-border-subtle min-w-0",
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div
className={classNames(
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
'px-1', // Give it some space on either end for focus outlines
"pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid",
"px-1", // Give it some space on either end for focus outlines
)}
>
{children}

View File

@@ -1,19 +1,19 @@
import type { HttpRequestHeader } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { charsets } from '../lib/data/charsets';
import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings';
import { headerNames } from '../lib/data/headerNames';
import { mimeTypes } from '../lib/data/mimetypes';
import { CountBadge } from './core/CountBadge';
import { DetailsBanner } from './core/DetailsBanner';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { InputProps } from './core/Input';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairEditorRow } from './core/PairEditor';
import { ensurePairId } from './core/PairEditor.util';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { HStack } from './core/Stacks';
import type { HttpRequestHeader } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import { charsets } from "../lib/data/charsets";
import { connections } from "../lib/data/connections";
import { encodings } from "../lib/data/encodings";
import { headerNames } from "../lib/data/headerNames";
import { mimeTypes } from "../lib/data/mimetypes";
import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner";
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import type { InputProps } from "./core/Input";
import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairEditorRow } from "./core/PairEditor";
import { ensurePairId } from "./core/PairEditor.util";
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
import { HStack } from "./core/Stacks";
type Props = {
forceUpdateKey: string;
@@ -29,7 +29,7 @@ export function HeadersEditor({
stateKey,
headers,
inheritedHeaders,
inheritedHeadersLabel = 'Inherited',
inheritedHeadersLabel = "Inherited",
onChange,
forceUpdateKey,
}: Props) {
@@ -41,15 +41,17 @@ export function HeadersEditor({
const validInheritedHeaders =
inheritedHeaders?.filter(
(pair) =>
pair.enabled && (pair.name || pair.value) && !currentHeaderNames.has(pair.name.toLowerCase()),
pair.enabled &&
(pair.name || pair.value) &&
!currentHeaderNames.has(pair.name.toLowerCase()),
) ?? [];
const hasInheritedHeaders = validInheritedHeaders.length > 0;
return (
<div
className={
hasInheritedHeaders
? '@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5'
: '@container w-full h-full'
? "@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5"
: "@container w-full h-full"
}
>
{hasInheritedHeaders && (
@@ -104,28 +106,28 @@ export function HeadersEditor({
const MIN_MATCH = 3;
const headerOptionsMap: Record<string, string[]> = {
'content-type': mimeTypes,
accept: ['*/*', ...mimeTypes],
'accept-encoding': encodings,
"content-type": mimeTypes,
accept: ["*/*", ...mimeTypes],
"accept-encoding": encodings,
connection: connections,
'accept-charset': charsets,
"accept-charset": charsets,
};
const valueType = (pair: Pair): InputProps['type'] => {
const valueType = (pair: Pair): InputProps["type"] => {
const name = pair.name.toLowerCase().trim();
if (
name.includes('authorization') ||
name.includes('api-key') ||
name.includes('access-token') ||
name.includes('auth') ||
name.includes('secret') ||
name.includes('token') ||
name === 'cookie' ||
name === 'set-cookie'
name.includes("authorization") ||
name.includes("api-key") ||
name.includes("access-token") ||
name.includes("auth") ||
name.includes("secret") ||
name.includes("token") ||
name === "cookie" ||
name === "set-cookie"
) {
return 'password';
return "password";
}
return 'text';
return "text";
};
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
@@ -133,19 +135,19 @@ const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefi
const options: GenericCompletionOption[] =
headerOptionsMap[name]?.map((o) => ({
label: o,
type: 'constant',
type: "constant",
boost: 1, // Put above other completions
})) ?? [];
return { minMatch: MIN_MATCH, options };
};
const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
const nameAutocomplete: PairEditorProps["nameAutocomplete"] = {
minMatch: MIN_MATCH,
options: headerNames.map((t) =>
typeof t === 'string'
typeof t === "string"
? {
label: t,
type: 'constant',
type: "constant",
boost: 1, // Put above other completions
}
: {
@@ -156,11 +158,11 @@ const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
};
const validateHttpHeader = (v: string) => {
if (v === '') {
if (v === "") {
return true;
}
// Template strings are not allowed so we replace them with a valid example string
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, '123');
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, "123");
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
};

View File

@@ -4,25 +4,25 @@ import type {
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { useCallback } from 'react';
import { openFolderSettings } from '../commands/openFolderSettings';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Input, type InputProps } from './core/Input';
import { Link } from './core/Link';
import { SegmentedControl } from './core/SegmentedControl';
import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText';
} from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import { useCallback } from "react";
import { openFolderSettings } from "../commands/openFolderSettings";
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
import { useRenderTemplate } from "../hooks/useRenderTemplate";
import { resolvedModelName } from "../lib/resolvedModelName";
import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
import { Input, type InputProps } from "./core/Input";
import { Link } from "./core/Link";
import { SegmentedControl } from "./core/SegmentedControl";
import { HStack } from "./core/Stacks";
import { DynamicForm } from "./DynamicForm";
import { EmptyStateText } from "./EmptyStateText";
interface Props {
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
@@ -41,7 +41,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
[model],
);
if (model.authenticationType === 'none') {
if (model.authenticationType === "none") {
return <EmptyStateText>No authentication</EmptyStateText>;
}
@@ -56,7 +56,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
}
if (inheritedAuth == null) {
if (model.model === 'workspace' || model.model === 'folder') {
if (model.model === "workspace" || model.model === "folder") {
return (
<EmptyStateText className="flex-col gap-1">
<p>
@@ -69,24 +69,24 @@ export function HttpAuthenticationEditor({ model }: Props) {
return <EmptyStateText>No authentication</EmptyStateText>;
}
if (inheritedAuth.authenticationType === 'none') {
if (inheritedAuth.authenticationType === "none") {
return <EmptyStateText>No authentication</EmptyStateText>;
}
const wasAuthInherited = inheritedAuth?.id !== model.id;
if (wasAuthInherited) {
const name = resolvedModelName(inheritedAuth);
const cta = inheritedAuth.model === 'workspace' ? 'Workspace' : name;
const cta = inheritedAuth.model === "workspace" ? "Workspace" : name;
return (
<EmptyStateText>
<p>
Inherited from{' '}
Inherited from{" "}
<button
type="submit"
className="underline hover:text-text"
onClick={() => {
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth');
else openWorkspaceSettings('auth');
if (inheritedAuth.model === "folder") openFolderSettings(inheritedAuth.id, "auth");
else openWorkspaceSettings("auth");
}}
>
{cta}
@@ -106,24 +106,24 @@ export function HttpAuthenticationEditor({ model }: Props) {
name="enabled"
value={
model.authentication.disabled === false || model.authentication.disabled == null
? '__TRUE__'
? "__TRUE__"
: model.authentication.disabled === true
? '__FALSE__'
: '__DYNAMIC__'
? "__FALSE__"
: "__DYNAMIC__"
}
options={[
{ label: 'Enabled', value: '__TRUE__' },
{ label: 'Disabled', value: '__FALSE__' },
{ label: 'Enabled when...', value: '__DYNAMIC__' },
{ label: "Enabled", value: "__TRUE__" },
{ label: "Disabled", value: "__FALSE__" },
{ label: "Enabled when...", value: "__DYNAMIC__" },
]}
onChange={async (enabled) => {
let disabled: boolean | string;
if (enabled === '__TRUE__') {
if (enabled === "__TRUE__") {
disabled = false;
} else if (enabled === '__FALSE__') {
} else if (enabled === "__FALSE__") {
disabled = true;
} else {
disabled = '';
disabled = "";
}
await handleChange({ ...model.authentication, disabled });
}}
@@ -147,7 +147,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
</Dropdown>
)}
</HStack>
{typeof model.authentication.disabled === 'string' && (
{typeof model.authentication.disabled === "string" && (
<div className="mt-3">
<AuthenticationDisabledInput
className="w-full"
@@ -178,14 +178,14 @@ function AuthenticationDisabledInput({
className,
}: {
value: string;
onChange: InputProps['onChange'];
onChange: InputProps["onChange"];
stateKey: string;
className?: string;
}) {
const rendered = useRenderTemplate({
template: value,
enabled: true,
purpose: 'preview',
purpose: "preview",
refreshKey: value,
});
@@ -200,7 +200,7 @@ function AuthenticationDisabledInput({
rightSlot={
<div className="px-1 flex items-center">
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
{rendered.isPending ? 'loading' : rendered.data ? 'enabled' : 'disabled'}
{rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"}
</div>
</div>
}

View File

@@ -1,15 +1,15 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL';
import { workspaceLayoutAtom } from '../lib/atoms';
import type { SlotProps } from './core/SplitLayout';
import { SplitLayout } from './core/SplitLayout';
import { GraphQLDocsExplorer } from './graphql/GraphQLDocsExplorer';
import { showGraphQLDocExplorerAtom } from './graphql/graphqlAtoms';
import { HttpRequestPane } from './HttpRequestPane';
import { HttpResponsePane } from './HttpResponsePane';
import type { HttpRequest } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties } from "react";
import { useCurrentGraphQLSchema } from "../hooks/useIntrospectGraphQL";
import { workspaceLayoutAtom } from "../lib/atoms";
import type { SlotProps } from "./core/SplitLayout";
import { SplitLayout } from "./core/SplitLayout";
import { GraphQLDocsExplorer } from "./graphql/GraphQLDocsExplorer";
import { showGraphQLDocExplorerAtom } from "./graphql/graphqlAtoms";
import { HttpRequestPane } from "./HttpRequestPane";
import { HttpResponsePane } from "./HttpResponsePane";
interface Props {
activeRequest: HttpRequest;
@@ -21,7 +21,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const requestResponseSplit = ({ style }: Pick<SlotProps, 'style'>) => (
const requestResponseSplit = ({ style }: Pick<SlotProps, "style">) => (
<SplitLayout
name="http_layout"
className="p-3 gap-1.5"
@@ -31,7 +31,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<HttpRequestPane
style={style}
activeRequest={activeRequest}
fullHeight={orientation === 'horizontal'}
fullHeight={orientation === "horizontal"}
/>
)}
secondSlot={({ style }) => (
@@ -41,7 +41,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
);
if (
activeRequest.bodyType === 'graphql' &&
activeRequest.bodyType === "graphql" &&
showGraphQLDocExplorer[activeRequest.id] !== undefined &&
graphQLSchema != null
) {
@@ -54,7 +54,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<GraphQLDocsExplorer
requestId={activeRequest.id}
schema={graphQLSchema}
className={classNames(orientation === 'horizontal' && '!ml-0')}
className={classNames(orientation === "horizontal" && "!ml-0")}
style={style}
/>
)}

View File

@@ -1,24 +1,24 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHeadersTab } from '../hooks/useHeadersTab';
import { useImportCurl } from '../hooks/useImportCurl';
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { generateId } from '../lib/generateId';
import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import classNames from "classnames";
import { atom, useAtomValue } from "jotai";
import type { CSSProperties } from "react";
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
import { activeRequestIdAtom } from "../hooks/useActiveRequestId";
import { allRequestsAtom } from "../hooks/useAllRequests";
import { useAuthTab } from "../hooks/useAuthTab";
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useHeadersTab } from "../hooks/useHeadersTab";
import { useImportCurl } from "../hooks/useImportCurl";
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { deepEqualAtom } from "../lib/atoms";
import { languageFromContentType } from "../lib/contentType";
import { generateId } from "../lib/generateId";
import {
BODY_TYPE_BINARY,
BODY_TYPE_FORM_MULTIPART,
@@ -29,33 +29,33 @@ import {
BODY_TYPE_OTHER,
BODY_TYPE_XML,
getContentTypeFromHeaders,
} from '../lib/model_util';
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
import { resolvedModelName } from '../lib/resolvedModelName';
import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor';
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody';
import { CountBadge } from './core/CountBadge';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Editor } from './core/Editor/LazyEditor';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import type { TabItem, TabsRef } from './core/Tabs/Tabs';
import { setActiveTab, TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { JsonBodyEditor } from './JsonBodyEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { RequestMethodDropdown } from './RequestMethodDropdown';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
} from "../lib/model_util";
import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
import { resolvedModelName } from "../lib/resolvedModelName";
import { showToast } from "../lib/toast";
import { BinaryFileEditor } from "./BinaryFileEditor";
import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
import { CountBadge } from "./core/CountBadge";
import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import { Editor } from "./core/Editor/LazyEditor";
import { InlineCode } from "./core/InlineCode";
import type { Pair } from "./core/PairEditor";
import { PlainInput } from "./core/PlainInput";
import type { TabItem, TabsRef } from "./core/Tabs/Tabs";
import { setActiveTab, TabContent, Tabs } from "./core/Tabs/Tabs";
import { EmptyStateText } from "./EmptyStateText";
import { FormMultipartEditor } from "./FormMultipartEditor";
import { FormUrlencodedEditor } from "./FormUrlencodedEditor";
import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { JsonBodyEditor } from "./JsonBodyEditor";
import { MarkdownEditor } from "./MarkdownEditor";
import { RequestMethodDropdown } from "./RequestMethodDropdown";
import { UrlBar } from "./UrlBar";
import { UrlParametersEditor } from "./UrlParameterEditor";
const GraphQLEditor = lazy(() =>
import('./graphql/GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })),
import("./graphql/GraphQLEditor").then((m) => ({ default: m.GraphQLEditor })),
);
interface Props {
@@ -65,19 +65,19 @@ interface Props {
activeRequest: HttpRequest;
}
const TAB_BODY = 'body';
const TAB_PARAMS = 'params';
const TAB_HEADERS = 'headers';
const TAB_AUTH = 'auth';
const TAB_DESCRIPTION = 'description';
const TABS_STORAGE_KEY = 'http_request_tabs';
const TAB_BODY = "body";
const TAB_PARAMS = "params";
const TAB_HEADERS = "headers";
const TAB_AUTH = "auth";
const TAB_DESCRIPTION = "description";
const TABS_STORAGE_KEY = "http_request_tabs";
const nonActiveRequestUrlsAtom = atom((get) => {
const activeRequestId = get(activeRequestIdAtom);
const requests = get(allRequestsAtom);
return requests
.filter((r) => r.id !== activeRequestId)
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url }));
.map((r): GenericCompletionOption => ({ type: "constant", label: r.url }));
});
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
@@ -94,22 +94,26 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const inheritedHeaders = useInheritedHeaders(activeRequest);
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
useRequestEditorEvent('request_pane.focus_tab', () => {
tabsRef.current?.setActiveTab(TAB_PARAMS);
}, []);
useRequestEditorEvent(
"request_pane.focus_tab",
() => {
tabsRef.current?.setActiveTab(TAB_PARAMS);
},
[],
);
const handleContentTypeChange = useCallback(
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
async (contentType: string | null, patch: Partial<Omit<HttpRequest, "headers">> = {}) => {
if (activeRequest == null) {
console.error('Failed to get active request to update', activeRequest);
console.error("Failed to get active request to update", activeRequest);
return;
}
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type');
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== "content-type");
if (contentType != null) {
headers.push({
name: 'Content-Type',
name: "Content-Type",
value: contentType,
enabled: true,
id: generateId(),
@@ -125,7 +129,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
(m) => m[1] ?? '',
(m) => m[1] ?? "",
);
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters];
@@ -134,10 +138,10 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
if (item) {
item.readOnlyName = true;
} else {
items.push({ name, value: '', enabled: true, readOnlyName: true, id: generateId() });
items.push({ name, value: "", enabled: true, readOnlyName: true, id: generateId() });
}
}
return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(',') };
return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(",") };
}, [activeRequest.url, activeRequest.urlParameters]);
let numParams = 0;
@@ -158,21 +162,21 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
options: {
value: activeRequest.bodyType,
items: [
{ type: 'separator', label: 'Form Data' },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
{ type: 'separator', label: 'Text Content' },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML },
{ type: "separator", label: "Form Data" },
{ label: "Url Encoded", value: BODY_TYPE_FORM_URLENCODED },
{ label: "Multi-Part", value: BODY_TYPE_FORM_MULTIPART },
{ type: "separator", label: "Text Content" },
{ label: "GraphQL", value: BODY_TYPE_GRAPHQL },
{ label: "JSON", value: BODY_TYPE_JSON },
{ label: "XML", value: BODY_TYPE_XML },
{
label: 'Other',
label: "Other",
value: BODY_TYPE_OTHER,
shortLabel: nameOfContentTypeOr(contentType, 'Other'),
shortLabel: nameOfContentTypeOr(contentType, "Other"),
},
{ type: 'separator', label: 'Other' },
{ label: 'Binary File', value: BODY_TYPE_BINARY },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
{ type: "separator", label: "Other" },
{ label: "Binary File", value: BODY_TYPE_BINARY },
{ label: "No Body", shortLabel: "Body", value: BODY_TYPE_NONE },
],
onChange: async (bodyType) => {
if (bodyType === activeRequest.bodyType) return;
@@ -180,7 +184,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const showMethodToast = (newMethod: string) => {
if (activeRequest.method.toLowerCase() === newMethod.toLowerCase()) return;
showToast({
id: 'switched-method',
id: "switched-method",
message: (
<>
Request method switched to <InlineCode>POST</InlineCode>
@@ -202,16 +206,16 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
) {
const isDefaultishRequest =
activeRequest.bodyType === BODY_TYPE_NONE &&
activeRequest.method.toLowerCase() === 'get';
activeRequest.method.toLowerCase() === "get";
const requiresPost = bodyType === BODY_TYPE_FORM_MULTIPART;
if (isDefaultishRequest || requiresPost) {
patch.method = 'POST';
patch.method = "POST";
showMethodToast(patch.method);
}
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType;
newContentType = bodyType === BODY_TYPE_OTHER ? "text/plain" : bodyType;
} else if (bodyType === BODY_TYPE_GRAPHQL) {
patch.method = 'POST';
newContentType = 'application/json';
patch.method = "POST";
newContentType = "application/json";
showMethodToast(patch.method);
}
@@ -226,13 +230,13 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{
value: TAB_PARAMS,
rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params',
label: "Params",
},
...headersTab,
...authTab,
{
value: TAB_DESCRIPTION,
label: 'Info',
label: "Info",
},
],
[
@@ -253,7 +257,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { mutate: importCurl } = useImportCurl();
const handleBodyChange = useCallback(
(body: HttpRequest['body']) => patchModel(activeRequest, { body }),
(body: HttpRequest["body"]) => patchModel(activeRequest, { body }),
[activeRequest],
);
@@ -271,8 +275,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
autocompleteUrls.length > 0
? autocompleteUrls
: [
{ label: 'http://', type: 'constant' },
{ label: 'https://', type: 'constant' },
{ label: "http://", type: "constant" },
{ label: "https://", type: "constant" },
],
}),
[autocompleteUrls],
@@ -280,7 +284,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const handlePaste = useCallback(
async (e: ClipboardEvent, text: string) => {
if (text.startsWith('curl ')) {
if (text.startsWith("curl ")) {
importCurl({ overwriteRequestId: activeRequestId, command: text });
} else {
const patch = prepareImportQuerystring(text);
@@ -318,7 +322,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
return (
<div
style={style}
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
className={classNames(className, "h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1")}
>
{activeRequest && (
<>
@@ -338,7 +342,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
</div>
}
forceUpdateKey={updateKey}
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
isLoading={activeResponse != null && activeResponse.state !== "closed"}
/>
<Tabs
ref={tabsRef}
@@ -373,7 +377,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{activeRequest.bodyType === BODY_TYPE_JSON ? (
<JsonBodyEditor
forceUpdateKey={forceUpdateKey}
heightMode={fullHeight ? 'full' : 'auto'}
heightMode={fullHeight ? "full" : "auto"}
request={activeRequest}
/>
) : activeRequest.bodyType === BODY_TYPE_XML ? (
@@ -382,8 +386,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
autocompleteFunctions
autocompleteVariables
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`}
heightMode={fullHeight ? "full" : "auto"}
defaultValue={`${activeRequest.body?.text ?? ""}`}
language="xml"
onChange={handleBodyTextChange}
stateKey={`xml.${activeRequest.id}`}
@@ -417,15 +421,15 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
onChange={(body) => patchModel(activeRequest, { body })}
onChangeContentType={handleContentTypeChange}
/>
) : typeof activeRequest.bodyType === 'string' ? (
) : typeof activeRequest.bodyType === "string" ? (
<Editor
forceUpdateKey={forceUpdateKey}
autocompleteFunctions
autocompleteVariables
language={languageFromContentType(contentType)}
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`}
heightMode={fullHeight ? "full" : "auto"}
defaultValue={`${activeRequest.body?.text ?? ""}`}
onChange={handleBodyTextChange}
stateKey={`other.${activeRequest.id}`}
/>
@@ -465,8 +469,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
function nameOfContentTypeOr(contentType: string | null, fallback: string) {
const language = languageFromContentType(contentType);
if (language === 'markdown') {
return 'Markdown';
if (language === "markdown") {
return "Markdown";
}
return fallback;
}

View File

@@ -1,49 +1,49 @@
import type { HttpResponse, HttpResponseEvent } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { ComponentType, CSSProperties } from 'react';
import { lazy, Suspense, useMemo } from 'react';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseBodyBytes, useResponseBodyText } from '../hooks/useResponseBodyText';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { useTimelineViewMode } from '../hooks/useTimelineViewMode';
import { getMimeTypeFromContentType } from '../lib/contentType';
import { getContentTypeFromHeaders, getCookieCounts } from '../lib/model_util';
import { ConfirmLargeResponse } from './ConfirmLargeResponse';
import { ConfirmLargeResponseRequest } from './ConfirmLargeResponseRequest';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { HotkeyList } from './core/HotkeyList';
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { LoadingIcon } from './core/LoadingIcon';
import { PillButton } from './core/PillButton';
import { SizeTag } from './core/SizeTag';
import { HStack, VStack } from './core/Stacks';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { Tooltip } from './core/Tooltip';
import { EmptyStateText } from './EmptyStateText';
import { ErrorBoundary } from './ErrorBoundary';
import { HttpResponseTimeline } from './HttpResponseTimeline';
import { RecentHttpResponsesDropdown } from './RecentHttpResponsesDropdown';
import { RequestBodyViewer } from './RequestBodyViewer';
import { ResponseCookies } from './ResponseCookies';
import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { EventStreamViewer } from './responseViewers/EventStreamViewer';
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { MultipartViewer } from './responseViewers/MultipartViewer';
import { SvgViewer } from './responseViewers/SvgViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
import classNames from "classnames";
import type { ComponentType, CSSProperties } from "react";
import { lazy, Suspense, useMemo } from "react";
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
import { useResponseViewMode } from "../hooks/useResponseViewMode";
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
import { getMimeTypeFromContentType } from "../lib/contentType";
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
import { ConfirmLargeResponse } from "./ConfirmLargeResponse";
import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { CountBadge } from "./core/CountBadge";
import { HotkeyList } from "./core/HotkeyList";
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
import { HttpStatusTag } from "./core/HttpStatusTag";
import { Icon } from "./core/Icon";
import { LoadingIcon } from "./core/LoadingIcon";
import { PillButton } from "./core/PillButton";
import { SizeTag } from "./core/SizeTag";
import { HStack, VStack } from "./core/Stacks";
import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { Tooltip } from "./core/Tooltip";
import { EmptyStateText } from "./EmptyStateText";
import { ErrorBoundary } from "./ErrorBoundary";
import { HttpResponseTimeline } from "./HttpResponseTimeline";
import { RecentHttpResponsesDropdown } from "./RecentHttpResponsesDropdown";
import { RequestBodyViewer } from "./RequestBodyViewer";
import { ResponseCookies } from "./ResponseCookies";
import { ResponseHeaders } from "./ResponseHeaders";
import { AudioViewer } from "./responseViewers/AudioViewer";
import { CsvViewer } from "./responseViewers/CsvViewer";
import { EventStreamViewer } from "./responseViewers/EventStreamViewer";
import { HTMLOrTextViewer } from "./responseViewers/HTMLOrTextViewer";
import { ImageViewer } from "./responseViewers/ImageViewer";
import { MultipartViewer } from "./responseViewers/MultipartViewer";
import { SvgViewer } from "./responseViewers/SvgViewer";
import { VideoViewer } from "./responseViewers/VideoViewer";
const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })),
import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })),
);
interface Props {
@@ -52,13 +52,13 @@ interface Props {
activeRequestId: string;
}
const TAB_BODY = 'body';
const TAB_REQUEST = 'request';
const TAB_HEADERS = 'headers';
const TAB_COOKIES = 'cookies';
const TAB_TIMELINE = 'timeline';
const TAB_BODY = "body";
const TAB_REQUEST = "request";
const TAB_HEADERS = "headers";
const TAB_COOKIES = "cookies";
const TAB_TIMELINE = "timeline";
export type TimelineViewMode = 'timeline' | 'text';
export type TimelineViewMode = "timeline" | "text";
interface RedirectDropWarning {
droppedBodyCount: number;
@@ -78,7 +78,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
[responseEvents.data],
);
const shouldShowRedirectDropWarning =
activeResponse?.state === 'closed' && redirectDropWarning != null;
activeResponse?.state === "closed" && redirectDropWarning != null;
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
@@ -86,27 +86,27 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
() => [
{
value: TAB_BODY,
label: 'Response',
label: "Response",
options: {
value: viewMode,
onChange: setViewMode,
items: [
{ label: 'Response', value: 'pretty' },
...(mimeType?.startsWith('image')
{ label: "Response", value: "pretty" },
...(mimeType?.startsWith("image")
? []
: [{ label: 'Response (Raw)', shortLabel: 'Raw', value: 'raw' }]),
: [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
],
},
},
{
value: TAB_REQUEST,
label: 'Request',
label: "Request",
rightSlot:
(activeResponse?.requestContentLength ?? 0) > 0 ? <CountBadge count={true} /> : null,
},
{
value: TAB_HEADERS,
label: 'Headers',
label: "Headers",
rightSlot: (
<CountBadge
count={activeResponse?.requestHeaders.length ?? 0}
@@ -117,7 +117,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
},
{
value: TAB_COOKIES,
label: 'Cookies',
label: "Cookies",
rightSlot:
cookieCounts.sent > 0 || cookieCounts.received > 0 ? (
<CountBadge count={cookieCounts.sent} count2={cookieCounts.received} showZero />
@@ -128,10 +128,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
rightSlot: <CountBadge count={responseEvents.data?.length ?? 0} />,
options: {
value: timelineViewMode,
onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? 'timeline'),
onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? "timeline"),
items: [
{ label: 'Timeline', value: 'timeline' },
{ label: 'Timeline (Text)', shortLabel: 'Timeline', value: 'text' },
{ label: "Timeline", value: "timeline" },
{ label: "Timeline (Text)", shortLabel: "Timeline", value: "text" },
],
},
},
@@ -158,33 +158,33 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
style={style}
className={classNames(
className,
'x-theme-responsePane',
'max-h-full h-full',
'bg-surface rounded-md border border-border-subtle overflow-hidden',
'relative',
"x-theme-responsePane",
"max-h-full h-full",
"bg-surface rounded-md border border-border-subtle overflow-hidden",
"relative",
)}
>
{activeResponse == null ? (
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
<HotkeyList hotkeys={["request.send", "model.create", "sidebar.focus", "url_bar.focus"]} />
) : (
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
<HStack
className={classNames(
'text-text-subtle w-full flex-shrink-0',
"text-text-subtle w-full flex-shrink-0",
// Remove a bit of space because the tabs have lots too
'-mb-1.5',
"-mb-1.5",
)}
>
{activeResponse && (
<div
className={classNames(
'grid grid-cols-[auto_minmax(4rem,1fr)_auto]',
'cursor-default select-none',
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars',
"grid grid-cols-[auto_minmax(4rem,1fr)_auto]",
"cursor-default select-none",
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
)}
>
<HStack space={2} className="w-full flex-shrink-0">
{activeResponse.state !== 'closed' && <LoadingIcon size="sm" />}
{activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={activeResponse} />
<span>&bull;</span>
<HttpResponseDurationTag response={activeResponse} />
@@ -205,17 +205,17 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
</span>
{redirectDropWarning.droppedBodyCount > 0 && (
<span>
Body dropped on {redirectDropWarning.droppedBodyCount}{' '}
Body dropped on {redirectDropWarning.droppedBodyCount}{" "}
{redirectDropWarning.droppedBodyCount === 1
? 'redirect hop'
: 'redirect hops'}
? "redirect hop"
: "redirect hops"}
</span>
)}
{redirectDropWarning.droppedHeaders.length > 0 && (
<span>
Headers dropped:{' '}
Headers dropped:{" "}
<span className="font-mono">
{redirectDropWarning.droppedHeaders.join(', ')}
{redirectDropWarning.droppedHeaders.join(", ")}
</span>
</span>
)}
@@ -269,7 +269,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<ErrorBoundary name="Http Response Viewer">
<Suspense>
<ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? (
{activeResponse.state === "initialized" ? (
<EmptyStateText>
<VStack space={3}>
<HStack space={3}>
@@ -281,10 +281,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
</Button>
</VStack>
</EmptyStateText>
) : activeResponse.state === 'closed' &&
) : activeResponse.state === "closed" &&
(activeResponse.contentLength ?? 0) === 0 ? (
<EmptyStateText>Empty</EmptyStateText>
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
) : mimeType?.match(/^text\/event-stream/i) && viewMode === "pretty" ? (
<EventStreamViewer response={activeResponse} />
) : mimeType?.match(/^image\/svg/) ? (
<HttpSvgViewer response={activeResponse} />
@@ -294,17 +294,17 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
) : mimeType?.match(/^multipart/i) && viewMode === 'pretty' ? (
) : mimeType?.match(/^multipart/i) && viewMode === "pretty" ? (
<HttpMultipartViewer response={activeResponse} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) && viewMode === 'pretty' ? (
) : mimeType?.match(/csv|tab-separated/i) && viewMode === "pretty" ? (
<HttpCsvViewer className="pb-2" response={activeResponse} />
) : (
<HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
pretty={viewMode === "pretty"}
/>
)}
</ConfirmLargeResponse>
@@ -342,7 +342,7 @@ function getRedirectDropWarning(
const droppedHeaders = new Set<string>();
for (const e of events) {
const event = e.event;
if (event.type !== 'redirect') {
if (event.type !== "redirect") {
continue;
}
@@ -373,12 +373,12 @@ function pushHeaderName(headers: Set<string>, headerName: string): void {
function getRedirectWarningLabel(warning: RedirectDropWarning): string {
if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) {
return 'Dropped body and headers';
return "Dropped body and headers";
}
if (warning.droppedBodyCount > 0) {
return 'Dropped body';
return "Dropped body";
}
return 'Dropped headers';
return "Dropped headers";
}
function EnsureCompleteResponse({
@@ -393,7 +393,7 @@ function EnsureCompleteResponse({
}
// Wait until the response has been fully-downloaded
if (response.state !== 'closed') {
if (response.state !== "closed") {
return (
<EmptyStateText>
<LoadingIcon />
@@ -424,7 +424,7 @@ function HttpMultipartViewer({ response }: { response: HttpResponse }) {
if (body.data == null) return null;
const contentTypeHeader = getContentTypeFromHeaders(response.headers);
const boundary = contentTypeHeader?.split('boundary=')[1] ?? 'unknown';
const boundary = contentTypeHeader?.split("boundary=")[1] ?? "unknown";
return <MultipartViewer data={body.data} boundary={boundary} idPrefix={response.id} />;
}

View File

@@ -2,16 +2,16 @@ import type {
HttpResponse,
HttpResponseEvent,
HttpResponseEventData,
} from '@yaakapp-internal/models';
import { type ReactNode, useMemo, useState } from 'react';
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
import { Editor } from './core/Editor/LazyEditor';
import { type EventDetailAction, EventDetailHeader, EventViewer } from './core/EventViewer';
import { EventViewerRow } from './core/EventViewerRow';
import { HttpStatusTagRaw } from './core/HttpStatusTag';
import { Icon, type IconProps } from './core/Icon';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import type { TimelineViewMode } from './HttpResponsePane';
} from "@yaakapp-internal/models";
import { type ReactNode, useMemo, useState } from "react";
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { Editor } from "./core/Editor/LazyEditor";
import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
import { EventViewerRow } from "./core/EventViewerRow";
import { HttpStatusTagRaw } from "./core/HttpStatusTag";
import { Icon, type IconProps } from "./core/Icon";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import type { TimelineViewMode } from "./HttpResponsePane";
interface Props {
response: HttpResponse;
@@ -28,12 +28,12 @@ function Inner({ response, viewMode }: Props) {
// Generate plain text representation of all events (with prefixes for timeline view)
const plainText = useMemo(() => {
if (!events || events.length === 0) return '';
return events.map((event) => formatEventText(event.event, true)).join('\n');
if (!events || events.length === 0) return "";
return events.map((event) => formatEventText(event.event, true)).join("\n");
}, [events]);
// Plain text view - show all events as text in an editor
if (viewMode === 'text') {
if (viewMode === "text") {
if (isLoading) {
return <div className="p-4 text-text-subtlest">Loading events...</div>;
} else if (error) {
@@ -98,8 +98,8 @@ function EventDetails({
const actions: EventDetailAction[] = [
{
key: 'toggle-raw',
label: showRaw ? 'Formatted' : 'Text',
key: "toggle-raw",
label: showRaw ? "Formatted" : "Text",
onClick: () => setShowRaw(!showRaw),
},
];
@@ -107,24 +107,24 @@ function EventDetails({
// Determine the title based on event type
const title = (() => {
switch (e.type) {
case 'header_up':
return 'Header Sent';
case 'header_down':
return 'Header Received';
case 'send_url':
return 'Request';
case 'receive_url':
return 'Response';
case 'redirect':
return 'Redirect';
case 'setting':
return 'Apply Setting';
case 'chunk_sent':
return 'Data Sent';
case 'chunk_received':
return 'Data Received';
case 'dns_resolved':
return e.overridden ? 'DNS Override' : 'DNS Resolution';
case "header_up":
return "Header Sent";
case "header_down":
return "Header Received";
case "send_url":
return "Request";
case "receive_url":
return "Response";
case "redirect":
return "Redirect";
case "setting":
return "Apply Setting";
case "chunk_sent":
return "Data Sent";
case "chunk_received":
return "Data Received";
case "dns_resolved":
return e.overridden ? "DNS Override" : "DNS Resolution";
default:
return label;
}
@@ -139,7 +139,7 @@ function EventDetails({
}
// Headers - show name and value
if (e.type === 'header_up' || e.type === 'header_down') {
if (e.type === "header_up" || e.type === "header_down") {
return (
<KeyValueRows>
<KeyValueRow label="Header">{e.name}</KeyValueRow>
@@ -149,13 +149,13 @@ function EventDetails({
}
// Request URL - show all URL parts separately
if (e.type === 'send_url') {
const auth = e.username || e.password ? `${e.username}:${e.password}@` : '';
if (e.type === "send_url") {
const auth = e.username || e.password ? `${e.username}:${e.password}@` : "";
const isDefaultPort =
(e.scheme === 'http' && e.port === 80) || (e.scheme === 'https' && e.port === 443);
const portStr = isDefaultPort ? '' : `:${e.port}`;
const query = e.query ? `?${e.query}` : '';
const fragment = e.fragment ? `#${e.fragment}` : '';
(e.scheme === "http" && e.port === 80) || (e.scheme === "https" && e.port === 443);
const portStr = isDefaultPort ? "" : `:${e.port}`;
const query = e.query ? `?${e.query}` : "";
const fragment = e.fragment ? `#${e.fragment}` : "";
const fullUrl = `${e.scheme}://${auth}${e.host}${portStr}${e.path}${query}${fragment}`;
return (
<KeyValueRows>
@@ -174,7 +174,7 @@ function EventDetails({
}
// Response status - show version and status separately
if (e.type === 'receive_url') {
if (e.type === "receive_url") {
return (
<KeyValueRows>
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
@@ -186,7 +186,7 @@ function EventDetails({
}
// Redirect - show status, URL, and behavior
if (e.type === 'redirect') {
if (e.type === "redirect") {
const droppedHeaders = e.dropped_headers ?? [];
return (
<KeyValueRows>
@@ -195,18 +195,18 @@ function EventDetails({
</KeyValueRow>
<KeyValueRow label="Location">{e.url}</KeyValueRow>
<KeyValueRow label="Behavior">
{e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'}
{e.behavior === "drop_body" ? "Drop body, change to GET" : "Preserve method and body"}
</KeyValueRow>
<KeyValueRow label="Body Dropped">{e.dropped_body ? 'Yes' : 'No'}</KeyValueRow>
<KeyValueRow label="Body Dropped">{e.dropped_body ? "Yes" : "No"}</KeyValueRow>
<KeyValueRow label="Headers Dropped">
{droppedHeaders.length > 0 ? droppedHeaders.join(', ') : '--'}
{droppedHeaders.length > 0 ? droppedHeaders.join(", ") : "--"}
</KeyValueRow>
</KeyValueRows>
);
}
// Settings - show as key/value
if (e.type === 'setting') {
if (e.type === "setting") {
return (
<KeyValueRows>
<KeyValueRow label="Setting">{e.name}</KeyValueRow>
@@ -216,16 +216,16 @@ function EventDetails({
}
// Chunks - show formatted bytes
if (e.type === 'chunk_sent' || e.type === 'chunk_received') {
if (e.type === "chunk_sent" || e.type === "chunk_received") {
return <div className="font-mono text-editor">{formatBytes(e.bytes)}</div>;
}
// DNS Resolution - show hostname, addresses, and timing
if (e.type === 'dns_resolved') {
if (e.type === "dns_resolved") {
return (
<KeyValueRows>
<KeyValueRow label="Hostname">{e.hostname}</KeyValueRow>
<KeyValueRow label="Addresses">{e.addresses.join(', ')}</KeyValueRow>
<KeyValueRow label="Addresses">{e.addresses.join(", ")}</KeyValueRow>
<KeyValueRow label="Duration">
{e.overridden ? (
<span className="text-text-subtlest">--</span>
@@ -255,57 +255,57 @@ function EventDetails({
);
}
type EventTextParts = { prefix: '>' | '<' | '*'; text: string };
type EventTextParts = { prefix: ">" | "<" | "*"; text: string };
/** Get the prefix and text for an event */
function getEventTextParts(event: HttpResponseEventData): EventTextParts {
switch (event.type) {
case 'send_url':
case "send_url":
return {
prefix: '>',
text: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`,
prefix: ">",
text: `${event.method} ${event.path}${event.query ? `?${event.query}` : ""}${event.fragment ? `#${event.fragment}` : ""}`,
};
case 'receive_url':
return { prefix: '<', text: `${event.version} ${event.status}` };
case 'header_up':
return { prefix: '>', text: `${event.name}: ${event.value}` };
case 'header_down':
return { prefix: '<', text: `${event.name}: ${event.value}` };
case 'redirect': {
const behavior = event.behavior === 'drop_body' ? 'drop body' : 'preserve';
case "receive_url":
return { prefix: "<", text: `${event.version} ${event.status}` };
case "header_up":
return { prefix: ">", text: `${event.name}: ${event.value}` };
case "header_down":
return { prefix: "<", text: `${event.name}: ${event.value}` };
case "redirect": {
const behavior = event.behavior === "drop_body" ? "drop body" : "preserve";
const droppedHeaders = event.dropped_headers ?? [];
const dropped = [
event.dropped_body ? 'body dropped' : null,
droppedHeaders.length > 0 ? `headers dropped: ${droppedHeaders.join(', ')}` : null,
event.dropped_body ? "body dropped" : null,
droppedHeaders.length > 0 ? `headers dropped: ${droppedHeaders.join(", ")}` : null,
]
.filter(Boolean)
.join(', ');
.join(", ");
return {
prefix: '*',
text: `Redirect ${event.status} -> ${event.url} (${behavior}${dropped ? `, ${dropped}` : ''})`,
prefix: "*",
text: `Redirect ${event.status} -> ${event.url} (${behavior}${dropped ? `, ${dropped}` : ""})`,
};
}
case 'setting':
return { prefix: '*', text: `Setting ${event.name}=${event.value}` };
case 'info':
return { prefix: '*', text: event.message };
case 'chunk_sent':
return { prefix: '*', text: `[${formatBytes(event.bytes)} sent]` };
case 'chunk_received':
return { prefix: '*', text: `[${formatBytes(event.bytes)} received]` };
case 'dns_resolved':
case "setting":
return { prefix: "*", text: `Setting ${event.name}=${event.value}` };
case "info":
return { prefix: "*", text: event.message };
case "chunk_sent":
return { prefix: "*", text: `[${formatBytes(event.bytes)} sent]` };
case "chunk_received":
return { prefix: "*", text: `[${formatBytes(event.bytes)} received]` };
case "dns_resolved":
if (event.overridden) {
return {
prefix: '*',
text: `DNS override ${event.hostname} -> ${event.addresses.join(', ')}`,
prefix: "*",
text: `DNS override ${event.hostname} -> ${event.addresses.join(", ")}`,
};
}
return {
prefix: '*',
text: `DNS resolved ${event.hostname} to ${event.addresses.join(', ')} (${event.duration}ms)`,
prefix: "*",
text: `DNS resolved ${event.hostname} to ${event.addresses.join(", ")} (${event.duration}ms)`,
};
default:
return { prefix: '*', text: '[unknown event]' };
return { prefix: "*", text: "[unknown event]" };
}
}
@@ -316,103 +316,103 @@ function formatEventText(event: HttpResponseEventData, includePrefix: boolean):
}
type EventDisplay = {
icon: IconProps['icon'];
color: IconProps['color'];
icon: IconProps["icon"];
color: IconProps["color"];
label: string;
summary: ReactNode;
};
function getEventDisplay(event: HttpResponseEventData): EventDisplay {
switch (event.type) {
case 'setting':
case "setting":
return {
icon: 'settings',
color: 'secondary',
label: 'Setting',
icon: "settings",
color: "secondary",
label: "Setting",
summary: `${event.name} = ${event.value}`,
};
case 'info':
case "info":
return {
icon: 'info',
color: 'secondary',
label: 'Info',
icon: "info",
color: "secondary",
label: "Info",
summary: event.message,
};
case 'redirect': {
case "redirect": {
const droppedHeaders = event.dropped_headers ?? [];
const dropped = [
event.dropped_body ? 'drop body' : null,
event.dropped_body ? "drop body" : null,
droppedHeaders.length > 0
? `drop ${droppedHeaders.length} ${droppedHeaders.length === 1 ? 'header' : 'headers'}`
? `drop ${droppedHeaders.length} ${droppedHeaders.length === 1 ? "header" : "headers"}`
: null,
]
.filter(Boolean)
.join(', ');
.join(", ");
return {
icon: 'arrow_big_right_dash',
color: 'success',
label: 'Redirect',
summary: `Redirecting ${event.status} ${event.url}${dropped ? ` (${dropped})` : ''}`,
icon: "arrow_big_right_dash",
color: "success",
label: "Redirect",
summary: `Redirecting ${event.status} ${event.url}${dropped ? ` (${dropped})` : ""}`,
};
}
case 'send_url':
case "send_url":
return {
icon: 'arrow_big_up_dash',
color: 'primary',
label: 'Request',
summary: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`,
icon: "arrow_big_up_dash",
color: "primary",
label: "Request",
summary: `${event.method} ${event.path}${event.query ? `?${event.query}` : ""}${event.fragment ? `#${event.fragment}` : ""}`,
};
case 'receive_url':
case "receive_url":
return {
icon: 'arrow_big_down_dash',
color: 'info',
label: 'Response',
icon: "arrow_big_down_dash",
color: "info",
label: "Response",
summary: `${event.version} ${event.status}`,
};
case 'header_up':
case "header_up":
return {
icon: 'arrow_big_up_dash',
color: 'primary',
label: 'Header',
icon: "arrow_big_up_dash",
color: "primary",
label: "Header",
summary: `${event.name}: ${event.value}`,
};
case 'header_down':
case "header_down":
return {
icon: 'arrow_big_down_dash',
color: 'info',
label: 'Header',
icon: "arrow_big_down_dash",
color: "info",
label: "Header",
summary: `${event.name}: ${event.value}`,
};
case 'chunk_sent':
case "chunk_sent":
return {
icon: 'info',
color: 'secondary',
label: 'Chunk',
icon: "info",
color: "secondary",
label: "Chunk",
summary: `${formatBytes(event.bytes)} chunk sent`,
};
case 'chunk_received':
case "chunk_received":
return {
icon: 'info',
color: 'secondary',
label: 'Chunk',
icon: "info",
color: "secondary",
label: "Chunk",
summary: `${formatBytes(event.bytes)} chunk received`,
};
case 'dns_resolved':
case "dns_resolved":
return {
icon: 'globe',
color: event.overridden ? 'success' : 'secondary',
label: event.overridden ? 'DNS Override' : 'DNS',
icon: "globe",
color: event.overridden ? "success" : "secondary",
label: event.overridden ? "DNS Override" : "DNS",
summary: event.overridden
? `${event.hostname}${event.addresses.join(', ')} (overridden)`
: `${event.hostname}${event.addresses.join(', ')} (${event.duration}ms)`,
? `${event.hostname}${event.addresses.join(", ")} (overridden)`
: `${event.hostname}${event.addresses.join(", ")} (${event.duration}ms)`,
};
default:
return {
icon: 'info',
color: 'secondary',
label: 'Unknown',
summary: 'Unknown event',
icon: "info",
color: "secondary",
label: "Unknown",
summary: "Unknown event",
};
}
}

View File

@@ -1,24 +1,26 @@
import { clear, readText } from '@tauri-apps/plugin-clipboard-manager';
import * as m from 'motion/react-m';
import { useEffect, useState } from 'react';
import { useImportCurl } from '../hooks/useImportCurl';
import { useWindowFocus } from '../hooks/useWindowFocus';
import { Button } from './core/Button';
import { Icon } from './core/Icon';
import { clear, readText } from "@tauri-apps/plugin-clipboard-manager";
import * as m from "motion/react-m";
import { useEffect, useState } from "react";
import { useImportCurl } from "../hooks/useImportCurl";
import { useWindowFocus } from "../hooks/useWindowFocus";
import { Button } from "./core/Button";
import { Icon } from "./core/Icon";
export function ImportCurlButton() {
const focused = useWindowFocus();
const [clipboardText, setClipboardText] = useState('');
const [clipboardText, setClipboardText] = useState("");
const importCurl = useImportCurl();
const [isLoading, setIsLoading] = useState(false);
// oxlint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
readText().then(setClipboardText).catch(() => {});
readText()
.then(setClipboardText)
.catch(() => {});
}, [focused]);
if (!clipboardText?.trim().startsWith('curl ')) {
if (!clipboardText?.trim().startsWith("curl ")) {
return null;
}
@@ -41,9 +43,9 @@ export function ImportCurlButton() {
try {
await importCurl.mutateAsync({ command: clipboardText });
await clear(); // Clear the clipboard so the button goes away
setClipboardText('');
setClipboardText("");
} catch (e) {
console.log('Failed to import curl', e);
console.log("Failed to import curl", e);
} finally {
setIsLoading(false);
}

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useLocalStorage } from 'react-use';
import { Button } from './core/Button';
import { VStack } from './core/Stacks';
import { SelectFile } from './SelectFile';
import { useState } from "react";
import { useLocalStorage } from "react-use";
import { Button } from "./core/Button";
import { VStack } from "./core/Stacks";
import { SelectFile } from "./SelectFile";
interface Props {
importData: (filePath: string) => Promise<void>;
@@ -10,7 +10,7 @@ interface Props {
export function ImportDataDialog({ importData }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filePath, setFilePath] = useLocalStorage<string | null>('importFilePath', null);
const [filePath, setFilePath] = useLocalStorage<string | null>("importFilePath", null);
return (
<VStack space={5} className="pb-4">
@@ -45,7 +45,7 @@ export function ImportDataDialog({ importData }: Props) {
}
}}
>
{isLoading ? 'Importing' : 'Import'}
{isLoading ? "Importing" : "Import"}
</Button>
)}
</VStack>

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { appInfo } from '../lib/appInfo';
import type { ReactNode } from "react";
import { appInfo } from "../lib/appInfo";
interface Props {
children: ReactNode;

View File

@@ -1,23 +1,23 @@
import { linter } from '@codemirror/lint';
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { useCallback, useMemo } from 'react';
import { fireAndForget } from '../lib/fireAndForget';
import { useKeyValue } from '../hooks/useKeyValue';
import { textLikelyContainsJsonComments } from '../lib/jsonComments';
import { Banner } from './core/Banner';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import type { EditorProps } from './core/Editor/Editor';
import { jsonParseLinter } from './core/Editor/json-lint';
import { Editor } from './core/Editor/LazyEditor';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { IconTooltip } from './core/IconTooltip';
import { linter } from "@codemirror/lint";
import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import { useCallback, useMemo } from "react";
import { fireAndForget } from "../lib/fireAndForget";
import { useKeyValue } from "../hooks/useKeyValue";
import { textLikelyContainsJsonComments } from "../lib/jsonComments";
import { Banner } from "./core/Banner";
import type { DropdownItem } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import type { EditorProps } from "./core/Editor/Editor";
import { jsonParseLinter } from "./core/Editor/json-lint";
import { Editor } from "./core/Editor/LazyEditor";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { IconTooltip } from "./core/IconTooltip";
interface Props {
forceUpdateKey: string;
heightMode: EditorProps['heightMode'];
heightMode: EditorProps["heightMode"];
request: HttpRequest;
}
@@ -42,13 +42,13 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
);
const hasComments = useMemo(
() => textLikelyContainsJsonComments(request.body?.text ?? ''),
() => textLikelyContainsJsonComments(request.body?.text ?? ""),
[request.body?.text],
);
const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['json-fix-3', request.workspaceId],
namespace: "no_sync",
key: ["json-fix-3", request.workspaceId],
fallback: false,
});
@@ -70,8 +70,8 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
const showBanner = hasComments && autoFix && !bannerDismissed;
const stripMessage = 'Automatically strip comments and trailing commas before sending';
const actions = useMemo<EditorProps['actions']>(
const stripMessage = "Automatically strip comments and trailing commas before sending";
const actions = useMemo<EditorProps["actions"]>(
() => [
showBanner && (
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs">
@@ -87,12 +87,12 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
items={
[
{
label: 'Automatically Fix JSON',
label: "Automatically Fix JSON",
keepOpenOnSelect: true,
onSelect: handleToggleAutoFix,
rightSlot: <IconTooltip content={stripMessage} />,
leftSlot: (
<Icon icon={autoFix ? 'check_square_checked' : 'check_square_unchecked'} />
<Icon icon={autoFix ? "check_square_checked" : "check_square_unchecked"} />
),
},
] satisfies DropdownItem[]
@@ -112,7 +112,7 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
autocompleteVariables
placeholder="..."
heightMode={heightMode}
defaultValue={`${request.body?.text ?? ''}`}
defaultValue={`${request.body?.text ?? ""}`}
language="json"
onChange={handleChange}
stateKey={`json.${request.id}`}

View File

@@ -1,5 +1,5 @@
import { hotkeyActions } from '../hooks/useHotKey';
import { HotkeyList } from './core/HotkeyList';
import { hotkeyActions } from "../hooks/useHotKey";
import { HotkeyList } from "./core/HotkeyList";
export function KeyboardShortcutsDialog() {
return (

View File

@@ -1,62 +1,62 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license';
import { settingsAtom } from '@yaakapp-internal/models';
import { differenceInCalendarDays } from 'date-fns';
import { formatDate } from 'date-fns/format';
import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { jotaiStore } from '../lib/jotai';
import { CargoFeature } from './CargoFeature';
import type { ButtonProps } from './core/Button';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { PillButton } from './core/PillButton';
import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useLicense } from "@yaakapp-internal/license";
import { settingsAtom } from "@yaakapp-internal/models";
import { differenceInCalendarDays } from "date-fns";
import { formatDate } from "date-fns/format";
import { useAtomValue } from "jotai";
import type { ReactNode } from "react";
import { openSettings } from "../commands/openSettings";
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
import { jotaiStore } from "../lib/jotai";
import { CargoFeature } from "./CargoFeature";
import type { ButtonProps } from "./core/Button";
import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { PillButton } from "./core/PillButton";
const dismissedAtom = atomWithKVStorage<string | null>('dismissed_license_expired', null);
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
function getDetail(
data: LicenseCheckStatus,
dismissedExpired: string | null,
): { label: ReactNode; color: ButtonProps['color']; options?: DropdownItem[] } | null | undefined {
): { label: ReactNode; color: ButtonProps["color"]; options?: DropdownItem[] } | null | undefined {
const dismissedAt = dismissedExpired ? new Date(dismissedExpired).getTime() : null;
switch (data.status) {
case 'active':
case "active":
return null;
case 'personal_use':
return { label: 'Personal Use', color: 'notice' };
case 'trialing':
return { label: 'Commercial Trial', color: 'secondary' };
case 'error':
return { label: 'Error', color: 'danger' };
case 'inactive':
return { label: 'Personal Use', color: 'notice' };
case 'past_due':
return { label: 'Past Due', color: 'danger' };
case 'expired':
case "personal_use":
return { label: "Personal Use", color: "notice" };
case "trialing":
return { label: "Commercial Trial", color: "secondary" };
case "error":
return { label: "Error", color: "danger" };
case "inactive":
return { label: "Personal Use", color: "notice" };
case "past_due":
return { label: "Past Due", color: "danger" };
case "expired":
// Don't show the expired message if it's been less than 14 days since the last dismissal
if (dismissedAt && differenceInCalendarDays(new Date(), dismissedAt) < 14) {
return null;
}
return {
color: 'notice',
label: data.data.changes > 0 ? 'Updates Paused' : 'License Expired',
color: "notice",
label: data.data.changes > 0 ? "Updates Paused" : "License Expired",
options: [
{
label: `${data.data.changes} New Updates`,
color: 'success',
color: "success",
leftSlot: <Icon icon="gift" />,
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
hidden: data.data.changes === 0 || data.data.changesUrl == null,
onSelect: () => openUrl(data.data.changesUrl ?? ''),
onSelect: () => openUrl(data.data.changesUrl ?? ""),
},
{
type: 'separator',
label: `License expired ${formatDate(data.data.periodEnd, 'MMM dd, yyyy')}`,
type: "separator",
label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`,
},
{
label: <div className="min-w-[12rem]">Renew License</div>,
@@ -66,12 +66,12 @@ function getDetail(
onSelect: () => openUrl(data.data.billingUrl),
},
{
label: 'Enter License Key',
label: "Enter License Key",
leftSlot: <Icon icon="key_round" />,
hidden: data.data.changesUrl == null,
onSelect: openLicenseDialog,
},
{ type: 'separator' },
{ type: "separator" },
{
label: <span className="text-text-subtle">Remind me Later</span>,
leftSlot: <Icon icon="alarm_clock" className="text-text-subtle" />,
@@ -135,5 +135,5 @@ function LicenseBadgeCmp() {
}
function openLicenseDialog() {
openSettings.mutate('license');
openSettings.mutate("license");
}

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { convertFileSrc } from '@tauri-apps/api/core';
import { resolveResource } from '@tauri-apps/api/path';
import classNames from 'classnames';
import { useQuery } from "@tanstack/react-query";
import { convertFileSrc } from "@tauri-apps/api/core";
import { resolveResource } from "@tauri-apps/api/path";
import classNames from "classnames";
interface Props {
src: string;
@@ -10,7 +10,7 @@ interface Props {
export function LocalImage({ src: srcPath, className }: Props) {
const src = useQuery({
queryKey: ['local-image', srcPath],
queryKey: ["local-image", srcPath],
queryFn: async () => {
const p = await resolveResource(srcPath);
return convertFileSrc(p);
@@ -23,8 +23,8 @@ export function LocalImage({ src: srcPath, className }: Props) {
alt="Response preview"
className={classNames(
className,
'transition-opacity',
src.data == null ? 'opacity-0' : 'opacity-100',
"transition-opacity",
src.data == null ? "opacity-0" : "opacity-100",
)}
/>
);

View File

@@ -1,9 +1,9 @@
import type { CSSProperties } from 'react';
import ReactMarkdown, { type Components } from 'react-markdown';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import remarkGfm from 'remark-gfm';
import { ErrorBoundary } from './ErrorBoundary';
import { Prose } from './Prose';
import type { CSSProperties } from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import remarkGfm from "remark-gfm";
import { ErrorBoundary } from "./ErrorBoundary";
import { Prose } from "./Prose";
interface Props {
children: string | null;
@@ -30,48 +30,48 @@ const prismTheme = {
},
// Syntax tokens
comment: { color: 'var(--textSubtle)' },
prolog: { color: 'var(--textSubtle)' },
doctype: { color: 'var(--textSubtle)' },
cdata: { color: 'var(--textSubtle)' },
comment: { color: "var(--textSubtle)" },
prolog: { color: "var(--textSubtle)" },
doctype: { color: "var(--textSubtle)" },
cdata: { color: "var(--textSubtle)" },
punctuation: { color: 'var(--textSubtle)' },
punctuation: { color: "var(--textSubtle)" },
property: { color: 'var(--primary)' },
'attr-name': { color: 'var(--primary)' },
property: { color: "var(--primary)" },
"attr-name": { color: "var(--primary)" },
string: { color: 'var(--notice)' },
char: { color: 'var(--notice)' },
string: { color: "var(--notice)" },
char: { color: "var(--notice)" },
number: { color: 'var(--info)' },
constant: { color: 'var(--info)' },
symbol: { color: 'var(--info)' },
number: { color: "var(--info)" },
constant: { color: "var(--info)" },
symbol: { color: "var(--info)" },
boolean: { color: 'var(--warning)' },
'attr-value': { color: 'var(--warning)' },
boolean: { color: "var(--warning)" },
"attr-value": { color: "var(--warning)" },
variable: { color: 'var(--success)' },
variable: { color: "var(--success)" },
tag: { color: 'var(--info)' },
operator: { color: 'var(--danger)' },
keyword: { color: 'var(--danger)' },
function: { color: 'var(--success)' },
'class-name': { color: 'var(--primary)' },
builtin: { color: 'var(--danger)' },
selector: { color: 'var(--danger)' },
inserted: { color: 'var(--success)' },
deleted: { color: 'var(--danger)' },
regex: { color: 'var(--warning)' },
tag: { color: "var(--info)" },
operator: { color: "var(--danger)" },
keyword: { color: "var(--danger)" },
function: { color: "var(--success)" },
"class-name": { color: "var(--primary)" },
builtin: { color: "var(--danger)" },
selector: { color: "var(--danger)" },
inserted: { color: "var(--success)" },
deleted: { color: "var(--danger)" },
regex: { color: "var(--warning)" },
important: { color: 'var(--danger)', fontWeight: 'bold' },
italic: { fontStyle: 'italic' },
bold: { fontWeight: 'bold' },
entity: { cursor: 'help' },
important: { color: "var(--danger)", fontWeight: "bold" },
italic: { fontStyle: "italic" },
bold: { fontWeight: "bold" },
entity: { cursor: "help" },
};
const lineStyle: CSSProperties = {
paddingRight: '1.5em',
paddingLeft: '0',
paddingRight: "1.5em",
paddingLeft: "0",
opacity: 0.5,
};
@@ -91,7 +91,7 @@ const markdownComponents: Partial<Components> = {
const { children, className, ref, ...extraProps } = props;
extraProps.node = undefined;
const match = /language-(\w+)/.exec(className || '');
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
{...extraProps}
@@ -102,7 +102,7 @@ const markdownComponents: Partial<Components> = {
language={match[1]}
style={prismTheme}
>
{String(children as string).replace(/\n$/, '')}
{String(children as string).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code {...extraProps} ref={ref} className={className}>

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames';
import { useRef, useState } from 'react';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { SegmentedControl } from './core/SegmentedControl';
import { Markdown } from './Markdown';
import classNames from "classnames";
import { useRef, useState } from "react";
import type { EditorProps } from "./core/Editor/Editor";
import { Editor } from "./core/Editor/LazyEditor";
import { SegmentedControl } from "./core/SegmentedControl";
import { Markdown } from "./Markdown";
type ViewMode = 'edit' | 'preview';
type ViewMode = "edit" | "preview";
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey' | 'forceUpdateKey'> {
interface Props extends Pick<EditorProps, "heightMode" | "stateKey" | "forceUpdateKey"> {
placeholder: string;
className?: string;
editorClassName?: string;
@@ -25,7 +25,7 @@ export function MarkdownEditor({
forceUpdateKey,
...editorProps
}: Props) {
const [viewMode, setViewMode] = useState<ViewMode>(defaultValue ? 'preview' : 'edit');
const [viewMode, setViewMode] = useState<ViewMode>(defaultValue ? "preview" : "edit");
const containerRef = useRef<HTMLDivElement>(null);
@@ -33,7 +33,7 @@ export function MarkdownEditor({
<Editor
hideGutter
wrapLines
className={classNames(editorClassName, '[&_.cm-line]:!max-w-lg max-h-full')}
className={classNames(editorClassName, "[&_.cm-line]:!max-w-lg max-h-full")}
language="markdown"
defaultValue={defaultValue}
onChange={onChange}
@@ -51,15 +51,15 @@ export function MarkdownEditor({
</div>
);
const contents = viewMode === 'preview' ? preview : editor;
const contents = viewMode === "preview" ? preview : editor;
return (
<div
ref={containerRef}
className={classNames(
'group/markdown',
'relative w-full h-full pt-1.5 rounded-md gap-x-1.5',
'min-w-0', // Not sure why this is needed
"group/markdown",
"relative w-full h-full pt-1.5 rounded-md gap-x-1.5",
"min-w-0", // Not sure why this is needed
className,
)}
>
@@ -73,8 +73,8 @@ export function MarkdownEditor({
value={viewMode}
className="opacity-0 group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100"
options={[
{ icon: 'eye', label: 'Preview mode', value: 'preview' },
{ icon: 'pencil', label: 'Edit mode', value: 'edit' },
{ icon: "eye", label: "Preview mode", value: "preview" },
{ icon: "pencil", label: "Edit mode", value: "edit" },
]}
/>
</div>

View File

@@ -1,15 +1,15 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { patchModel, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { pluralizeCount } from '../lib/pluralize';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { showToast } from '../lib/toast';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { patchModel, workspacesAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { pluralizeCount } from "../lib/pluralize";
import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from "../lib/router";
import { showToast } from "../lib/toast";
import { Button } from "./core/Button";
import { InlineCode } from "./core/InlineCode";
import { Select } from "./core/Select";
import { VStack } from "./core/Stacks";
interface Props {
activeWorkspaceId: string;
@@ -50,17 +50,17 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
// Hide after a moment, to give time for requests to disappear
setTimeout(onDone, 100);
showToast({
id: 'workspace-moved',
id: "workspace-moved",
message:
requests.length === 1 && requests[0] != null ? (
<>
<InlineCode>{resolvedModelName(requests[0])}</InlineCode> moved to{' '}
<InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode>
<InlineCode>{resolvedModelName(requests[0])}</InlineCode> moved to{" "}
<InlineCode>{targetWorkspace?.name ?? "unknown"}</InlineCode>
</>
) : (
<>
{pluralizeCount('request', requests.length)} moved to{' '}
<InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode>
{pluralizeCount("request", requests.length)} moved to{" "}
<InlineCode>{targetWorkspace?.name ?? "unknown"}</InlineCode>
</>
),
action: ({ hide }) => (
@@ -70,7 +70,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
className="mr-auto min-w-[5rem]"
onClick={async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: selectedWorkspaceId },
});
hide();
@@ -82,7 +82,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
});
}}
>
{requests.length === 1 ? 'Move' : `Move ${pluralizeCount('Request', requests.length)}`}
{requests.length === 1 ? "Move" : `Move ${pluralizeCount("Request", requests.length)}`}
</Button>
</VStack>
);

View File

@@ -1,9 +1,9 @@
import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import { useRef } from 'react';
import { Portal } from './Portal';
import classNames from "classnames";
import { FocusTrap } from "focus-trap-react";
import * as m from "motion/react-m";
import type { ReactNode } from "react";
import { useRef } from "react";
import { Portal } from "./Portal";
interface Props {
children: ReactNode;
@@ -11,20 +11,20 @@ interface Props {
open: boolean;
onClose?: () => void;
zIndex?: keyof typeof zIndexes;
variant?: 'default' | 'transparent';
variant?: "default" | "transparent";
noBackdrop?: boolean;
}
const zIndexes: Record<number, string> = {
10: 'z-10',
20: 'z-20',
30: 'z-30',
40: 'z-40',
50: 'z-50',
10: "z-10",
20: "z-20",
30: "z-30",
40: "z-40",
50: "z-50",
};
export function Overlay({
variant = 'default',
variant = "default",
zIndex = 30,
open,
onClose,
@@ -63,7 +63,7 @@ export function Overlay({
>
<m.div
ref={containerRef}
className={classNames('fixed inset-0', zIndexes[zIndex])}
className={classNames("fixed inset-0", zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
@@ -71,14 +71,14 @@ export function Overlay({
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
"absolute inset-0",
variant === "default" && "bg-backdrop backdrop-blur-sm",
)}
/>
{/* Show the draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && (
{variant === "default" && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)}
{children}

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { usePortal } from '../hooks/usePortal';
import type { ReactNode } from "react";
import { createPortal } from "react-dom";
import { usePortal } from "../hooks/usePortal";
interface Props {
children: ReactNode;

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import './Prose.css';
import classNames from "classnames";
import type { ReactNode } from "react";
import "./Prose.css";
interface Props {
children: ReactNode;
@@ -8,5 +8,5 @@ interface Props {
}
export function Prose({ className, ...props }: Props) {
return <div className={classNames('prose', className)} {...props} />;
return <div className={classNames("prose", className)} {...props} />;
}

View File

@@ -1,12 +1,12 @@
import type { GrpcConnection } from '@yaakapp-internal/models';
import { deleteModel } from '@yaakapp-internal/models';
import { formatDistanceToNowStrict } from 'date-fns';
import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections';
import { pluralizeCount } from '../lib/pluralize';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import type { GrpcConnection } from "@yaakapp-internal/models";
import { deleteModel } from "@yaakapp-internal/models";
import { formatDistanceToNowStrict } from "date-fns";
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { HStack } from "./core/Stacks";
interface Props {
connections: GrpcConnection[];
@@ -20,27 +20,27 @@ export function RecentGrpcConnectionsDropdown({
onPinnedConnectionId,
}: Props) {
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
const latestConnectionId = connections[0]?.id ?? 'n/a';
const latestConnectionId = connections[0]?.id ?? "n/a";
return (
<Dropdown
items={[
{
label: 'Clear Connection',
label: "Clear Connection",
onSelect: () => deleteModel(activeConnection),
disabled: connections.length === 0,
},
{
label: `Clear ${pluralizeCount('Connection', connections.length)}`,
label: `Clear ${pluralizeCount("Connection", connections.length)}`,
onSelect: deleteAllConnections.mutate,
hidden: connections.length <= 1,
disabled: connections.length === 0,
},
{ type: 'separator', label: 'History' },
{ type: "separator", label: "History" },
...connections.map((c) => ({
label: (
<HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '}
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),
@@ -51,7 +51,7 @@ export function RecentGrpcConnectionsDropdown({
>
<IconButton
title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'history' : 'pin'}
icon={activeConnection?.id === latestConnectionId ? "history" : "pin"}
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"

View File

@@ -1,14 +1,14 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { deleteModel } from '@yaakapp-internal/models';
import { useCopyHttpResponse } from '../hooks/useCopyHttpResponse';
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
import { useSaveResponse } from '../hooks/useSaveResponse';
import { pluralize } from '../lib/pluralize';
import { Dropdown } from './core/Dropdown';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import type { HttpResponse } from "@yaakapp-internal/models";
import { deleteModel } from "@yaakapp-internal/models";
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
import { useSaveResponse } from "../hooks/useSaveResponse";
import { pluralize } from "../lib/pluralize";
import { Dropdown } from "./core/Dropdown";
import { HttpStatusTag } from "./core/HttpStatusTag";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { HStack } from "./core/Stacks";
interface Props {
responses: HttpResponse[];
@@ -23,7 +23,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
onPinnedResponseId,
}: Props) {
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const latestResponseId = responses[0]?.id ?? 'n/a';
const latestResponseId = responses[0]?.id ?? "n/a";
const saveResponse = useSaveResponse(activeResponse);
const copyResponse = useCopyHttpResponse(activeResponse);
@@ -31,45 +31,45 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
<Dropdown
items={[
{
label: 'Save to File',
label: "Save to File",
onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />,
hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
},
{
label: 'Copy Body',
label: "Copy Body",
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
},
{
label: 'Delete',
label: "Delete",
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteModel(activeResponse),
},
{
label: 'Unpin Response',
label: "Unpin Response",
onSelect: () => onPinnedResponseId(activeResponse.id),
leftSlot: <Icon icon="unpin" />,
hidden: latestResponseId === activeResponse.id,
disabled: responses.length === 0,
},
{ type: 'separator', label: 'History' },
{ type: "separator", label: "History" },
{
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`,
label: `Delete ${responses.length} ${pluralize("Response", responses.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{ type: 'separator' },
{ type: "separator" },
...responses.map((r: HttpResponse) => ({
label: (
<HStack space={2}>
<HttpStatusTag short className="text-xs" response={r} />
<span className="text-text-subtle">&rarr;</span>{' '}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
<span className="text-text-subtle">&rarr;</span>{" "}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : "n/a"}</span>
</HStack>
),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
@@ -79,7 +79,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
>
<IconButton
title="Show response history"
icon={activeResponse?.id === latestResponseId ? 'history' : 'pin'}
icon={activeResponse?.id === latestResponseId ? "history" : "pin"}
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"

View File

@@ -1,18 +1,18 @@
import classNames from 'classnames';
import { useMemo, useRef } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useHotKey } from '../hooks/useHotKey';
import { useKeyboardEvent } from '../hooks/useKeyboardEvent';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { Button } from './core/Button';
import type { DropdownItem, DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
import classNames from "classnames";
import { useMemo, useRef } from "react";
import { useActiveRequest } from "../hooks/useActiveRequest";
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { allRequestsAtom } from "../hooks/useAllRequests";
import { useHotKey } from "../hooks/useHotKey";
import { useKeyboardEvent } from "../hooks/useKeyboardEvent";
import { useRecentRequests } from "../hooks/useRecentRequests";
import { jotaiStore } from "../lib/jotai";
import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from "../lib/router";
import { Button } from "./core/Button";
import type { DropdownItem, DropdownRef } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import { HttpMethodTag } from "./core/HttpMethodTag";
interface Props {
className?: string;
@@ -26,13 +26,13 @@ export function RecentRequestsDropdown({ className }: Props) {
// Handle key-up
// TODO: Somehow make useHotKey have this functionality. Note: e.key does not work
// on Linux, for example, when Control is mapped to CAPS. This will never fire.
useKeyboardEvent('keyup', 'Control', () => {
useKeyboardEvent("keyup", "Control", () => {
if (dropdownRef.current?.isOpen) {
dropdownRef.current?.select?.();
}
});
useHotKey('switcher.prev', () => {
useHotKey("switcher.prev", () => {
if (!dropdownRef.current?.isOpen) {
// Select the second because the first is the current request
dropdownRef.current?.open(1);
@@ -41,7 +41,7 @@ export function RecentRequestsDropdown({ className }: Props) {
}
});
useHotKey('switcher.next', () => {
useHotKey("switcher.next", () => {
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
dropdownRef.current?.prev?.();
});
@@ -61,7 +61,7 @@ export function RecentRequestsDropdown({ className }: Props) {
leftSlot: <HttpMethodTag short className="text-xs" request={request} />,
onSelect: async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: activeWorkspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
@@ -73,8 +73,8 @@ export function RecentRequestsDropdown({ className }: Props) {
if (recentRequestItems.length === 0) {
return [
{
key: 'no-recent-requests',
label: 'No recent requests',
key: "no-recent-requests",
label: "No recent requests",
disabled: true,
},
];
@@ -90,8 +90,8 @@ export function RecentRequestsDropdown({ className }: Props) {
hotkeyAction="switcher.toggle"
className={classNames(
className,
'truncate pointer-events-auto',
activeRequest == null && 'text-text-subtlest italic',
"truncate pointer-events-auto",
activeRequest == null && "text-text-subtlest italic",
)}
>
{resolvedModelName(activeRequest)}

View File

@@ -1,12 +1,12 @@
import type { WebsocketConnection } from '@yaakapp-internal/models';
import { deleteModel, getModel } from '@yaakapp-internal/models';
import { formatDistanceToNowStrict } from 'date-fns';
import { deleteWebsocketConnections } from '../commands/deleteWebsocketConnections';
import { pluralizeCount } from '../lib/pluralize';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import type { WebsocketConnection } from "@yaakapp-internal/models";
import { deleteModel, getModel } from "@yaakapp-internal/models";
import { formatDistanceToNowStrict } from "date-fns";
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { HStack } from "./core/Stacks";
interface Props {
connections: WebsocketConnection[];
@@ -19,20 +19,20 @@ export function RecentWebsocketConnectionsDropdown({
connections,
onPinnedConnectionId,
}: Props) {
const latestConnectionId = connections[0]?.id ?? 'n/a';
const latestConnectionId = connections[0]?.id ?? "n/a";
return (
<Dropdown
items={[
{
label: 'Clear Connection',
label: "Clear Connection",
onSelect: () => deleteModel(activeConnection),
disabled: connections.length === 0,
},
{
label: `Clear ${pluralizeCount('Connection', connections.length)}`,
label: `Clear ${pluralizeCount("Connection", connections.length)}`,
onSelect: () => {
const request = getModel('websocket_request', activeConnection.requestId);
const request = getModel("websocket_request", activeConnection.requestId);
if (request != null) {
deleteWebsocketConnections.mutate(request);
}
@@ -40,11 +40,11 @@ export function RecentWebsocketConnectionsDropdown({
hidden: connections.length <= 1,
disabled: connections.length === 0,
},
{ type: 'separator', label: 'History' },
{ type: "separator", label: "History" },
...connections.map((c) => ({
label: (
<HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '}
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),
@@ -55,7 +55,7 @@ export function RecentWebsocketConnectionsDropdown({
>
<IconButton
title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'history' : 'pin'}
icon={activeConnection?.id === latestConnectionId ? "history" : "pin"}
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"

View File

@@ -1,12 +1,12 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { fireAndForget } from '../lib/fireAndForget';
import { router } from '../lib/router';
import { workspacesAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useEffect } from "react";
import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
import { getRecentRequests } from "../hooks/useRecentRequests";
import { useRecentWorkspaces } from "../hooks/useRecentWorkspaces";
import { fireAndForget } from "../lib/fireAndForget";
import { router } from "../lib/router";
export function RedirectToLatestWorkspace() {
const workspaces = useAtomValue(workspacesAtom);
@@ -14,28 +14,30 @@ export function RedirectToLatestWorkspace() {
useEffect(() => {
if (workspaces.length === 0 || recentWorkspaces == null) {
console.log('No workspaces found to redirect to. Skipping.', {
console.log("No workspaces found to redirect to. Skipping.", {
workspaces,
recentWorkspaces,
});
return;
}
fireAndForget((async () => {
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const params = { workspaceId };
const search = {
cookie_jar_id: cookieJarId,
environment_id: environmentId,
request_id: requestId,
};
fireAndForget(
(async () => {
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? "n/a";
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const params = { workspaceId };
const search = {
cookie_jar_id: cookieJarId,
environment_id: environmentId,
request_id: requestId,
};
console.log('Redirecting to workspace', params, search);
await router.navigate({ to: '/workspaces/$workspaceId', params, search });
})());
console.log("Redirecting to workspace", params, search);
await router.navigate({ to: "/workspaces/$workspaceId", params, search });
})(),
);
}, [recentWorkspaces, workspaces, workspaces.length]);
return null;

View File

@@ -1,20 +1,20 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { lazy, Suspense } from 'react';
import { useHttpRequestBody } from '../hooks/useHttpRequestBody';
import { getMimeTypeFromContentType, languageFromContentType } from '../lib/contentType';
import { LoadingIcon } from './core/LoadingIcon';
import { EmptyStateText } from './EmptyStateText';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { MultipartViewer } from './responseViewers/MultipartViewer';
import { SvgViewer } from './responseViewers/SvgViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
import type { HttpResponse } from "@yaakapp-internal/models";
import { lazy, Suspense } from "react";
import { useHttpRequestBody } from "../hooks/useHttpRequestBody";
import { getMimeTypeFromContentType, languageFromContentType } from "../lib/contentType";
import { LoadingIcon } from "./core/LoadingIcon";
import { EmptyStateText } from "./EmptyStateText";
import { AudioViewer } from "./responseViewers/AudioViewer";
import { CsvViewer } from "./responseViewers/CsvViewer";
import { ImageViewer } from "./responseViewers/ImageViewer";
import { MultipartViewer } from "./responseViewers/MultipartViewer";
import { SvgViewer } from "./responseViewers/SvgViewer";
import { TextViewer } from "./responseViewers/TextViewer";
import { VideoViewer } from "./responseViewers/VideoViewer";
import { WebPageViewer } from "./responseViewers/WebPageViewer";
const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })),
import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })),
);
interface Props {
@@ -48,7 +48,7 @@ function RequestBodyViewerInner({ response }: Props) {
// Try to detect language from content-type header that was sent
const contentTypeHeader = response.requestHeaders.find(
(h) => h.name.toLowerCase() === 'content-type',
(h) => h.name.toLowerCase() === "content-type",
);
const contentType = contentTypeHeader?.value ?? null;
const mimeType = contentType ? getMimeTypeFromContentType(contentType).essence : null;
@@ -56,7 +56,7 @@ function RequestBodyViewerInner({ response }: Props) {
// Route to appropriate viewer based on content type
if (mimeType?.match(/^multipart/i)) {
const boundary = contentType?.split('boundary=')[1] ?? 'unknown';
const boundary = contentType?.split("boundary=")[1] ?? "unknown";
// Create a copy because parseMultipart may detach the buffer
const bodyCopy = new Uint8Array(body);
return (

View File

@@ -1,14 +1,14 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { showPrompt } from '../lib/prompt';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { HttpMethodTag, HttpMethodTagRaw } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import classNames from "classnames";
import { memo, useCallback, useMemo } from "react";
import { showPrompt } from "../lib/prompt";
import { Button } from "./core/Button";
import type { DropdownItem } from "./core/Dropdown";
import { HttpMethodTag, HttpMethodTagRaw } from "./core/HttpMethodTag";
import { Icon } from "./core/Icon";
import type { RadioDropdownItem } from "./core/RadioDropdown";
import { RadioDropdown } from "./core/RadioDropdown";
type Props = {
request: HttpRequest;
@@ -16,14 +16,14 @@ type Props = {
};
const radioItems: RadioDropdownItem<string>[] = [
'GET',
'PUT',
'POST',
'PATCH',
'DELETE',
'OPTIONS',
'QUERY',
'HEAD',
"GET",
"PUT",
"POST",
"PATCH",
"DELETE",
"OPTIONS",
"QUERY",
"HEAD",
].map((m) => ({
value: m,
label: <HttpMethodTagRaw method={m} />,
@@ -43,17 +43,17 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
const itemsAfter = useMemo<DropdownItem[]>(
() => [
{
key: 'custom',
label: 'CUSTOM',
key: "custom",
label: "CUSTOM",
leftSlot: <Icon icon="sparkles" />,
onSelect: async () => {
const newMethod = await showPrompt({
id: 'custom-method',
label: 'Http Method',
title: 'Custom Method',
confirmText: 'Save',
description: 'Enter a custom method name',
placeholder: 'CUSTOM',
id: "custom-method",
label: "Http Method",
title: "Custom Method",
confirmText: "Save",
description: "Enter a custom method name",
placeholder: "CUSTOM",
});
if (newMethod == null) return;
await handleChange(newMethod);
@@ -70,7 +70,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
itemsAfter={itemsAfter}
onChange={handleChange}
>
<Button size="xs" className={classNames(className, 'text-text-subtle hover:text')}>
<Button size="xs" className={classNames(className, "text-text-subtle hover:text")}>
<HttpMethodTag request={request} noAlias />
</Button>
</RadioDropdown>

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
import classNames from "classnames";
import type { CSSProperties, MouseEvent as ReactMouseEvent } from "react";
import { useCallback, useRef, useState } from "react";
const START_DISTANCE = 7;
@@ -18,8 +18,8 @@ interface Props {
onResizeEnd?: () => void;
onResizeMove?: (e: ResizeHandleEvent) => void;
onReset?: () => void;
side: 'left' | 'right' | 'top';
justify: 'center' | 'end' | 'start';
side: "left" | "right" | "top";
justify: "center" | "end" | "start";
}
export function ResizeHandle({
@@ -32,7 +32,7 @@ export function ResizeHandle({
onReset,
side,
}: Props) {
const vertical = side === 'top';
const vertical = side === "top";
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{
move: (e: MouseEvent) => void;
@@ -67,15 +67,15 @@ export function ResizeHandle({
function up() {
setIsResizing(false);
moveState.current = null;
document.documentElement.removeEventListener('mousemove', move);
document.documentElement.removeEventListener('mouseup', up);
document.documentElement.removeEventListener("mousemove", move);
document.documentElement.removeEventListener("mouseup", up);
onResizeEnd?.();
}
moveState.current = { calledStart: false, xStart: e.clientX, yStart: e.clientY, move, up };
document.documentElement.addEventListener('mousemove', move);
document.documentElement.addEventListener('mouseup', up);
document.documentElement.addEventListener("mousemove", move);
document.documentElement.addEventListener("mouseup", up);
},
[onResizeEnd, onResizeMove, onResizeStart, vertical],
);
@@ -88,15 +88,15 @@ export function ResizeHandle({
onPointerDown={handlePointerDown}
className={classNames(
className,
'group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full',
"group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full",
// 'bg-info', // For debugging
vertical ? 'w-full h-1.5 cursor-row-resize' : 'h-full w-1.5 cursor-col-resize',
justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end',
justify === 'start' && 'justify-start',
side === 'right' && 'right-0',
side === 'left' && 'left-0',
side === 'top' && 'top-0',
vertical ? "w-full h-1.5 cursor-row-resize" : "h-full w-1.5 cursor-col-resize",
justify === "center" && "justify-center",
justify === "end" && "justify-end",
justify === "start" && "justify-start",
side === "right" && "right-0",
side === "left" && "left-0",
side === "top" && "top-0",
)}
>
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
@@ -104,9 +104,9 @@ export function ResizeHandle({
<div
className={classNames(
// 'bg-[rgba(255,0,0,0.1)]', // For debugging
'fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]',
vertical && 'cursor-row-resize',
!vertical && 'cursor-col-resize',
"fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]",
vertical && "cursor-row-resize",
!vertical && "cursor-col-resize",
)}
/>
)}

View File

@@ -1,11 +1,11 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useMemo } from 'react';
import type { JSX } from 'react/jsx-runtime';
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
import { CountBadge } from './core/CountBadge';
import { DetailsBanner } from './core/DetailsBanner';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import type { HttpResponse } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useMemo } from "react";
import type { JSX } from "react/jsx-runtime";
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props {
response: HttpResponse;
@@ -26,37 +26,37 @@ interface ParsedCookie {
function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> {
// Parse "Cookie: name=value; name2=value2" format
return cookieHeader.split(';').map((pair) => {
const [name = '', ...valueParts] = pair.split('=');
return cookieHeader.split(";").map((pair) => {
const [name = "", ...valueParts] = pair.split("=");
return {
name: name.trim(),
value: valueParts.join('=').trim(),
value: valueParts.join("=").trim(),
};
});
}
function parseSetCookieHeader(setCookieHeader: string): ParsedCookie {
// Parse "Set-Cookie: name=value; Domain=...; Path=..." format
const parts = setCookieHeader.split(';').map((p) => p.trim());
const [nameValue = '', ...attributes] = parts;
const [name = '', ...valueParts] = nameValue.split('=');
const parts = setCookieHeader.split(";").map((p) => p.trim());
const [nameValue = "", ...attributes] = parts;
const [name = "", ...valueParts] = nameValue.split("=");
const cookie: ParsedCookie = {
name: name.trim(),
value: valueParts.join('=').trim(),
value: valueParts.join("=").trim(),
};
for (const attr of attributes) {
const [key = '', val] = attr.split('=').map((s) => s.trim());
const [key = "", val] = attr.split("=").map((s) => s.trim());
const lowerKey = key.toLowerCase();
if (lowerKey === 'domain') cookie.domain = val;
else if (lowerKey === 'path') cookie.path = val;
else if (lowerKey === 'expires') cookie.expires = val;
else if (lowerKey === 'max-age') cookie.maxAge = val;
else if (lowerKey === 'secure') cookie.secure = true;
else if (lowerKey === 'httponly') cookie.httpOnly = true;
else if (lowerKey === 'samesite') cookie.sameSite = val;
if (lowerKey === "domain") cookie.domain = val;
else if (lowerKey === "path") cookie.path = val;
else if (lowerKey === "expires") cookie.expires = val;
else if (lowerKey === "max-age") cookie.maxAge = val;
else if (lowerKey === "secure") cookie.secure = true;
else if (lowerKey === "httponly") cookie.httpOnly = true;
else if (lowerKey === "samesite") cookie.sameSite = val;
}
// Detect if cookie is being deleted
@@ -94,7 +94,7 @@ export function ResponseCookies({ response }: Props) {
const e = event.event;
// Cookie headers sent (header_up with name=cookie)
if (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') {
if (e.type === "header_up" && e.name.toLowerCase() === "cookie") {
const cookies = parseCookieHeader(e.value);
for (const cookie of cookies) {
sentMap.set(cookie.name, cookie);
@@ -102,7 +102,7 @@ export function ResponseCookies({ response }: Props) {
}
// Set-Cookie headers received (header_down with name=set-cookie)
if (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') {
if (e.type === "header_down" && e.name.toLowerCase() === "set-cookie") {
const cookie = parseSetCookieHeader(e.value);
receivedMap.set(cookie.name, cookie);
}
@@ -158,8 +158,8 @@ export function ResponseCookies({ response }: Props) {
<div className="flex items-center gap-2 my-1">
<span
className={classNames(
'font-mono text-editor select-auto cursor-auto',
cookie.isDeleted ? 'line-through opacity-60 text-text-subtle' : 'text-text',
"font-mono text-editor select-auto cursor-auto",
cookie.isDeleted ? "line-through opacity-60 text-text-subtle" : "text-text",
)}
>
{cookie.name}

View File

@@ -1,10 +1,10 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import type { HttpResponse } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { CountBadge } from './core/CountBadge';
import { DetailsBanner } from './core/DetailsBanner';
import { IconButton } from './core/IconButton';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from "@yaakapp-internal/models";
import { useMemo } from "react";
import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner";
import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props {
response: HttpResponse;

View File

@@ -1,7 +1,7 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import type { HttpResponse } from '@yaakapp-internal/models';
import { IconButton } from './core/IconButton';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from "@yaakapp-internal/models";
import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props {
response: HttpResponse;

View File

@@ -1,16 +1,16 @@
import { Button } from './core/Button';
import { DetailsBanner } from './core/DetailsBanner';
import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading';
import { VStack } from './core/Stacks';
import { Button } from "./core/Button";
import { DetailsBanner } from "./core/DetailsBanner";
import { FormattedError } from "./core/FormattedError";
import { Heading } from "./core/Heading";
import { VStack } from "./core/Stacks";
export default function RouteError({ error }: { error: unknown }) {
console.log('Error', error);
console.log("Error", error);
const stringified = JSON.stringify(error);
// oxlint-disable-next-line no-explicit-any
const message = (error as any).message ?? stringified;
const stack =
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null;
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
return (
<div className="flex items-center justify-center h-full">
<VStack space={5} className="w-[50rem] !h-auto">
@@ -31,7 +31,7 @@ export default function RouteError({ error }: { error: unknown }) {
<Button
color="primary"
onClick={async () => {
window.location.assign('/');
window.location.assign("/");
}}
>
Go Home

View File

@@ -1,17 +1,17 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { open } from '@tauri-apps/plugin-dialog';
import classNames from 'classnames';
import mime from 'mime';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import { IconButton } from './core/IconButton';
import { IconTooltip } from './core/IconTooltip';
import { Label } from './core/Label';
import { HStack } from './core/Stacks';
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { open } from "@tauri-apps/plugin-dialog";
import classNames from "classnames";
import mime from "mime";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import type { ButtonProps } from "./core/Button";
import { Button } from "./core/Button";
import { IconButton } from "./core/IconButton";
import { IconTooltip } from "./core/IconTooltip";
import { Label } from "./core/Label";
import { HStack } from "./core/Stacks";
type Props = Omit<ButtonProps, 'type'> & {
type Props = Omit<ButtonProps, "type"> & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null;
nameOverride?: string | null;
@@ -33,14 +33,14 @@ export function SelectFile({
directory,
noun,
nameOverride,
size = 'sm',
size = "sm",
label,
help,
...props
}: Props) {
const handleClick = async () => {
const filePath = await open({
title: directory ? 'Select Folder' : 'Select File',
title: directory ? "Select Folder" : "Select File",
multiple: false,
directory,
});
@@ -53,8 +53,8 @@ export function SelectFile({
onChange({ filePath: null, contentType: null });
};
const itemLabel = noun ?? (directory ? 'Folder' : 'File');
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
const itemLabel = noun ?? (directory ? "Folder" : "File");
const selectOrChange = (filePath ? "Change " : "Select ") + itemLabel;
const [isHovering, setIsHovering] = useState(false);
const ref = useRef<HTMLDivElement>(null);
@@ -66,20 +66,20 @@ export function SelectFile({
const setup = async () => {
const webview = getCurrentWebviewWindow();
unlisten = await webview.onDragDropEvent((event) => {
if (event.payload.type === 'over') {
if (event.payload.type === "over") {
const p = event.payload.position;
const r = ref.current?.getBoundingClientRect();
if (r == null) return;
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
console.log('IS OVER', isOver);
console.log("IS OVER", isOver);
setIsHovering(isOver);
} else if (event.payload.type === 'drop' && isHovering) {
console.log('User dropped', event.payload.paths);
} else if (event.payload.type === "drop" && isHovering) {
console.log("User dropped", event.payload.paths);
const p = event.payload.paths[0];
if (p) onChange({ filePath: p, contentType: null });
setIsHovering(false);
} else {
console.log('File drop cancelled');
console.log("File drop cancelled");
setIsHovering(false);
}
});
@@ -103,12 +103,12 @@ export function SelectFile({
<Button
className={classNames(
className,
'rtl mr-1.5',
inline && 'w-full',
filePath && inline && 'font-mono text-xs',
isHovering && '!border-notice',
"rtl mr-1.5",
inline && "w-full",
filePath && inline && "font-mono text-xs",
isHovering && "!border-notice",
)}
color={isHovering ? 'primary' : 'secondary'}
color={isHovering ? "primary" : "secondary"}
onClick={handleClick}
size={size}
{...props}
@@ -121,7 +121,7 @@ export function SelectFile({
<>
{filePath && (
<IconButton
size={size === 'auto' ? 'md' : size}
size={size === "auto" ? "md" : size}
variant="border"
icon="x"
title={`Unset ${itemLabel}`}
@@ -130,10 +130,10 @@ export function SelectFile({
)}
<div
className={classNames(
'truncate rtl pl-1.5 pr-3 text-text',
filePath && 'font-mono',
size === 'xs' && filePath && 'text-xs',
size === 'sm' && filePath && 'text-sm',
"truncate rtl pl-1.5 pr-3 text-text",
filePath && "font-mono",
size === "xs" && filePath && "text-xs",
size === "sm" && filePath && "text-sm",
)}
>
{rtlEscapeChar}

View File

@@ -1,39 +1,39 @@
import { useSearch } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { useLicense } from '@yaakapp-internal/license';
import { pluginsAtom, settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useKeyPressEvent } from 'react-use';
import { appInfo } from '../../lib/appInfo';
import { capitalize } from '../../lib/capitalize';
import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon';
import { HStack } from '../core/Stacks';
import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { SettingsCertificates } from './SettingsCertificates';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsHotkeys } from './SettingsHotkeys';
import { SettingsInterface } from './SettingsInterface';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';
import { SettingsTheme } from './SettingsTheme';
import { useSearch } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { type } from "@tauri-apps/plugin-os";
import { useLicense } from "@yaakapp-internal/license";
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { useKeyPressEvent } from "react-use";
import { appInfo } from "../../lib/appInfo";
import { capitalize } from "../../lib/capitalize";
import { CountBadge } from "../core/CountBadge";
import { Icon } from "../core/Icon";
import { HStack } from "../core/Stacks";
import { TabContent, type TabItem, Tabs } from "../core/Tabs/Tabs";
import { HeaderSize } from "../HeaderSize";
import { SettingsCertificates } from "./SettingsCertificates";
import { SettingsGeneral } from "./SettingsGeneral";
import { SettingsHotkeys } from "./SettingsHotkeys";
import { SettingsInterface } from "./SettingsInterface";
import { SettingsLicense } from "./SettingsLicense";
import { SettingsPlugins } from "./SettingsPlugins";
import { SettingsProxy } from "./SettingsProxy";
import { SettingsTheme } from "./SettingsTheme";
interface Props {
hide?: () => void;
}
const TAB_GENERAL = 'general';
const TAB_INTERFACE = 'interface';
const TAB_THEME = 'theme';
const TAB_SHORTCUTS = 'shortcuts';
const TAB_PROXY = 'proxy';
const TAB_CERTIFICATES = 'certificates';
const TAB_PLUGINS = 'plugins';
const TAB_LICENSE = 'license';
const TAB_GENERAL = "general";
const TAB_INTERFACE = "interface";
const TAB_THEME = "theme";
const TAB_SHORTCUTS = "shortcuts";
const TAB_PROXY = "proxy";
const TAB_CERTIFICATES = "certificates";
const TAB_PLUGINS = "plugins";
const TAB_LICENSE = "license";
const tabs = [
TAB_GENERAL,
TAB_THEME,
@@ -47,16 +47,16 @@ const tabs = [
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const { tab: tabFromQuery } = useSearch({ from: "/workspaces/$workspaceId/settings" });
// Parse tab and subtab (e.g., "plugins:installed")
const [mainTab, subtab] = tabFromQuery?.split(':') ?? [];
const [mainTab, subtab] = tabFromQuery?.split(":") ?? [];
const settings = useAtomValue(settingsAtom);
const plugins = useAtomValue(pluginsAtom);
const licenseCheck = useLicense();
// Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
useKeyPressEvent('Escape', async () => {
useKeyPressEvent("Escape", async () => {
if (hide != null) {
// It's being shown in a dialog, so close the dialog
hide();
@@ -67,7 +67,7 @@ export default function Settings({ hide }: Props) {
});
return (
<div className={classNames('grid grid-rows-[auto_minmax(0,1fr)] h-full')}>
<div className={classNames("grid grid-rows-[auto_minmax(0,1fr)] h-full")}>
{hide ? (
<span />
) : (
@@ -83,7 +83,7 @@ export default function Settings({ hide }: Props) {
justifyContent="center"
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
>
<div className={classNames(type() === 'macos' ? 'text-center' : 'pl-2')}>Settings</div>
<div className={classNames(type() === "macos" ? "text-center" : "pl-2")}>Settings</div>
</HStack>
</HeaderSize>
)}
@@ -120,10 +120,10 @@ export default function Settings({ hide }: Props) {
value === TAB_CERTIFICATES ? (
<CountBadge count={settings.clientCertificates.length} />
) : value === TAB_PLUGINS ? (
<CountBadge count={plugins.filter((p) => p.source !== 'bundled').length} />
) : value === TAB_PROXY && settings.proxy?.type === 'enabled' ? (
<CountBadge count={plugins.filter((p) => p.source !== "bundled").length} />
) : value === TAB_PROXY && settings.proxy?.type === "enabled" ? (
<CountBadge count />
) : value === TAB_LICENSE && licenseCheck.check.data?.status === 'personal_use' ? (
) : value === TAB_LICENSE && licenseCheck.check.data?.status === "personal_use" ? (
<CountBadge count color="notice" />
) : null,
}),

View File

@@ -1,22 +1,22 @@
import type { ClientCertificate } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useRef } from 'react';
import { showConfirmDelete } from '../../lib/confirm';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { DetailsBanner } from '../core/DetailsBanner';
import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { PlainInput } from '../core/PlainInput';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
import { SelectFile } from '../SelectFile';
import type { ClientCertificate } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useRef } from "react";
import { showConfirmDelete } from "../../lib/confirm";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
import { DetailsBanner } from "../core/DetailsBanner";
import { Heading } from "../core/Heading";
import { IconButton } from "../core/IconButton";
import { InlineCode } from "../core/InlineCode";
import { PlainInput } from "../core/PlainInput";
import { Separator } from "../core/Separator";
import { HStack, VStack } from "../core/Stacks";
import { SelectFile } from "../SelectFile";
function createEmptyCertificate(): ClientCertificate {
return {
host: '',
host: "",
port: null,
crtFile: null,
keyFile: null,
@@ -44,11 +44,11 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
const hasPfx = Boolean(certificate.pfxFile && certificate.pfxFile.length > 0);
const hasCrtKey = Boolean(
(certificate.crtFile && certificate.crtFile.length > 0) ||
(certificate.keyFile && certificate.keyFile.length > 0),
(certificate.keyFile && certificate.keyFile.length > 0),
);
// Determine certificate type for display
const certType = hasPfx ? 'PFX' : hasCrtKey ? 'CERT' : null;
const certType = hasPfx ? "PFX" : hasCrtKey ? "CERT" : null;
const defaultOpen = useRef<boolean>(!certificate.host);
return (
@@ -60,9 +60,9 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
<Checkbox
className="ml-1"
checked={certificate.enabled ?? true}
title={certificate.enabled ? 'Disable certificate' : 'Enable certificate'}
title={certificate.enabled ? "Disable certificate" : "Enable certificate"}
hideLabel
onChange={(enabled) => updateField('enabled', enabled)}
onChange={(enabled) => updateField("enabled", enabled)}
/>
{certificate.host ? (
@@ -103,7 +103,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
size="sm"
required
defaultValue={certificate.host}
onChange={(host) => updateField('host', host)}
onChange={(host) => updateField("host", host)}
/>
<PlainInput
label="Port"
@@ -121,8 +121,8 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
}
size="sm"
className="w-24"
defaultValue={certificate.port?.toString() ?? ''}
onChange={(port) => updateField('port', port ? parseInt(port, 10) : null)}
defaultValue={certificate.port?.toString() ?? ""}
onChange={(port) => updateField("port", port ? parseInt(port, 10) : null)}
/>
</HStack>
@@ -135,7 +135,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.crtFile ?? null}
size="sm"
disabled={hasPfx}
onChange={({ filePath }) => updateField('crtFile', filePath)}
onChange={({ filePath }) => updateField("crtFile", filePath)}
/>
<SelectFile
label="KEY File"
@@ -143,7 +143,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.keyFile ?? null}
size="sm"
disabled={hasPfx}
onChange={({ filePath }) => updateField('keyFile', filePath)}
onChange={({ filePath }) => updateField("keyFile", filePath)}
/>
</VStack>
@@ -155,15 +155,15 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.pfxFile ?? null}
size="sm"
disabled={hasCrtKey}
onChange={({ filePath }) => updateField('pfxFile', filePath)}
onChange={({ filePath }) => updateField("pfxFile", filePath)}
/>
<PlainInput
label="Passphrase"
size="sm"
type="password"
defaultValue={certificate.passphrase ?? ''}
onChange={(passphrase) => updateField('passphrase', passphrase || null)}
defaultValue={certificate.passphrase ?? ""}
onChange={(passphrase) => updateField("passphrase", passphrase || null)}
/>
</VStack>
</DetailsBanner>
@@ -193,15 +193,15 @@ export function SettingsCertificates() {
const cert = certificates[index];
if (cert == null) return;
const host = cert.host || 'this certificate';
const port = cert.port != null ? `:${cert.port}` : '';
const host = cert.host || "this certificate";
const port = cert.port != null ? `:${cert.port}` : "";
const confirmed = await showConfirmDelete({
id: 'confirm-remove-certificate',
title: 'Delete Certificate',
id: "confirm-remove-certificate",
title: "Delete Certificate",
description: (
<>
Permanently delete certificate for{' '}
Permanently delete certificate for{" "}
<InlineCode>
{host}
{port}

View File

@@ -1,19 +1,19 @@
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { appInfo } from '../../lib/appInfo';
import { revealInFinderText } from '../../lib/reveal';
import { CargoFeature } from '../CargoFeature';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton';
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
import { PlainInput } from '../core/PlainInput';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { VStack } from '../core/Stacks';
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
import { appInfo } from "../../lib/appInfo";
import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from "../CargoFeature";
import { Checkbox } from "../core/Checkbox";
import { Heading } from "../core/Heading";
import { IconButton } from "../core/IconButton";
import { KeyValueRow, KeyValueRows } from "../core/KeyValueRow";
import { PlainInput } from "../core/PlainInput";
import { Select } from "../core/Select";
import { Separator } from "../core/Separator";
import { VStack } from "../core/Stacks";
export function SettingsGeneral() {
const workspace = useAtomValue(activeWorkspaceAtom);
@@ -41,8 +41,8 @@ export function SettingsGeneral() {
value={settings.updateChannel}
onChange={(updateChannel) => patchModel(settings, { updateChannel })}
options={[
{ label: 'Stable', value: 'stable' },
{ label: 'Beta (more frequent)', value: 'beta' },
{ label: "Stable", value: "stable" },
{ label: "Beta (more frequent)", value: "beta" },
]}
/>
<IconButton
@@ -57,15 +57,15 @@ export function SettingsGeneral() {
<Select
name="autoupdate"
value={settings.autoupdate ? 'auto' : 'manual'}
value={settings.autoupdate ? "auto" : "manual"}
label="Update Behavior"
labelPosition="left"
size="sm"
labelClassName="w-[14rem]"
onChange={(v) => patchModel(settings, { autoupdate: v === 'auto' })}
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
options={[
{ label: 'Automatic', value: 'auto' },
{ label: 'Manual', value: 'manual' },
{ label: "Automatic", value: "auto" },
{ label: "Manual", value: "manual" },
]}
/>
<Checkbox
@@ -97,7 +97,7 @@ export function SettingsGeneral() {
<Separator className="my-4" />
<Heading level={2}>
Workspace{' '}
Workspace{" "}
<div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink">
{workspace.name}
</div>

View File

@@ -1,8 +1,8 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { fuzzyMatch } from 'fuzzbunny';
import { useAtomValue } from 'jotai';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import classNames from "classnames";
import { fuzzyMatch } from "fuzzbunny";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
defaultHotkeys,
formatHotkeyString,
@@ -11,27 +11,27 @@ import {
hotkeyActions,
hotkeysAtom,
useHotkeyLabel,
} from '../../hooks/useHotKey';
import { capitalize } from '../../lib/capitalize';
import { showDialog } from '../../lib/dialog';
import { Button } from '../core/Button';
import { Dropdown, type DropdownItem } from '../core/Dropdown';
import { Heading } from '../core/Heading';
import { HotkeyRaw } from '../core/Hotkey';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import { PlainInput } from '../core/PlainInput';
import { HStack, VStack } from '../core/Stacks';
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table';
} from "../../hooks/useHotKey";
import { capitalize } from "../../lib/capitalize";
import { showDialog } from "../../lib/dialog";
import { Button } from "../core/Button";
import { Dropdown, type DropdownItem } from "../core/Dropdown";
import { Heading } from "../core/Heading";
import { HotkeyRaw } from "../core/Hotkey";
import { Icon } from "../core/Icon";
import { IconButton } from "../core/IconButton";
import { PlainInput } from "../core/PlainInput";
import { HStack, VStack } from "../core/Stacks";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "../core/Table";
const HOLD_KEYS = ['Shift', 'Control', 'Alt', 'Meta'];
const HOLD_KEYS = ["Shift", "Control", "Alt", "Meta"];
const LAYOUT_INSENSITIVE_KEYS = [
'Equal',
'Minus',
'BracketLeft',
'BracketRight',
'Backquote',
'Space',
"Equal",
"Minus",
"BracketLeft",
"BracketRight",
"Backquote",
"Space",
];
/** Convert a KeyboardEvent to a hotkey string like "Meta+Shift+k" or "Control+Shift+k" */
@@ -45,37 +45,37 @@ function eventToHotkeyString(e: KeyboardEvent): string | null {
// Add modifiers in consistent order (Meta, Control, Alt, Shift)
if (e.metaKey) {
parts.push('Meta');
parts.push("Meta");
}
if (e.ctrlKey) {
parts.push('Control');
parts.push("Control");
}
if (e.altKey) {
parts.push('Alt');
parts.push("Alt");
}
if (e.shiftKey) {
parts.push('Shift');
parts.push("Shift");
}
// Get the main key - use the same logic as useHotKey.ts
const key = LAYOUT_INSENSITIVE_KEYS.includes(e.code) ? e.code : e.key;
parts.push(key);
return parts.join('+');
return parts.join("+");
}
export function SettingsHotkeys() {
const settings = useAtomValue(settingsAtom);
const hotkeys = useAtomValue(hotkeysAtom);
const [filter, setFilter] = useState('');
const [filter, setFilter] = useState("");
const filteredActions = useMemo(() => {
if (!filter.trim()) {
return hotkeyActions;
}
return hotkeyActions.filter((action) => {
const scope = getHotkeyScope(action).replace(/_/g, ' ');
const label = action.replace(/[_.]/g, ' ');
const scope = getHotkeyScope(action).replace(/_/g, " ");
const label = action.replace(/[_.]/g, " ");
const searchText = `${scope} ${label}`;
return fuzzyMatch(searchText, filter) != null;
});
@@ -152,7 +152,7 @@ interface HotkeyRowProps {
function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: HotkeyRowProps) {
const label = useHotkeyLabel(action);
const scope = capitalize(getHotkeyScope(action).replace(/_/g, ' '));
const scope = capitalize(getHotkeyScope(action).replace(/_/g, " "));
const isCustomized = !arraysEqual(currentKeys, defaultKeys);
const isDisabled = currentKeys.length === 0;
@@ -160,7 +160,7 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
showDialog({
id: `record-hotkey-${action}`,
title: label,
size: 'sm',
size: "sm",
render: ({ hide }) => (
<RecordHotkeyDialog
label={label}
@@ -189,7 +189,7 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
// Build dropdown items dynamically
const dropdownItems: DropdownItem[] = [
{
label: 'Add Keyboard Shortcut',
label: "Add Keyboard Shortcut",
leftSlot: <Icon icon="plus" />,
onSelect: handleStartRecording,
},
@@ -213,10 +213,10 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
if (currentKeys.length > 1) {
dropdownItems.push(
{
type: 'separator',
type: "separator",
},
{
label: 'Remove All Shortcuts',
label: "Remove All Shortcuts",
leftSlot: <Icon icon="trash" />,
onSelect: handleClearAll,
},
@@ -226,10 +226,10 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
if (isCustomized) {
dropdownItems.push({
type: 'separator',
type: "separator",
});
dropdownItems.push({
label: 'Reset to Default',
label: "Reset to Default",
leftSlot: <Icon icon="refresh" />,
onSelect: onReset,
});
@@ -292,7 +292,7 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
e.preventDefault();
e.stopPropagation();
if (e.key === 'Escape') {
if (e.key === "Escape") {
onCancel();
return;
}
@@ -303,9 +303,9 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
}
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
window.removeEventListener("keydown", handleKeyDown, { capture: true });
};
}, [isFocused, onCancel]);
@@ -332,9 +332,9 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
e.currentTarget.focus();
}}
className={classNames(
'flex items-center justify-center',
'px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full',
'border-border-subtle focus:border-border-focus',
"flex items-center justify-center",
"px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full",
"border-border-subtle focus:border-border-focus",
)}
>
{recordedKey ? (

View File

@@ -1,35 +1,35 @@
import { type } from '@tauri-apps/plugin-os';
import { useFonts } from '@yaakapp-internal/fonts';
import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { type } from "@tauri-apps/plugin-os";
import { useFonts } from "@yaakapp-internal/fonts";
import { useLicense } from "@yaakapp-internal/license";
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
import { invokeCmd } from '../../lib/tauri';
import { CargoFeature } from '../CargoFeature';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { clamp } from "../../lib/clamp";
import { showConfirm } from "../../lib/confirm";
import { invokeCmd } from "../../lib/tauri";
import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
import { Heading } from "../core/Heading";
import { Icon } from "../core/Icon";
import { Link } from "../core/Link";
import { Select } from "../core/Select";
import { HStack, VStack } from "../core/Stacks";
const NULL_FONT_VALUE = '__NULL_FONT__';
const NULL_FONT_VALUE = "__NULL_FONT__";
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
{ value: "default", label: "Default" },
{ value: "vim", label: "Vim" },
{ value: "vscode", label: "VSCode" },
{ value: "emacs", label: "Emacs" },
];
export function SettingsInterface() {
@@ -54,20 +54,20 @@ export function SettingsInterface() {
help="When opening a workspace, should it open in the current window or a new window?"
value={
settings.openWorkspaceNewWindow === true
? 'new'
? "new"
: settings.openWorkspaceNewWindow === false
? 'current'
: 'ask'
? "current"
: "ask"
}
onChange={async (v) => {
if (v === 'current') await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === 'new') await patchModel(settings, { openWorkspaceNewWindow: true });
if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
else await patchModel(settings, { openWorkspaceNewWindow: null });
}}
options={[
{ label: 'Always ask', value: 'ask' },
{ label: 'Open in current window', value: 'current' },
{ label: 'Open in new window', value: 'new' },
{ label: "Always ask", value: "ask" },
{ label: "Open in current window", value: "current" },
{ label: "Open in new window", value: "new" },
]}
/>
<HStack space={2} alignItems="end">
@@ -78,7 +78,7 @@ export function SettingsInterface() {
label="Interface font"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System default', value: NULL_FONT_VALUE },
{ label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({
label: f,
value: f,
@@ -114,7 +114,7 @@ export function SettingsInterface() {
label="Editor font"
value={settings.editorFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System default', value: NULL_FONT_VALUE },
{ label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
@@ -164,7 +164,7 @@ export function SettingsInterface() {
<NativeTitlebarSetting settings={settings} />
{type() !== 'macos' && (
{type() !== "macos" && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide window controls"
@@ -192,7 +192,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
size="2xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd('cmd_restart');
await invokeCmd("cmd_restart");
}}
>
Apply and Restart
@@ -204,7 +204,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
function LicenseSettings({ settings }: { settings: Settings }) {
const license = useLicense();
if (license.check.data?.status !== 'personal_use') {
if (license.check.data?.status !== "personal_use") {
return null;
}
@@ -215,24 +215,24 @@ function LicenseSettings({ settings }: { settings: Settings }) {
onChange={async (hideLicenseBadge) => {
if (hideLicenseBadge) {
const confirmed = await showConfirm({
id: 'hide-license-badge',
title: 'Confirm Personal Use',
confirmText: 'Confirm',
id: "hide-license-badge",
title: "Confirm Personal Use",
confirmText: "Confirm",
description: (
<VStack space={3}>
<p>Hey there 👋🏼</p>
<p>
Yaak is free for personal projects and learning.{' '}
Yaak is free for personal projects and learning.{" "}
<strong>If youre using Yaak at work, a license is required.</strong>
</p>
<p>
Licenses help keep Yaak independent and sustainable.{' '}
Licenses help keep Yaak independent and sustainable.{" "}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
</p>
</VStack>
),
requireTyping: 'Personal Use',
color: 'info',
requireTyping: "Personal Use",
color: "info",
});
if (!confirmed) {
return; // Cancel

View File

@@ -1,18 +1,18 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { differenceInDays } from 'date-fns';
import { formatDate } from 'date-fns/format';
import { useState } from 'react';
import { useToggle } from '../../hooks/useToggle';
import { pluralizeCount } from '../../lib/pluralize';
import { CargoFeature } from '../CargoFeature';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
import { PlainInput } from '../core/PlainInput';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
import { openUrl } from "@tauri-apps/plugin-opener";
import { useLicense } from "@yaakapp-internal/license";
import { differenceInDays } from "date-fns";
import { formatDate } from "date-fns/format";
import { useState } from "react";
import { useToggle } from "../../hooks/useToggle";
import { pluralizeCount } from "../../lib/pluralize";
import { CargoFeature } from "../CargoFeature";
import { Banner } from "../core/Banner";
import { Button } from "../core/Button";
import { Icon } from "../core/Icon";
import { Link } from "../core/Link";
import { PlainInput } from "../core/PlainInput";
import { Separator } from "../core/Separator";
import { HStack, VStack } from "../core/Stacks";
export function SettingsLicense() {
return (
@@ -24,7 +24,7 @@ export function SettingsLicense() {
function SettingsLicenseCmp() {
const { check, activate, deactivate } = useLicense();
const [key, setKey] = useState<string>('');
const [key, setKey] = useState<string>("");
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
if (check.isPending) {
@@ -35,16 +35,16 @@ function SettingsLicenseCmp() {
if (!check.data) return null;
switch (check.data.status) {
case 'active':
case "active":
return <Banner color="success">Your license is active 🥳</Banner>;
case 'trialing':
case "trialing":
return (
<Banner color="info" className="max-w-lg">
<p className="w-full">
<strong>
{pluralizeCount('day', differenceInDays(check.data.data.end, new Date()))}
</strong>{' '}
{pluralizeCount("day", differenceInDays(check.data.data.end, new Date()))}
</strong>{" "}
left to evaluate Yaak for commercial use.
<br />
<span className="opacity-50">Personal use is always free, forever.</span>
@@ -58,7 +58,7 @@ function SettingsLicenseCmp() {
</Banner>
);
case 'personal_use':
case "personal_use":
return (
<Banner color="notice" className="max-w-lg">
<p className="w-full">
@@ -78,19 +78,19 @@ function SettingsLicenseCmp() {
</Banner>
);
case 'inactive':
case "inactive":
return (
<Banner color="danger">
Your license is invalid. Please <Link href="https://yaak.app/dashboard">Sign In</Link>{' '}
Your license is invalid. Please <Link href="https://yaak.app/dashboard">Sign In</Link>{" "}
for more details
</Banner>
);
case 'expired':
case "expired":
return (
<Banner color="notice">
Your license expired{' '}
<strong>{formatDate(check.data.data.periodEnd, 'MMMM dd, yyyy')}</strong>. Please{' '}
Your license expired{" "}
<strong>{formatDate(check.data.data.periodEnd, "MMMM dd, yyyy")}</strong>. Please{" "}
<Link href="https://yaak.app/dashboard">Resubscribe</Link> to continue receiving
updates.
{check.data.data.changesUrl && (
@@ -102,17 +102,17 @@ function SettingsLicenseCmp() {
</Banner>
);
case 'past_due':
case "past_due":
return (
<Banner color="danger">
<strong>Your payment method needs attention.</strong>
<br />
To re-activate your license, please{' '}
To re-activate your license, please{" "}
<Link href={check.data.data.billingUrl}>update your billing info</Link>.
</Banner>
);
case 'error':
case "error":
return (
<Banner color="danger">
License check failed: {check.data.data.message} (Code: {check.data.data.code})
@@ -128,7 +128,7 @@ function SettingsLicenseCmp() {
{check.error && <Banner color="danger">{check.error}</Banner>}
{activate.error && <Banner color="danger">{activate.error}</Banner>}
{check.data?.status === 'active' ? (
{check.data?.status === "active" ? (
<HStack space={2}>
<Button variant="border" color="secondary" size="sm" onClick={() => deactivate.mutate()}>
Deactivate License
@@ -136,7 +136,7 @@ function SettingsLicenseCmp() {
<Button
color="secondary"
size="sm"
onClick={() => openUrl('https://yaak.app/dashboard?s=support&ref=app.yaak.desktop')}
onClick={() => openUrl("https://yaak.app/dashboard?s=support&ref=app.yaak.desktop")}
rightSlot={<Icon icon="external_link" />}
>
Direct Support
@@ -153,7 +153,7 @@ function SettingsLicenseCmp() {
rightSlot={<Icon icon="external_link" />}
onClick={() =>
openUrl(
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ''}`,
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
)
}
>

View File

@@ -1,37 +1,37 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models';
import { patchModel, pluginsAtom } from '@yaakapp-internal/models';
import type { PluginVersion } from '@yaakapp-internal/plugins';
import { useMutation, useQuery } from "@tanstack/react-query";
import { openUrl } from "@tauri-apps/plugin-opener";
import type { Plugin } from "@yaakapp-internal/models";
import { patchModel, pluginsAtom } from "@yaakapp-internal/models";
import type { PluginVersion } from "@yaakapp-internal/plugins";
import {
checkPluginUpdates,
installPlugin,
searchPlugins,
uninstallPlugin,
} from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
import { showConfirmDelete } from '../../lib/confirm';
import { minPromiseMillis } from '../../lib/minPromiseMillis';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { Link } from '../core/Link';
import { LoadingIcon } from '../core/LoadingIcon';
import { PlainInput } from '../core/PlainInput';
import { HStack } from '../core/Stacks';
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { EmptyStateText } from '../EmptyStateText';
import { SelectFile } from '../SelectFile';
} from "@yaakapp-internal/plugins";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
import { useInstallPlugin } from "../../hooks/useInstallPlugin";
import { usePluginInfo } from "../../hooks/usePluginInfo";
import { usePluginsKey, useRefreshPlugins } from "../../hooks/usePlugins";
import { showConfirmDelete } from "../../lib/confirm";
import { minPromiseMillis } from "../../lib/minPromiseMillis";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
import { CountBadge } from "../core/CountBadge";
import { Icon } from "../core/Icon";
import { IconButton } from "../core/IconButton";
import { InlineCode } from "../core/InlineCode";
import { Link } from "../core/Link";
import { LoadingIcon } from "../core/LoadingIcon";
import { PlainInput } from "../core/PlainInput";
import { HStack } from "../core/Stacks";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "../core/Table";
import { TabContent, Tabs } from "../core/Tabs/Tabs";
import { EmptyStateText } from "../EmptyStateText";
import { SelectFile } from "../SelectFile";
interface SettingsPluginsProps {
defaultSubtab?: string;
@@ -40,8 +40,8 @@ interface SettingsPluginsProps {
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => p.source === 'bundled');
const installedPlugins = plugins.filter((p) => p.source !== 'bundled');
const bundledPlugins = plugins.filter((p) => p.source === "bundled");
const installedPlugins = plugins.filter((p) => p.source !== "bundled");
const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins();
return (
@@ -52,15 +52,15 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
addBorders
tabListClassName="px-6 pt-2"
tabs={[
{ label: 'Discover', value: 'search' },
{ label: "Discover", value: "search" },
{
label: 'Installed',
value: 'installed',
label: "Installed",
value: "installed",
rightSlot: <CountBadge count={installedPlugins.length} />,
},
{
label: 'Bundled',
value: 'bundled',
label: "Bundled",
value: "bundled",
rightSlot: <CountBadge count={bundledPlugins.length} />,
},
]}
@@ -106,7 +106,7 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
icon="help"
title="View documentation"
onClick={() =>
openUrl('https://yaak.app/docs/plugin-development/plugins-quick-start')
openUrl("https://yaak.app/docs/plugin-development/plugins-quick-start")
}
/>
</HStack>
@@ -195,7 +195,7 @@ function PluginTableRow({
const updates = usePluginUpdates();
const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version;
const installPluginMutation = useMutation({
mutationKey: ['install_plugin', name],
mutationKey: ["install_plugin", name],
mutationFn: (name: string) => installPlugin(name, null),
});
const uninstall = usePromptUninstall(plugin?.id ?? null, displayName);
@@ -207,7 +207,7 @@ function PluginTableRow({
<TableCell className="!py-0">
<Checkbox
hideLabel
title={plugin?.enabled ? 'Disable plugin' : 'Enable plugin'}
title={plugin?.enabled ? "Disable plugin" : "Enable plugin"}
checked={plugin?.enabled ?? false}
disabled={plugin == null}
onChange={async (enabled) => {
@@ -285,10 +285,10 @@ function PluginTableRow({
}
function PluginSearch() {
const [query, setQuery] = useState<string>('');
const [query, setQuery] = useState<string>("");
const debouncedQuery = useDebouncedValue(query);
const results = useQuery({
queryKey: ['plugins', debouncedQuery],
queryKey: ["plugins", debouncedQuery],
queryFn: () => searchPlugins(query),
});
@@ -334,7 +334,7 @@ function PluginSearch() {
function InstalledPlugins({ plugins, className }: { plugins: Plugin[]; className?: string }) {
return plugins.length === 0 ? (
<div className={classNames(className, 'pb-4')}>
<div className={classNames(className, "pb-4")}>
<EmptyStateText className="text-center">
Plugins extend the functionality of Yaak.
<br />
@@ -388,14 +388,14 @@ function BundledPlugins({ plugins }: { plugins: Plugin[] }) {
function usePromptUninstall(pluginId: string | null, name: string) {
const mut = useMutation({
mutationKey: ['uninstall_plugin', pluginId],
mutationKey: ["uninstall_plugin", pluginId],
mutationFn: async () => {
if (pluginId == null) return;
const confirmed = await showConfirmDelete({
id: `uninstall-plugin-${pluginId}`,
title: 'Uninstall Plugin',
confirmText: 'Uninstall',
title: "Uninstall Plugin",
confirmText: "Uninstall",
description: (
<>
Permanently uninstall <InlineCode>{name}</InlineCode>?
@@ -413,7 +413,7 @@ function usePromptUninstall(pluginId: string | null, name: string) {
function usePluginUpdates() {
return useQuery({
queryKey: ['plugin_updates', usePluginsKey()],
queryKey: ["plugin_updates", usePluginsKey()],
queryFn: () => checkPluginUpdates(),
});
}

View File

@@ -1,13 +1,13 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { InlineCode } from '../core/InlineCode';
import { PlainInput } from '../core/PlainInput';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
import { Checkbox } from "../core/Checkbox";
import { Heading } from "../core/Heading";
import { InlineCode } from "../core/InlineCode";
import { PlainInput } from "../core/PlainInput";
import { Select } from "../core/Select";
import { Separator } from "../core/Separator";
import { HStack, VStack } from "../core/Stacks";
export function SettingsProxy() {
const settings = useAtomValue(settingsAtom);
@@ -26,32 +26,32 @@ export function SettingsProxy() {
label="Proxy"
hideLabel
size="sm"
value={settings.proxy?.type ?? 'automatic'}
value={settings.proxy?.type ?? "automatic"}
onChange={async (v) => {
if (v === 'automatic') {
if (v === "automatic") {
await patchModel(settings, { proxy: undefined });
} else if (v === 'enabled') {
} else if (v === "enabled") {
await patchModel(settings, {
proxy: {
disabled: false,
type: 'enabled',
http: '',
https: '',
auth: { user: '', password: '' },
bypass: '',
type: "enabled",
http: "",
https: "",
auth: { user: "", password: "" },
bypass: "",
},
});
} else {
await patchModel(settings, { proxy: { type: 'disabled' } });
await patchModel(settings, { proxy: { type: "disabled" } });
}
}}
options={[
{ label: 'Automatic proxy detection', value: 'automatic' },
{ label: 'Custom proxy configuration', value: 'enabled' },
{ label: 'No proxy', value: 'disabled' },
{ label: "Automatic proxy detection", value: "automatic" },
{ label: "Custom proxy configuration", value: "enabled" },
{ label: "No proxy", value: "disabled" },
]}
/>
{settings.proxy?.type === 'enabled' && (
{settings.proxy?.type === "enabled" && (
<VStack space={1.5}>
<Checkbox
className="my-3"
@@ -60,13 +60,13 @@ export function SettingsProxy() {
help="Use this to temporarily disable the proxy without losing the configuration"
onChange={async (enabled) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = !enabled;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
@@ -82,13 +82,13 @@ export function SettingsProxy() {
defaultValue={settings.proxy?.http}
onChange={async (http) => {
const { proxy } = settings;
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const https = proxy?.type === "enabled" ? proxy.https : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
await patchModel(settings, {
proxy: {
type: 'enabled',
type: "enabled",
http,
https,
auth,
@@ -109,12 +109,12 @@ export function SettingsProxy() {
defaultValue={settings.proxy?.https}
onChange={async (https) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const http = proxy?.type === "enabled" ? proxy.http : "";
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = proxy?.type === "enabled" ? proxy.auth : null;
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
@@ -125,13 +125,13 @@ export function SettingsProxy() {
title="Enable authentication"
onChange={async (enabled) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = enabled ? { user: '', password: '' } : null;
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const auth = enabled ? { user: "", password: "" } : null;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
@@ -146,14 +146,14 @@ export function SettingsProxy() {
defaultValue={settings.proxy.auth.user}
onChange={async (user) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
@@ -165,20 +165,20 @@ export function SettingsProxy() {
defaultValue={settings.proxy.auth.password}
onChange={async (password) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const bypass = proxy?.type === "enabled" ? proxy.bypass : "";
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>
</HStack>
)}
{settings.proxy.type === 'enabled' && (
{settings.proxy.type === "enabled" && (
<>
<Separator className="my-6" />
<PlainInput
@@ -188,14 +188,14 @@ export function SettingsProxy() {
placeholder="127.0.0.1, *.example.com, localhost:3000"
onChange={async (bypass) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const http = proxy?.type === "enabled" ? proxy.http : "";
const https = proxy?.type === "enabled" ? proxy.https : "";
const disabled = proxy?.type === "enabled" ? proxy.disabled : false;
const user = proxy?.type === "enabled" ? (proxy.auth?.user ?? "") : "";
const password = proxy?.type === "enabled" ? (proxy.auth?.password ?? "") : "";
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
proxy: { type: "enabled", http, https, auth, disabled, bypass },
});
}}
/>

View File

@@ -1,48 +1,48 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { lazy, Suspense } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import type { ButtonProps } from '../core/Button';
import { Heading } from '../core/Heading';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import { Link } from '../core/Link';
import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { lazy, Suspense } from "react";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { useResolvedAppearance } from "../../hooks/useResolvedAppearance";
import { useResolvedTheme } from "../../hooks/useResolvedTheme";
import type { ButtonProps } from "../core/Button";
import { Heading } from "../core/Heading";
import type { IconProps } from "../core/Icon";
import { Icon } from "../core/Icon";
import { IconButton } from "../core/IconButton";
import { Link } from "../core/Link";
import type { SelectProps } from "../core/Select";
import { Select } from "../core/Select";
import { HStack, VStack } from "../core/Stacks";
const Editor = lazy(() => import('../core/Editor/Editor').then((m) => ({ default: m.Editor })));
const Editor = lazy(() => import("../core/Editor/Editor").then((m) => ({ default: m.Editor })));
const buttonColors: ButtonProps['color'][] = [
'primary',
'info',
'success',
'notice',
'warning',
'danger',
'secondary',
'default',
const buttonColors: ButtonProps["color"][] = [
"primary",
"info",
"success",
"notice",
"warning",
"danger",
"secondary",
"default",
];
const icons: IconProps['icon'][] = [
'info',
'box',
'update',
'alert_triangle',
'arrow_big_right_dash',
'download',
'copy',
'magic_wand',
'settings',
'trash',
'sparkles',
'pencil',
'paste',
'search',
'send_horizontal',
const icons: IconProps["icon"][] = [
"info",
"box",
"update",
"alert_triangle",
"arrow_big_right_dash",
"download",
"copy",
"magic_wand",
"settings",
"trash",
"sparkles",
"pencil",
"paste",
"search",
"send_horizontal",
];
export function SettingsTheme() {
@@ -55,14 +55,14 @@ export function SettingsTheme() {
return null;
}
const lightThemes: SelectProps<string>['options'] = activeTheme.data.themes
const lightThemes: SelectProps<string>["options"] = activeTheme.data.themes
.filter((theme) => !theme.dark)
.map((theme) => ({
label: theme.label,
value: theme.id,
}));
const darkThemes: SelectProps<string>['options'] = activeTheme.data.themes
const darkThemes: SelectProps<string>["options"] = activeTheme.data.themes
.filter((theme) => theme.dark)
.map((theme) => ({
label: theme.label,
@@ -74,7 +74,7 @@ export function SettingsTheme() {
<div className="mb-3">
<Heading>Theme</Heading>
<p className="text-text-subtle">
Make Yaak your own by selecting a theme, or{' '}
Make Yaak your own by selecting a theme, or{" "}
<Link href="https://yaak.app/docs/plugin-development/plugins-quick-start">
Create Your Own
</Link>
@@ -88,13 +88,13 @@ export function SettingsTheme() {
value={settings.appearance}
onChange={(appearance) => patchModel(settings, { appearance })}
options={[
{ label: 'Automatic', value: 'system' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
{ label: "Automatic", value: "system" },
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
]}
/>
<HStack space={2}>
{(settings.appearance === 'system' || settings.appearance === 'light') && (
{(settings.appearance === "system" || settings.appearance === "light") && (
<Select
hideLabel
leftSlot={<Icon icon="sun" color="secondary" />}
@@ -107,7 +107,7 @@ export function SettingsTheme() {
onChange={(themeLight) => patchModel(settings, { themeLight })}
/>
)}
{(settings.appearance === 'system' || settings.appearance === 'dark') && (
{(settings.appearance === "system" || settings.appearance === "dark") && (
<Select
hideLabel
name="darkTheme"
@@ -127,7 +127,7 @@ export function SettingsTheme() {
className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
>
<HStack className="text" space={1.5}>
<Icon icon={appearance === 'dark' ? 'moon' : 'sun'} />
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
<strong>{activeTheme.data.active.label}</strong>
<em>(preview)</em>
</HStack>
@@ -138,7 +138,7 @@ export function SettingsTheme() {
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? 'info'}
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
@@ -150,7 +150,7 @@ export function SettingsTheme() {
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length] ?? 'info'}
icon={icons[i % icons.length] ?? "info"}
iconClassName="text"
title={`${c}`}
/>
@@ -159,11 +159,11 @@ export function SettingsTheme() {
<Suspense>
<Editor
defaultValue={[
'let foo = { // Demo code editor',
"let foo = { // Demo code editor",
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
" baz: [1, 10.2, null, false, true],",
"};",
].join("\n")}
heightMode="auto"
language="javascript"
stateKey={null}

View File

@@ -1,17 +1,17 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { useRef } from 'react';
import { openSettings } from '../commands/openSettings';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useExportData } from '../hooks/useExportData';
import { appInfo } from '../lib/appInfo';
import { showDialog } from '../lib/dialog';
import { importData } from '../lib/importData';
import type { DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { openUrl } from "@tauri-apps/plugin-opener";
import { useLicense } from "@yaakapp-internal/license";
import { useRef } from "react";
import { openSettings } from "../commands/openSettings";
import { useCheckForUpdates } from "../hooks/useCheckForUpdates";
import { useExportData } from "../hooks/useExportData";
import { appInfo } from "../lib/appInfo";
import { showDialog } from "../lib/dialog";
import { importData } from "../lib/importData";
import type { DropdownRef } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
export function SettingsDropdown() {
const exportData = useExportData();
@@ -24,75 +24,75 @@ export function SettingsDropdown() {
ref={dropdownRef}
items={[
{
label: 'Settings',
hotKeyAction: 'settings.show',
label: "Settings",
hotKeyAction: "settings.show",
leftSlot: <Icon icon="settings" />,
onSelect: () => openSettings.mutate(null),
},
{
label: 'Keyboard shortcuts',
hotKeyAction: 'hotkeys.showHelp',
label: "Keyboard shortcuts",
hotKeyAction: "hotkeys.showHelp",
leftSlot: <Icon icon="keyboard" />,
onSelect: () => {
showDialog({
id: 'hotkey',
title: 'Keyboard Shortcuts',
size: 'dynamic',
id: "hotkey",
title: "Keyboard Shortcuts",
size: "dynamic",
render: () => <KeyboardShortcutsDialog />,
});
},
},
{
label: 'Plugins',
label: "Plugins",
leftSlot: <Icon icon="puzzle" />,
onSelect: () => openSettings.mutate('plugins'),
onSelect: () => openSettings.mutate("plugins"),
},
{ type: 'separator', label: 'Share Workspace(s)' },
{ type: "separator", label: "Share Workspace(s)" },
{
label: 'Import Data',
label: "Import Data",
leftSlot: <Icon icon="folder_input" />,
onSelect: () => importData.mutate(),
},
{
label: 'Export Data',
label: "Export Data",
leftSlot: <Icon icon="folder_output" />,
onSelect: () => exportData.mutate(),
},
{
label: 'Create Run Button',
label: "Create Run Button",
leftSlot: <Icon icon="rocket" />,
onSelect: () => openUrl('https://yaak.app/button/new'),
onSelect: () => openUrl("https://yaak.app/button/new"),
},
{ type: 'separator', label: `Yaak v${appInfo.version}` },
{ type: "separator", label: `Yaak v${appInfo.version}` },
{
label: 'Check for Updates',
label: "Check for Updates",
leftSlot: <Icon icon="update" />,
hidden: !appInfo.featureUpdater,
onSelect: () => checkForUpdates.mutate(),
},
{
label: 'Purchase License',
color: 'success',
hidden: check.data == null || check.data.status === 'active',
label: "Purchase License",
color: "success",
hidden: check.data == null || check.data.status === "active",
leftSlot: <Icon icon="circle_dollar_sign" />,
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
onSelect: () => openUrl('https://yaak.app/pricing'),
onSelect: () => openUrl("https://yaak.app/pricing"),
},
{
label: 'Install CLI',
label: "Install CLI",
hidden: appInfo.cliVersion != null,
leftSlot: <Icon icon="square_terminal" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl('https://yaak.app/docs/cli'),
onSelect: () => openUrl("https://yaak.app/docs/cli"),
},
{
label: 'Feedback',
label: "Feedback",
leftSlot: <Icon icon="chat" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl('https://yaak.app/feedback'),
onSelect: () => openUrl("https://yaak.app/feedback"),
},
{
label: 'Changelog',
label: "Changelog",
leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="external_link" color="secondary" />,
onSelect: () => openUrl(`https://yaak.app/changelog/${appInfo.version}`),

View File

@@ -1,6 +1,6 @@
import type { Extension } from '@codemirror/state';
import { Compartment } from '@codemirror/state';
import { debounce } from '@yaakapp-internal/lib';
import type { Extension } from "@codemirror/state";
import { Compartment } from "@codemirror/state";
import { debounce } from "@yaakapp-internal/lib";
import type {
AnyModel,
Folder,
@@ -9,7 +9,7 @@ import type {
ModelPayload,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
} from "@yaakapp-internal/models";
import {
duplicateModel,
foldersAtom,
@@ -20,57 +20,57 @@ import {
patchModel,
websocketConnectionsAtom,
workspacesAtom,
} from '@yaakapp-internal/models';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { moveToWorkspace } from '../commands/moveToWorkspace';
import { openFolderSettings } from '../commands/openFolderSettings';
import { activeFolderIdAtom } from '../hooks/useActiveFolderId';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { getCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { getFolderActions } from '../hooks/useFolderActions';
import { getGrpcRequestActions } from '../hooks/useGrpcRequestActions';
import { useHotKey } from '../hooks/useHotKey';
import { getHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { getModelAncestors } from '../hooks/useModelAncestors';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { getWebsocketRequestActions } from '../hooks/useWebsocketRequestActions';
import { deepEqualAtom } from '../lib/atoms';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { fireAndForget } from '../lib/fireAndForget';
import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { isSidebarFocused } from '../lib/scopes';
import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import type { FieldDef } from './core/Editor/filter/extension';
import { filter } from './core/Editor/filter/extension';
import { evaluate, parseQuery } from './core/Editor/filter/query';
import { HttpMethodTag } from './core/HttpMethodTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import type { InputHandle } from './core/Input';
import { Input } from './core/Input';
import { LoadingIcon } from './core/LoadingIcon';
import { collapsedFamily, isSelectedFamily, selectedIdsFamily } from './core/tree/atoms';
import type { TreeNode } from './core/tree/common';
import type { TreeHandle, TreeProps } from './core/tree/Tree';
import { Tree } from './core/tree/Tree';
import type { TreeItemProps } from './core/tree/TreeItem';
import { GitDropdown } from './git/GitDropdown';
} from "@yaakapp-internal/models";
import classNames from "classnames";
import { atom, useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
import { moveToWorkspace } from "../commands/moveToWorkspace";
import { openFolderSettings } from "../commands/openFolderSettings";
import { activeFolderIdAtom } from "../hooks/useActiveFolderId";
import { activeRequestIdAtom } from "../hooks/useActiveRequestId";
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { allRequestsAtom } from "../hooks/useAllRequests";
import { getCreateDropdownItems } from "../hooks/useCreateDropdownItems";
import { getFolderActions } from "../hooks/useFolderActions";
import { getGrpcRequestActions } from "../hooks/useGrpcRequestActions";
import { useHotKey } from "../hooks/useHotKey";
import { getHttpRequestActions } from "../hooks/useHttpRequestActions";
import { useListenToTauriEvent } from "../hooks/useListenToTauriEvent";
import { getModelAncestors } from "../hooks/useModelAncestors";
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { useSidebarHidden } from "../hooks/useSidebarHidden";
import { getWebsocketRequestActions } from "../hooks/useWebsocketRequestActions";
import { deepEqualAtom } from "../lib/atoms";
import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { fireAndForget } from "../lib/fireAndForget";
import { jotaiStore } from "../lib/jotai";
import { resolvedModelName } from "../lib/resolvedModelName";
import { isSidebarFocused } from "../lib/scopes";
import { navigateToRequestOrFolderOrWorkspace } from "../lib/setWorkspaceSearchParams";
import type { ContextMenuProps, DropdownItem } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import type { FieldDef } from "./core/Editor/filter/extension";
import { filter } from "./core/Editor/filter/extension";
import { evaluate, parseQuery } from "./core/Editor/filter/query";
import { HttpMethodTag } from "./core/HttpMethodTag";
import { HttpStatusTag } from "./core/HttpStatusTag";
import { Icon } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
import type { InputHandle } from "./core/Input";
import { Input } from "./core/Input";
import { LoadingIcon } from "./core/LoadingIcon";
import { collapsedFamily, isSelectedFamily, selectedIdsFamily } from "./core/tree/atoms";
import type { TreeNode } from "./core/tree/common";
import type { TreeHandle, TreeProps } from "./core/tree/Tree";
import { Tree } from "./core/tree/Tree";
import type { TreeItemProps } from "./core/tree/TreeItem";
import { GitDropdown } from "./git/GitDropdown";
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
function isSidebarLeafModel(m: AnyModel): boolean {
const modelMap: Record<Exclude<SidebarModel['model'], 'workspace'>, null> = {
const modelMap: Record<Exclude<SidebarModel["model"], "workspace">, null> = {
http_request: null,
grpc_request: null,
websocket_request: null,
@@ -79,12 +79,12 @@ function isSidebarLeafModel(m: AnyModel): boolean {
return m.model in modelMap;
}
const OPACITY_SUBTLE = 'opacity-80';
const OPACITY_SUBTLE = "opacity-80";
function Sidebar({ className }: { className?: string }) {
const [hidden, setHidden] = useSidebarHidden();
const activeWorkspaceId = useAtomValue(activeWorkspaceAtom)?.id;
const treeId = `tree.${activeWorkspaceId ?? 'unknown'}`;
const treeId = `tree.${activeWorkspaceId ?? "unknown"}`;
const filterText = useAtomValue(sidebarFilterAtom);
const [tree, allFields] = useAtomValue(sidebarTreeAtom) ?? [];
const wrapperRef = useRef<HTMLElement>(null);
@@ -106,9 +106,9 @@ function Sidebar({ className }: { className?: string }) {
}, []);
// Focus any new sidebar models when created
useListenToTauriEvent<ModelPayload>('model_write', ({ payload }) => {
useListenToTauriEvent<ModelPayload>("model_write", ({ payload }) => {
if (!isSidebarLeafModel(payload.model)) return;
if (!(payload.change.type === 'upsert' && payload.change.created)) return;
if (!(payload.change.type === "upsert" && payload.change.created)) return;
treeRef.current?.selectItem(payload.model.id, true);
});
@@ -122,7 +122,7 @@ function Sidebar({ className }: { className?: string }) {
}, []);
useHotKey(
'sidebar.filter',
"sidebar.filter",
() => {
filterRef.current?.focus();
},
@@ -131,7 +131,7 @@ function Sidebar({ className }: { className?: string }) {
},
);
useHotKey('sidebar.focus', async function focusHotkey() {
useHotKey("sidebar.focus", async function focusHotkey() {
// Hide the sidebar if it's already focused
if (!hidden && isSidebarFocused()) {
await setHidden(true);
@@ -160,7 +160,7 @@ function Sidebar({ className }: { className?: string }) {
}) {
const prev = children[insertAt - 1] as Exclude<SidebarModel, Workspace>;
const next = children[insertAt] as Exclude<SidebarModel, Workspace>;
const folderId = parent.model === 'folder' ? parent.id : null;
const folderId = parent.model === "folder" ? parent.id : null;
const beforePriority = prev?.sortPriority ?? 0;
const afterPriority = next?.sortPriority ?? 0;
@@ -205,7 +205,7 @@ function Sidebar({ className }: { className?: string }) {
);
const clearFilterText = useCallback(() => {
jotaiStore.set(sidebarFilterAtom, { text: '', key: `${Math.random()}` });
jotaiStore.set(sidebarFilterAtom, { text: "", key: `${Math.random()}` });
requestAnimationFrame(() => {
filterRef.current?.focus();
});
@@ -214,7 +214,7 @@ function Sidebar({ className }: { className?: string }) {
const handleFilterKeyDown = useCallback(
(e: KeyboardEvent) => {
e.stopPropagation(); // Don't trigger tree navigation hotkeys
if (e.key === 'Escape') {
if (e.key === "Escape") {
e.preventDefault();
clearFilterText();
}
@@ -234,17 +234,17 @@ function Sidebar({ className }: { className?: string }) {
const enable = () => treeRef.current?.hasFocus() ?? false;
const actions = {
'sidebar.context_menu': {
"sidebar.context_menu": {
enable,
cb: () => treeRef.current?.showContextMenu(),
},
'sidebar.expand_all': {
"sidebar.expand_all": {
enable: isSidebarFocused,
cb: () => {
jotaiStore.set(collapsedFamily(treeId), {});
},
},
'sidebar.collapse_all': {
"sidebar.collapse_all": {
enable: isSidebarFocused,
cb: () => {
if (tree == null) return;
@@ -252,7 +252,7 @@ function Sidebar({ className }: { className?: string }) {
const next = (node: TreeNode<SidebarModel>, collapsed: Record<string, boolean>) => {
let newCollapsed = { ...collapsed };
for (const n of node.children ?? []) {
if (n.item.model !== 'folder') continue;
if (n.item.model !== "folder") continue;
newCollapsed[n.item.id] = true;
newCollapsed = next(n, newCollapsed);
}
@@ -262,13 +262,13 @@ function Sidebar({ className }: { className?: string }) {
jotaiStore.set(collapsedFamily(treeId), collapsed);
},
},
'sidebar.selected.delete': {
"sidebar.selected.delete": {
enable,
cb: async (items: SidebarModel[]) => {
await deleteModelWithConfirm(items);
},
},
'sidebar.selected.rename': {
"sidebar.selected.rename": {
enable,
allowDefault: true,
cb: async (items: SidebarModel[]) => {
@@ -278,7 +278,7 @@ function Sidebar({ className }: { className?: string }) {
}
},
},
'sidebar.selected.duplicate': {
"sidebar.selected.duplicate": {
// Higher priority so this takes precedence over model.duplicate (same Meta+d binding)
priority: 10,
enable,
@@ -292,24 +292,26 @@ function Sidebar({ className }: { className?: string }) {
}
},
},
'sidebar.selected.move': {
"sidebar.selected.move": {
enable,
cb: async (items: SidebarModel[]) => {
const requests = items.filter(
(i): i is HttpRequest | GrpcRequest | WebsocketRequest =>
i.model === 'http_request' || i.model === 'grpc_request' || i.model === 'websocket_request'
i.model === "http_request" ||
i.model === "grpc_request" ||
i.model === "websocket_request",
);
if (requests.length > 0) {
moveToWorkspace.mutate(requests);
}
},
},
'request.send': {
"request.send": {
enable,
cb: async (items: SidebarModel[]) => {
await Promise.all(
items
.filter((i) => i.model === 'http_request')
.filter((i) => i.model === "http_request")
.map((i) => sendAnyHttpRequest.mutate(i.id)),
);
},
@@ -333,76 +335,78 @@ function Sidebar({ className }: { className?: string }) {
}
const workspaces = jotaiStore.get(workspacesAtom);
const onlyHttpRequests = items.every((i) => i.model === 'http_request');
const onlyHttpRequests = items.every((i) => i.model === "http_request");
const requestItems = items.filter(
(i) =>
i.model === 'http_request' || i.model === 'grpc_request' || i.model === 'websocket_request',
i.model === "http_request" ||
i.model === "grpc_request" ||
i.model === "websocket_request",
);
const initialItems: ContextMenuProps['items'] = [
const initialItems: ContextMenuProps["items"] = [
{
label: 'Folder Settings',
hidden: !(items.length === 1 && child.model === 'folder'),
label: "Folder Settings",
hidden: !(items.length === 1 && child.model === "folder"),
leftSlot: <Icon icon="folder_cog" />,
onSelect: () => openFolderSettings(child.id),
},
{
label: 'Send',
hotKeyAction: 'request.send',
label: "Send",
hotKeyAction: "request.send",
hotKeyLabelOnly: true,
hidden: !onlyHttpRequests,
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => actions['request.send'].cb(items),
onSelect: () => actions["request.send"].cb(items),
},
...(items.length === 1 && child.model === 'http_request'
...(items.length === 1 && child.model === "http_request"
? await getHttpRequestActions()
: []
).map((a) => ({
label: a.label,
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? "empty"} />,
onSelect: async () => {
const request = getModel('http_request', child.id);
const request = getModel("http_request", child.id);
if (request != null) await a.call(request);
},
})),
...(items.length === 1 && child.model === 'grpc_request'
...(items.length === 1 && child.model === "grpc_request"
? await getGrpcRequestActions()
: []
).map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? "empty"} />,
onSelect: async () => {
const request = getModel('grpc_request', child.id);
const request = getModel("grpc_request", child.id);
if (request != null) await a.call(request);
},
})),
...(items.length === 1 && child.model === 'websocket_request'
...(items.length === 1 && child.model === "websocket_request"
? await getWebsocketRequestActions()
: []
).map((a) => ({
label: a.label,
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? "empty"} />,
onSelect: async () => {
const request = getModel('websocket_request', child.id);
const request = getModel("websocket_request", child.id);
if (request != null) await a.call(request);
},
})),
...(items.length === 1 && child.model === 'folder' ? await getFolderActions() : []).map(
...(items.length === 1 && child.model === "folder" ? await getFolderActions() : []).map(
(a) => ({
label: a.label,
leftSlot: <Icon icon={a.icon ?? 'empty'} />,
leftSlot: <Icon icon={a.icon ?? "empty"} />,
onSelect: async () => {
const model = getModel('folder', child.id);
const model = getModel("folder", child.id);
if (model != null) await a.call(model);
},
}),
),
];
const modelCreationItems: DropdownItem[] =
items.length === 1 && child.model === 'folder'
items.length === 1 && child.model === "folder"
? [
{ type: 'separator' },
{ type: "separator" },
...getCreateDropdownItems({
workspaceId,
activeRequest: null,
@@ -410,46 +414,49 @@ function Sidebar({ className }: { className?: string }) {
}),
]
: [];
const menuItems: ContextMenuProps['items'] = [
const menuItems: ContextMenuProps["items"] = [
...initialItems,
{
type: 'separator',
type: "separator",
hidden: initialItems.filter((v) => !v.hidden).length === 0,
},
{
label: 'Rename',
label: "Rename",
leftSlot: <Icon icon="pencil" />,
hidden: items.length > 1,
hotKeyAction: 'sidebar.selected.rename',
hotKeyAction: "sidebar.selected.rename",
hotKeyLabelOnly: true,
onSelect: () => {
treeRef.current?.renameItem(child.id);
},
},
{
label: 'Duplicate',
hotKeyAction: 'model.duplicate',
label: "Duplicate",
hotKeyAction: "model.duplicate",
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => actions['sidebar.selected.duplicate'].cb(items),
onSelect: () => actions["sidebar.selected.duplicate"].cb(items),
},
{
label: items.length <= 1 ? 'Move' : `Move ${requestItems.length} Requests`,
hotKeyAction: 'sidebar.selected.move',
label: items.length <= 1 ? "Move" : `Move ${requestItems.length} Requests`,
hotKeyAction: "sidebar.selected.move",
hotKeyLabelOnly: true,
leftSlot: <Icon icon="arrow_right_circle" />,
hidden: workspaces.length <= 1 || requestItems.length === 0 || requestItems.length !== items.length,
hidden:
workspaces.length <= 1 ||
requestItems.length === 0 ||
requestItems.length !== items.length,
onSelect: () => {
fireAndForget(actions['sidebar.selected.move'].cb(items));
fireAndForget(actions["sidebar.selected.move"].cb(items));
},
},
{
color: 'danger',
label: 'Delete',
hotKeyAction: 'sidebar.selected.delete',
color: "danger",
label: "Delete",
hotKeyAction: "sidebar.selected.delete",
hotKeyLabelOnly: true,
leftSlot: <Icon icon="trash" />,
onSelect: () => actions['sidebar.selected.delete'].cb(items),
onSelect: () => actions["sidebar.selected.delete"].cb(items),
},
...modelCreationItems,
];
@@ -458,7 +465,7 @@ function Sidebar({ className }: { className?: string }) {
[actions],
);
const hotkeys = useMemo<TreeProps<SidebarModel>['hotkeys']>(() => ({ actions }), [actions]);
const hotkeys = useMemo<TreeProps<SidebarModel>["hotkeys"]>(() => ({ actions }), [actions]);
// Use a language compartment for the filter so we can reconfigure it when the autocompletion changes
const filterLanguageCompartmentRef = useRef(new Compartment());
@@ -486,7 +493,7 @@ function Sidebar({ className }: { className?: string }) {
<aside
ref={wrapperRef}
aria-hidden={hidden ?? undefined}
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)_auto]')}
className={classNames(className, "h-full grid grid-rows-[auto_minmax(0,1fr)_auto]")}
>
<div className="w-full pl-3 pr-0.5 pt-3 grid grid-cols-[minmax(0,1fr)_auto] items-center">
{(tree.children?.length ?? 0) > 0 && (
@@ -519,7 +526,7 @@ function Sidebar({ className }: { className?: string }) {
<Dropdown
items={[
{
label: 'Focus Active Request',
label: "Focus Active Request",
leftSlot: <Icon icon="crosshair" />,
onSelect: () => {
const activeId = jotaiStore.get(activeIdAtom);
@@ -532,7 +539,7 @@ function Sidebar({ className }: { className?: string }) {
jotaiStore.set(collapsedFamily(treeId), (prev) => {
const n = { ...prev };
for (const ancestor of ancestors) {
if (ancestor.model === 'folder') {
if (ancestor.model === "folder") {
delete n[ancestor.id];
}
}
@@ -543,17 +550,17 @@ function Sidebar({ className }: { className?: string }) {
},
},
{
label: 'Expand All Folders',
label: "Expand All Folders",
leftSlot: <Icon icon="chevrons_up_down" />,
onSelect: actions['sidebar.expand_all'].cb,
hotKeyAction: 'sidebar.expand_all',
onSelect: actions["sidebar.expand_all"].cb,
hotKeyAction: "sidebar.expand_all",
hotKeyLabelOnly: true,
},
{
label: 'Collapse All Folders',
label: "Collapse All Folders",
leftSlot: <Icon icon="chevrons_down_up" />,
onSelect: actions['sidebar.collapse_all'].cb,
hotKeyAction: 'sidebar.collapse_all',
onSelect: actions["sidebar.collapse_all"].cb,
hotKeyAction: "sidebar.collapse_all",
hotKeyLabelOnly: true,
},
]}
@@ -601,7 +608,7 @@ const activeIdAtom = atom<string | null>((get) => {
function getEditOptions(
item: SidebarModel,
): ReturnType<NonNullable<TreeItemProps<SidebarModel>['getEditOptions']>> {
): ReturnType<NonNullable<TreeItemProps<SidebarModel>["getEditOptions"]>> {
return {
onChange: handleSubmitEdit,
defaultValue: resolvedModelName(item),
@@ -615,7 +622,7 @@ async function handleSubmitEdit(item: SidebarModel, text: string) {
function handleActivate(item: SidebarModel) {
// TODO: Add folder layout support
if (item.model !== 'folder' && item.model !== 'workspace') {
if (item.model !== "folder" && item.model !== "workspace") {
navigateToRequestOrFolderOrWorkspace(item.id, item.model);
}
}
@@ -629,8 +636,8 @@ const allPotentialChildrenAtom = atom<SidebarModel[]>((get) => {
const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom);
const sidebarFilterAtom = atom<{ text: string; key: string }>({
text: '',
key: '',
text: "",
key: "",
});
const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get) => {
@@ -640,10 +647,10 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
const childrenMap: Record<string, Exclude<SidebarModel, Workspace>[]> = {};
for (const item of allModels) {
if ('folderId' in item && item.folderId == null) {
if ("folderId" in item && item.folderId == null) {
childrenMap[item.workspaceId] = childrenMap[item.workspaceId] ?? [];
childrenMap[item.workspaceId]?.push(item);
} else if ('folderId' in item && item.folderId != null) {
} else if ("folderId" in item && item.folderId != null) {
childrenMap[item.folderId] = childrenMap[item.folderId] ?? [];
childrenMap[item.folderId]?.push(item);
}
@@ -662,7 +669,7 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
let matchesSelf = true;
const fields = getItemFields(node);
const model = node.item.model;
const isLeafNode = !(model === 'folder' || model === 'workspace');
const isLeafNode = !(model === "folder" || model === "workspace");
for (const [field, value] of Object.entries(fields)) {
if (!value) continue;
@@ -727,9 +734,9 @@ const sidebarTreeAtom = atom<[TreeNode<SidebarModel>, FieldDef[]] | null>((get)
function getItemKey(item: SidebarModel) {
const responses = jotaiStore.get(httpResponsesAtom);
const latestResponse = responses.find((r) => r.requestId === item.id) ?? null;
const url = 'url' in item ? item.url : 'n/a';
const method = 'method' in item ? item.method : 'n/a';
const service = 'service' in item ? item.service : 'n/a';
const url = "url" in item ? item.url : "n/a";
const method = "method" in item ? item.method : "n/a";
const service = "service" in item ? item.service : "n/a";
return [
item.id,
item.name,
@@ -737,8 +744,8 @@ function getItemKey(item: SidebarModel) {
method,
service,
latestResponse?.elapsed,
latestResponse?.id ?? 'n/a',
].join('::');
latestResponse?.id ?? "n/a",
].join("::");
}
const SidebarLeftSlot = memo(function SidebarLeftSlot({
@@ -748,17 +755,17 @@ const SidebarLeftSlot = memo(function SidebarLeftSlot({
treeId: string;
item: SidebarModel;
}) {
if (item.model === 'folder') {
if (item.model === "folder") {
return <Icon icon="folder" />;
}
if (item.model === 'workspace') {
if (item.model === "workspace") {
return null;
}
const isSelected = jotaiStore.get(isSelectedFamily({ treeId, itemId: item.id }));
return (
<HttpMethodTag
short
className={classNames('text-xs pl-1.5', !isSelected && OPACITY_SUBTLE)}
className={classNames("text-xs pl-1.5", !isSelected && OPACITY_SUBTLE)}
request={item}
/>
);
@@ -791,9 +798,9 @@ const SidebarInnerItem = memo(function SidebarInnerItem({
<div className="truncate">{resolvedModelName(item)}</div>
{response != null && (
<div className="ml-auto">
{response.state !== 'closed' ? (
{response.state !== "closed" ? (
<LoadingIcon size="sm" className="text-text-subtlest" />
) : response.model === 'http_response' ? (
) : response.model === "http_response" ? (
<HttpStatusTag short className="text-xs" response={response} />
) : null}
</div>
@@ -805,26 +812,26 @@ const SidebarInnerItem = memo(function SidebarInnerItem({
function getItemFields(node: TreeNode<SidebarModel>): Record<string, string> {
const item = node.item;
if (item.model === 'workspace') return {};
if (item.model === "workspace") return {};
const fields: Record<string, string> = {};
if (item.model === 'http_request') {
if (item.model === "http_request") {
fields.method = item.method.toUpperCase();
}
if (item.model === 'grpc_request') {
fields.grpc_method = item.method ?? '';
fields.grpc_service = item.service ?? '';
if (item.model === "grpc_request") {
fields.grpc_method = item.method ?? "";
fields.grpc_service = item.service ?? "";
}
if ('url' in item) fields.url = item.url;
if ("url" in item) fields.url = item.url;
fields.name = resolvedModelName(item);
fields.type = 'http';
if (item.model === 'grpc_request') fields.type = 'grpc';
else if (item.model === 'websocket_request') fields.type = 'ws';
fields.type = "http";
if (item.model === "grpc_request") fields.type = "grpc";
else if (item.model === "websocket_request") fields.type = "ws";
if (node.parent?.item.model === 'folder') {
if (node.parent?.item.model === "folder") {
fields.folder = node.parent.item.name;
}
@@ -833,11 +840,11 @@ function getItemFields(node: TreeNode<SidebarModel>): Record<string, string> {
function getItemText(item: SidebarModel): string {
const segments = [];
if (item.model === 'http_request') {
if (item.model === "http_request") {
segments.push(item.method);
}
segments.push(resolvedModelName(item));
return segments.join(' ');
return segments.join(" ");
}

View File

@@ -1,10 +1,10 @@
import { useMemo } from 'react';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { CreateDropdown } from './CreateDropdown';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { useMemo } from "react";
import { useFloatingSidebarHidden } from "../hooks/useFloatingSidebarHidden";
import { useShouldFloatSidebar } from "../hooks/useShouldFloatSidebar";
import { useSidebarHidden } from "../hooks/useSidebarHidden";
import { CreateDropdown } from "./CreateDropdown";
import { IconButton } from "./core/IconButton";
import { HStack } from "./core/Stacks";
export function SidebarActions() {
const floating = useShouldFloatSidebar();
@@ -28,7 +28,7 @@ export function SidebarActions() {
className="pointer-events-auto"
size="sm"
title="Toggle sidebar"
icon={hidden ? 'left_panel_hidden' : 'left_panel_visible'}
icon={hidden ? "left_panel_hidden" : "left_panel_visible"}
iconColor="secondary"
/>
<CreateDropdown hotKeyAction="model.create">

View File

@@ -1,13 +1,13 @@
import type { Workspace } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { switchWorkspace } from '../commands/switchWorkspace';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { HStack, VStack } from './core/Stacks';
import type { Workspace } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { switchWorkspace } from "../commands/switchWorkspace";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { Icon } from "./core/Icon";
import { InlineCode } from "./core/InlineCode";
import { HStack, VStack } from "./core/Stacks";
interface Props {
hide: () => void;

View File

@@ -1,11 +1,11 @@
import { readDir } from '@tauri-apps/plugin-fs';
import { useState } from 'react';
import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { VStack } from './core/Stacks';
import { SelectFile } from './SelectFile';
import { readDir } from "@tauri-apps/plugin-fs";
import { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { Banner } from "./core/Banner";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { VStack } from "./core/Stacks";
import { SelectFile } from "./SelectFile";
export interface SyncToFilesystemSettingProps {
onChange: (args: { filePath: string | null; initGit?: boolean }) => void;
@@ -62,7 +62,7 @@ export function SyncToFilesystemSetting({
}}
/>
{value.filePath && typeof value.initGit === 'boolean' && (
{value.filePath && typeof value.initGit === "boolean" && (
<Checkbox
checked={value.initGit}
onChange={(initGit) => onChange({ ...value, initGit })}

View File

@@ -1,37 +1,37 @@
import type { EditorView } from '@codemirror/view';
import type { EditorView } from "@codemirror/view";
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import { parseTemplate } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { useDebouncedValue } from '../hooks/useDebouncedValue';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateFunctionConfig } from '../hooks/useTemplateFunctionConfig';
} from "@yaakapp-internal/models";
import type { TemplateFunction } from "@yaakapp-internal/plugins";
import type { FnArg, Tokens } from "@yaakapp-internal/templates";
import { parseTemplate } from "@yaakapp-internal/templates";
import classNames from "classnames";
import { useEffect, useMemo, useState } from "react";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { useDebouncedValue } from "../hooks/useDebouncedValue";
import { useRenderTemplate } from "../hooks/useRenderTemplate";
import { useTemplateFunctionConfig } from "../hooks/useTemplateFunctionConfig";
import {
templateTokensToString,
useTemplateTokensToString,
} from '../hooks/useTemplateTokensToString';
import { useToggle } from '../hooks/useToggle';
import { showDialog } from '../lib/dialog';
import { convertTemplateToInsecure } from '../lib/encryption';
import { jotaiStore } from '../lib/jotai';
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption';
import { Button } from './core/Button';
import { collectArgumentValues } from './core/Editor/twig/util';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { LoadingIcon } from './core/LoadingIcon';
import { PlainInput } from './core/PlainInput';
import { HStack } from './core/Stacks';
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from './DynamicForm';
} from "../hooks/useTemplateTokensToString";
import { useToggle } from "../hooks/useToggle";
import { showDialog } from "../lib/dialog";
import { convertTemplateToInsecure } from "../lib/encryption";
import { jotaiStore } from "../lib/jotai";
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
import { Button } from "./core/Button";
import { collectArgumentValues } from "./core/Editor/twig/util";
import { IconButton } from "./core/IconButton";
import { InlineCode } from "./core/InlineCode";
import { LoadingIcon } from "./core/LoadingIcon";
import { PlainInput } from "./core/PlainInput";
import { HStack } from "./core/Stacks";
import { DYNAMIC_FORM_NULL_ARG, DynamicForm } from "./DynamicForm";
interface Props {
templateFunction: TemplateFunction;
@@ -55,7 +55,7 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
// HACK: Replace the secure() function's encrypted `value` arg with the decrypted version so
// we can display it in the editor input.
if (templateFunction.name === 'secure') {
if (templateFunction.name === "secure") {
const template = await templateTokensToString(initialTokens);
initial.value = await convertTemplateToInsecure(template);
}
@@ -88,10 +88,10 @@ function InitializedTemplateFunctionDialog({
hide,
onChange,
model,
}: Omit<Props, 'initialTokens'> & {
}: Omit<Props, "initialTokens"> & {
initialArgValues: Record<string, string | boolean>;
}) {
const previewType = ogPreviewType == null ? 'live' : ogPreviewType;
const previewType = ogPreviewType == null ? "live" : ogPreviewType;
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(initialArgValues);
@@ -100,18 +100,18 @@ function InitializedTemplateFunctionDialog({
name,
value:
argValues[name] === DYNAMIC_FORM_NULL_ARG
? { type: 'null' }
: typeof argValues[name] === 'boolean'
? { type: 'bool', value: argValues[name] === true }
: { type: 'str', text: String(argValues[name] ?? '') },
? { type: "null" }
: typeof argValues[name] === "boolean"
? { type: "bool", value: argValues[name] === true }
: { type: "str", text: String(argValues[name] ?? "") },
}));
return {
tokens: [
{
type: 'tag',
type: "tag",
val: {
type: 'fn',
type: "fn",
name,
args: argTokens,
},
@@ -130,13 +130,13 @@ function InitializedTemplateFunctionDialog({
hide();
};
const debouncedTagText = useDebouncedValue(tagText.data ?? '', 400);
const debouncedTagText = useDebouncedValue(tagText.data ?? "", 400);
const [renderKey, setRenderKey] = useState<string | null>(null);
const rendered = useRenderTemplate({
template: debouncedTagText,
enabled: previewType !== 'none',
purpose: previewType === 'click' ? 'send' : 'preview',
refreshKey: previewType === 'live' ? renderKey + debouncedTagText : renderKey,
enabled: previewType !== "none",
purpose: previewType === "click" ? "send" : "preview",
refreshKey: previewType === "live" ? renderKey + debouncedTagText : renderKey,
ignoreError: false,
});
@@ -144,9 +144,9 @@ function InitializedTemplateFunctionDialog({
// oxlint-disable-next-line react-hooks/exhaustive-deps -- Only update this on rendered data change to keep secrets hidden on input change
const dataContainsSecrets = useMemo(() => {
for (const [name, value] of Object.entries(argValues)) {
const arg = templateFunction.data?.args.find((a) => 'name' in a && a.name === name);
const isTextPassword = arg?.type === 'text' && arg.password;
if (isTextPassword && typeof value === 'string' && value && rendered.data?.includes(value)) {
const arg = templateFunction.data?.args.find((a) => "name" in a && a.name === name);
const isTextPassword = arg?.type === "text" && arg.password;
if (isTextPassword && typeof value === "string" && value && rendered.data?.includes(value)) {
return true;
}
}
@@ -170,14 +170,14 @@ function InitializedTemplateFunctionDialog({
}}
>
<div className="overflow-y-auto h-full px-6">
{name === 'secure' ? (
{name === "secure" ? (
<PlainInput
required
label="Value"
name="value"
type="password"
placeholder="••••••••••••"
defaultValue={String(argValues.value ?? '')}
defaultValue={String(argValues.value ?? "")}
onChange={(value) => setArgValues({ ...argValues, value })}
/>
) : (
@@ -192,7 +192,7 @@ function InitializedTemplateFunctionDialog({
)}
</div>
<div className="px-6 border-t border-t-border pt-3 pb-6 bg-surface-highlight w-full flex flex-col gap-4">
{previewType !== 'none' ? (
{previewType !== "none" ? (
<div className="w-full grid grid-cols-1 grid-rows-[auto_auto]">
<HStack space={0.5}>
<HStack className="text-sm text-text-subtle" space={1.5}>
@@ -202,32 +202,32 @@ function InitializedTemplateFunctionDialog({
<IconButton
size="xs"
iconSize="sm"
icon={showSecretsInPreview ? 'lock' : 'lock_open'}
title={showSecretsInPreview ? 'Show preview' : 'Hide preview'}
icon={showSecretsInPreview ? "lock" : "lock_open"}
title={showSecretsInPreview ? "Show preview" : "Hide preview"}
onClick={toggleShowSecretsInPreview}
className={classNames(
'ml-auto text-text-subtlest',
!dataContainsSecrets && 'invisible',
"ml-auto text-text-subtlest",
!dataContainsSecrets && "invisible",
)}
/>
</HStack>
<div className="relative w-full max-h-[10rem]">
<InlineCode
className={classNames(
'block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest',
tooLarge && 'italic text-danger',
"block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest",
tooLarge && "italic text-danger",
)}
>
{rendered.error || tagText.error ? (
<em className="text-danger">
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, '')}
{`${rendered.error || tagText.error}`.replace(/^Render Error: /, "")}
</em>
) : dataContainsSecrets && !showSecretsInPreview ? (
<span className="italic text-text-subtle">
------ sensitive values hidden ------
</span>
) : tooLarge ? (
'too large to preview'
"too large to preview"
) : (
rendered.data || <>&nbsp;</>
)}
@@ -250,7 +250,7 @@ function InitializedTemplateFunctionDialog({
<span />
)}
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
{templateFunction.data.name === 'secure' && (
{templateFunction.data.name === "secure" && (
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
Reveal Encryption Key
</Button>
@@ -273,8 +273,8 @@ TemplateFunctionDialog.show = (
const initialTokens = parseTemplate(tagValue);
showDialog({
id: `template-function-${Math.random()}`, // Allow multiple at once
size: 'md',
className: 'h-[60rem]',
size: "md",
className: "h-[60rem]",
noPadding: true,
title: <InlineCode>{fn.name}()</InlineCode>,
description: fn.description,

View File

@@ -1,18 +1,18 @@
import { useAtomValue } from 'jotai';
import { AnimatePresence } from 'motion/react';
import type { ReactNode } from 'react';
import { hideToast, toastsAtom } from '../lib/toast';
import { Toast, type ToastProps } from './core/Toast';
import { ErrorBoundary } from './ErrorBoundary';
import { Portal } from './Portal';
import { useAtomValue } from "jotai";
import { AnimatePresence } from "motion/react";
import type { ReactNode } from "react";
import { hideToast, toastsAtom } from "../lib/toast";
import { Toast, type ToastProps } from "./core/Toast";
import { ErrorBoundary } from "./ErrorBoundary";
import { Portal } from "./Portal";
export type ToastInstance = {
id: string;
uniqueKey: string;
message: ReactNode;
timeout: 3000 | 5000 | 8000 | (number & {}) | null;
onClose?: ToastProps['onClose'];
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
onClose?: ToastProps["onClose"];
} & Omit<ToastProps, "onClose" | "open" | "children" | "timeout">;
export const Toasts = () => {
const toasts = useAtomValue(toastsAtom);

View File

@@ -1,29 +1,29 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { FormEvent, ReactNode } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { useHotKey } from '../hooks/useHotKey';
import type { IconProps } from './core/Icon';
import { IconButton } from './core/IconButton';
import type { InputHandle, InputProps } from './core/Input';
import { Input } from './core/Input';
import { HStack } from './core/Stacks';
import type { HttpRequest } from "@yaakapp-internal/models";
import classNames from "classnames";
import type { FormEvent, ReactNode } from "react";
import { memo, useCallback, useRef, useState } from "react";
import { useHotKey } from "../hooks/useHotKey";
import type { IconProps } from "./core/Icon";
import { IconButton } from "./core/IconButton";
import type { InputHandle, InputProps } from "./core/Input";
import { Input } from "./core/Input";
import { HStack } from "./core/Stacks";
type Props = Pick<HttpRequest, 'url'> & {
type Props = Pick<HttpRequest, "url"> & {
className?: string;
placeholder: string;
onSend: () => void;
onUrlChange: (url: string) => void;
onPaste?: (v: string) => void;
onPasteOverwrite?: InputProps['onPasteOverwrite'];
onPasteOverwrite?: InputProps["onPasteOverwrite"];
onCancel: () => void;
submitIcon?: IconProps['icon'] | null;
submitIcon?: IconProps["icon"] | null;
isLoading: boolean;
forceUpdateKey: string;
rightSlot?: ReactNode;
leftSlot?: ReactNode;
autocomplete?: InputProps['autocomplete'];
stateKey: InputProps['stateKey'];
autocomplete?: InputProps["autocomplete"];
stateKey: InputProps["stateKey"];
};
export const UrlBar = memo(function UrlBar({
@@ -36,7 +36,7 @@ export const UrlBar = memo(function UrlBar({
onCancel,
onPaste,
onPasteOverwrite,
submitIcon = 'send_horizontal',
submitIcon = "send_horizontal",
autocomplete,
leftSlot,
rightSlot,
@@ -50,7 +50,7 @@ export const UrlBar = memo(function UrlBar({
inputRef.current = h;
}, []);
useHotKey('url_bar.focus', () => {
useHotKey("url_bar.focus", () => {
inputRef.current?.selectAll();
});
@@ -61,7 +61,7 @@ export const UrlBar = memo(function UrlBar({
};
return (
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
<form onSubmit={handleSubmit} className={classNames("x-theme-urlBar", className)}>
<Input
setRef={handleInitInputRef}
autocompleteFunctions
@@ -96,7 +96,7 @@ export const UrlBar = memo(function UrlBar({
type="submit"
className="w-8 mr-0.5 !h-full"
iconColor="secondary"
icon={isLoading ? 'x' : submitIcon}
icon={isLoading ? "x" : submitIcon}
hotkeyAction="request.send"
onMouseDown={(e) => {
// Prevent the button from taking focus

View File

@@ -1,15 +1,15 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { useCallback, useRef } from 'react';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import type { PairEditorHandle, PairEditorProps } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { VStack } from './core/Stacks';
import type { HttpRequest } from "@yaakapp-internal/models";
import { useCallback, useRef } from "react";
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
import type { PairEditorHandle, PairEditorProps } from "./core/PairEditor";
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
import { VStack } from "./core/Stacks";
type Props = {
forceUpdateKey: string;
pairs: HttpRequest['headers'];
stateKey: PairEditorProps['stateKey'];
onChange: (headers: HttpRequest['urlParameters']) => void;
pairs: HttpRequest["headers"];
stateKey: PairEditorProps["stateKey"];
onChange: (headers: HttpRequest["urlParameters"]) => void;
};
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey }: Props) {
@@ -21,7 +21,7 @@ export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey
const [{ urlParametersKey }] = useRequestEditor();
useRequestEditorEvent(
'request_params.focus_value',
"request_params.focus_value",
(name) => {
const pair = pairs.find((p) => p.name === name);
if (pair?.id != null) {

View File

@@ -1,12 +1,12 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import type { WebsocketRequest } from "@yaakapp-internal/models";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { CSSProperties } from "react";
import { workspaceLayoutAtom } from '../lib/atoms';
import { SplitLayout } from './core/SplitLayout';
import { WebsocketRequestPane } from './WebsocketRequestPane';
import { WebsocketResponsePane } from './WebsocketResponsePane';
import { workspaceLayoutAtom } from "../lib/atoms";
import { SplitLayout } from "./core/SplitLayout";
import { WebsocketRequestPane } from "./WebsocketRequestPane";
import { WebsocketResponsePane } from "./WebsocketResponsePane";
interface Props {
activeRequest: WebsocketRequest;
@@ -25,17 +25,17 @@ export function WebsocketRequestLayout({ activeRequest, style }: Props) {
<WebsocketRequestPane
style={style}
activeRequest={activeRequest}
fullHeight={orientation === 'horizontal'}
fullHeight={orientation === "horizontal"}
/>
)}
secondSlot={({ style }) => (
<div
style={style}
className={classNames(
'x-theme-responsePane',
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
'bg-surface rounded-md border border-border-subtle',
'shadow relative',
"x-theme-responsePane",
"max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1",
"bg-surface rounded-md border border-border-subtle",
"shadow relative",
)}
>
<WebsocketResponsePane activeRequest={activeRequest} />

Some files were not shown because too many files have changed in this diff Show More