Move a bunch of git ops to use the git binary (#302)

This commit is contained in:
Gregory Schier
2025-11-17 15:22:39 -08:00
committed by GitHub
parent 84219571e8
commit 9c52652a5e
43 changed files with 1238 additions and 1176 deletions

View File

@@ -1,4 +1,4 @@
import { useGitInit } from '@yaakapp-internal/git';
import { gitMutations } from '@yaakapp-internal/git';
import type { WorkspaceMeta } from '@yaakapp-internal/models';
import { createGlobalModel, updateModel } from '@yaakapp-internal/models';
import { useState } from 'react';
@@ -12,6 +12,7 @@ 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 {
@@ -20,7 +21,6 @@ interface Props {
export function CreateWorkspaceDialog({ hide }: Props) {
const [name, setName] = useState<string>('');
const gitInit = useGitInit();
const [syncConfig, setSyncConfig] = useState<{
filePath: string | null;
initGit?: boolean;
@@ -48,9 +48,11 @@ export function CreateWorkspaceDialog({ hide }: Props) {
});
if (syncConfig.initGit && syncConfig.filePath) {
gitInit.mutateAsync({ dir: syncConfig.filePath }).catch((err) => {
showErrorToast('git-init-error', String(err));
});
gitMutations(syncConfig.filePath, gitCallbacks(syncConfig.filePath))
.init.mutateAsync()
.catch((err) => {
showErrorToast('git-init-error', String(err));
});
}
// Navigate to workspace

View File

@@ -32,10 +32,10 @@ function DialogInstance({ render: Component, onClose, id, ...props }: DialogInst
}, [id, onClose]);
return (
<ErrorBoundary name={`Dialog ${id}`}>
<Dialog open onClose={handleClose} {...props}>
<Dialog open onClose={handleClose} {...props}>
<ErrorBoundary name={`Dialog ${id}`}>
<Component hide={hide} {...props} />
</Dialog>
</ErrorBoundary>
</ErrorBoundary>
</Dialog>
);
}

View File

@@ -23,8 +23,10 @@ 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 { PlainInput } from './core/PlainInput';
import { Select } from './core/Select';
import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
@@ -269,28 +271,31 @@ function TextArg({
autocompleteVariables: boolean;
stateKey: string;
}) {
return (
<Input
name={arg.name}
multiLine={arg.multiLine}
onChange={onChange}
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'}
label={arg.label ?? arg.name}
size={INPUT_SIZE}
hideLabel={arg.hideLabel ?? arg.label == null}
placeholder={arg.placeholder ?? undefined}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables}
stateKey={stateKey}
forceUpdateKey={stateKey}
/>
);
const props: InputProps = {
onChange,
name: arg.name,
multiLine: arg.multiLine,
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',
label: arg.label ?? arg.name,
size: INPUT_SIZE,
hideLabel: arg.hideLabel ?? arg.label == null,
placeholder: arg.placeholder ?? undefined,
forceUpdateKey: stateKey,
autocomplete: arg.completionOptions ? { options: arg.completionOptions } : undefined,
stateKey,
autocompleteFunctions,
autocompleteVariables,
};
if (autocompleteVariables || autocompleteFunctions) {
return <Input {...props} />;
} else {
return <PlainInput {...props} />;
}
}
function EditorArg({

View File

@@ -79,6 +79,7 @@ export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
</div>
) : (
<EnvironmentEditor
key={selectedEnvironment.id}
setRef={setRef}
className="pl-4 pt-3"
environment={selectedEnvironment}

View File

@@ -10,6 +10,17 @@ import type {
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import {
duplicateModel,
foldersAtom,
getAnyModel,
getModel,
grpcConnectionsAtom,
httpResponsesAtom,
patchModel,
websocketConnectionsAtom,
workspacesAtom,
} from '@yaakapp-internal/models';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
@@ -55,18 +66,7 @@ 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 './GitDropdown';
import {
getAnyModel,
duplicateModel,
foldersAtom,
getModel,
grpcConnectionsAtom,
httpResponsesAtom,
patchModel,
websocketConnectionsAtom,
workspacesAtom,
} from '@yaakapp-internal/models';
import { GitDropdown } from './git/GitDropdown';
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
function isSidebarLeafModel(m: AnyModel): boolean {

View File

@@ -16,7 +16,7 @@ export function Banner({ children, className, color }: BannerProps) {
color && 'bg-surface',
`x-theme-banner--${color}`,
'border border-border border-dashed',
'px-4 py-2 rounded-lg select-auto',
'px-4 py-2 rounded-lg select-auto cursor-auto',
'overflow-auto text-text',
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
)}

View File

@@ -75,6 +75,7 @@ import {
GitPullRequestIcon,
GripVerticalIcon,
HandIcon,
HardDriveDownloadIcon,
HistoryIcon,
HomeIcon,
ImportIcon,
@@ -202,6 +203,7 @@ const icons = {
grip_vertical: GripVerticalIcon,
circle_off: CircleOffIcon,
hand: HandIcon,
hard_drive_download: HardDriveDownloadIcon,
help: CircleHelpIcon,
history: HistoryIcon,
house: HomeIcon,

View File

@@ -16,7 +16,18 @@ import type { InputProps } from './Input';
import { Label } from './Label';
import { HStack } from './Stacks';
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type' | 'stateKey'> &
export type PlainInputProps = Omit<
InputProps,
| 'wrapLines'
| 'onKeyDown'
| 'type'
| 'stateKey'
| 'autocompleteVariables'
| 'autocompleteFunctions'
| 'autocomplete'
| 'extraExtensions'
| 'forcedEnvironmentId'
> &
Pick<HTMLAttributes<HTMLInputElement>, 'onKeyDownCapture'> & {
onFocusRaw?: HTMLAttributes<HTMLInputElement>['onFocus'];
type?: 'text' | 'password' | 'number';

View File

@@ -1,28 +1,27 @@
import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
import type { FormEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
import { generateId } from '../../lib/generateId';
import { DynamicForm } from '../DynamicForm';
import { Button } from './Button';
import { PlainInput } from './PlainInput';
import { HStack } from './Stacks';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
description?: ReactNode;
export interface PromptProps {
inputs: FormInput[];
onCancel: () => void;
onResult: (value: string | null) => void;
};
onResult: (value: Record<string, JsonPrimitive> | null) => void;
confirmText?: string;
cancelText?: string;
}
export function Prompt({
onCancel,
label,
defaultValue,
placeholder,
password,
inputs,
onResult,
required,
confirmText,
cancelText,
confirmText = 'Confirm',
cancelText = 'Cancel',
}: PromptProps) {
const [value, setValue] = useState<string>(defaultValue ?? '');
const [value, setValue] = useState<Record<string, JsonPrimitive>>({});
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -31,20 +30,14 @@ export function Prompt({
[onResult, value],
);
const id = 'prompt.form.' + useRef(generateId()).current;
return (
<form
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<PlainInput
autoSelect
required={required}
placeholder={placeholder ?? 'Enter text'}
type={password ? 'password' : 'text'}
label={label}
defaultValue={defaultValue}
onChange={setValue}
/>
<DynamicForm inputs={inputs} onChange={setValue} data={value} stateKey={id} />
<HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary">
{cancelText || 'Cancel'}

View File

@@ -42,7 +42,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
[open],
);
const toastIcon = icon === null ? null : icon ?? (color && color in ICONS && ICONS[color]);
const toastIcon = icon === null ? null : (icon ?? (color && color in ICONS && ICONS[color]));
return (
<m.div
@@ -64,7 +64,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
<div className="px-3 py-3 flex items-start gap-2 w-full">
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1" />}
<VStack space={2} className="w-full">
<div>{children}</div>
<div className="select-auto">{children}</div>
{action?.({ hide: onClose })}
</VStack>
</div>

View File

@@ -11,19 +11,21 @@ import type {
import classNames from 'classnames';
import { useMemo, useState } from 'react';
import { resolvedModelName } from '../lib/resolvedModelName';
import { showErrorToast, showToast } from '../lib/toast';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import type { CheckboxProps } from './core/Checkbox';
import { Checkbox } from './core/Checkbox';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { Input } from './core/Input';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { HStack } from './core/Stacks';
import { EmptyStateText } from './EmptyStateText';
import { resolvedModelName } from '../../lib/resolvedModelName';
import { showErrorToast } from '../../lib/toast';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import type { CheckboxProps } from '../core/Checkbox';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
import { Input } from '../core/Input';
import { Separator } from '../core/Separator';
import { SplitLayout } from '../core/SplitLayout';
import { HStack } from '../core/Stacks';
import { EmptyStateText } from '../EmptyStateText';
import { handlePushResult } from './git-util';
import { gitCallbacks } from './callbacks';
interface Props {
syncDir: string;
@@ -39,25 +41,34 @@ interface CommitTreeNode {
}
export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const [{ status }, { commit, commitAndPush, add, unstage, push }] = useGit(syncDir);
const [{ status }, { commit, commitAndPush, add, unstage }] = useGit(
syncDir,
gitCallbacks(syncDir),
);
const [isPushing, setIsPushing] = useState(false);
const [commitError, setCommitError] = useState<string | null>(null);
const [message, setMessage] = useState<string>('');
const handleCreateCommit = async () => {
setCommitError(null);
try {
await commit.mutateAsync({ message });
onDone();
} catch (err) {
showErrorToast('git-commit-error', String(err));
setCommitError(String(err));
}
};
const handleCreateCommitAndPush = async () => {
setIsPushing(true);
try {
await commitAndPush.mutateAsync({ message });
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
const r = await commitAndPush.mutateAsync({ message });
handlePushResult(r);
onDone();
} catch (err) {
showErrorToast('git-commit-and-push-error', String(err));
} finally {
setIsPushing(false);
}
};
@@ -81,7 +92,10 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const hasAnythingToAdd = allEntries.find((e) => e.status !== 'current') != null;
const tree: CommitTreeNode | null = useMemo(() => {
const next = (model: CommitTreeNode['model'], ancestors: CommitTreeNode[]): CommitTreeNode | null => {
const next = (
model: CommitTreeNode['model'],
ancestors: CommitTreeNode[],
): CommitTreeNode | null => {
const statusEntry = internalEntries?.find((s) => s.relaPath.includes(model.id));
if (statusEntry == null) {
return null;
@@ -147,7 +161,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
layout="vertical"
defaultRatio={0.3}
firstSlot={({ style }) => (
<div style={style} className="h-full overflow-y-auto -ml-1 pb-3">
<div style={style} className="h-full overflow-y-auto pb-3">
<TreeNodeChildren node={tree} depth={0} onCheck={checkNode} />
{externalEntries.find((e) => e.status !== 'current') && (
<>
@@ -175,7 +189,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
multiLine
hideLabel
/>
{commit.error && <Banner color="danger">{commit.error}</Banner>}
{commitError && <Banner color="danger">{commitError}</Banner>}
<HStack alignItems="center">
<InlineCode>{status.data?.headRefShorthand}</InlineCode>
<HStack space={2} className="ml-auto">
@@ -184,7 +198,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
size="sm"
onClick={handleCreateCommit}
disabled={!hasAddedAnything}
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
isLoading={isPushing}
>
Commit
</Button>
@@ -193,7 +207,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
size="sm"
disabled={!hasAddedAnything}
onClick={handleCreateCommitAndPush}
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
isLoading={isPushing}
>
Commit and Push
</Button>

View File

@@ -4,22 +4,25 @@ import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { HTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
import { useKeyValue } from '../hooks/useKeyValue';
import { sync } from '../init/sync';
import { showConfirm, showConfirmDelete } from '../lib/confirm';
import { showDialog } from '../lib/dialog';
import { showPrompt } from '../lib/prompt';
import { showErrorToast, showToast } from '../lib/toast';
import { Banner } from './core/Banner';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { BranchSelectionDialog } from './git/BranchSelectionDialog';
import { HistoryDialog } from './git/HistoryDialog';
import { openWorkspaceSettings } from '../../commands/openWorkspaceSettings';
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../../hooks/useActiveWorkspace';
import { useKeyValue } from '../../hooks/useKeyValue';
import { sync } from '../../init/sync';
import { showConfirm, showConfirmDelete } from '../../lib/confirm';
import { showDialog } from '../../lib/dialog';
import { showPrompt } from '../../lib/prompt';
import { showErrorToast, showToast } from '../../lib/toast';
import { Banner } from '../core/Banner';
import type { DropdownItem } from '../core/Dropdown';
import { Dropdown } from '../core/Dropdown';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
import { BranchSelectionDialog } from './BranchSelectionDialog';
import { gitCallbacks } from './callbacks';
import { handlePullResult } from './git-util';
import { GitCommitDialog } from './GitCommitDialog';
import { GitRemotesDialog } from './GitRemotesDialog';
import { HistoryDialog } from './HistoryDialog';
export function GitDropdown() {
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
@@ -37,7 +40,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
const [
{ status, log },
{ branch, deleteBranch, fetchAll, mergeBranch, push, pull, checkout, init },
] = useGit(syncDir);
] = useGit(syncDir, gitCallbacks(syncDir));
const localBranches = status.data?.localBranches ?? [];
const remoteBranches = status.data?.remoteBranches ?? [];
@@ -52,9 +55,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
const noRepo = status.error?.includes('not found');
if (noRepo) {
return (
<SetupGitDropdown workspaceId={workspace.id} initRepo={() => init.mutate({ dir: syncDir })} />
);
return <SetupGitDropdown workspaceId={workspace.id} initRepo={init.mutate} />;
}
const tryCheckout = (branch: string, force: boolean) => {
@@ -110,6 +111,12 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
});
},
},
{
label: 'Manage Remotes',
leftSlot: <Icon icon="hard_drive_download" />,
onSelect: () => GitRemotesDialog.show(syncDir),
},
{ type: 'separator' },
{
label: 'New Branch',
leftSlot: <Icon icon="git_branch_plus" />,
@@ -119,17 +126,17 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
title: 'Create Branch',
label: 'Branch Name',
});
if (name) {
await branch.mutateAsync(
{ branch: name },
{
onError: (err) => {
showErrorToast('git-branch-error', String(err));
},
if (!name) return;
await branch.mutateAsync(
{ branch: name },
{
onError: (err) => {
showErrorToast('git-branch-error', String(err));
},
);
tryCheckout(name, false);
}
},
);
tryCheckout(name, false);
},
},
{
@@ -214,18 +221,11 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
{ type: 'separator' },
{
label: 'Push',
hidden: (status.data?.origins ?? []).length === 0,
leftSlot: <Icon icon="arrow_up_from_line" />,
waitForOnSelect: true,
async onSelect() {
push.mutate(undefined, {
onSuccess(message) {
if (message === 'nothing_to_push') {
showToast({ id: 'push-success', message: 'Nothing to push', color: 'info' });
} else {
showToast({ id: 'push-success', message: 'Push successful', color: 'success' });
}
},
await push.mutateAsync(undefined, {
onSuccess: handlePullResult,
onError(err) {
showErrorToast('git-pull-error', String(err));
},
@@ -238,26 +238,17 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
leftSlot: <Icon icon="arrow_down_to_line" />,
waitForOnSelect: true,
async onSelect() {
const result = await pull.mutateAsync(undefined, {
await pull.mutateAsync(undefined, {
onSuccess: handlePullResult,
onError(err) {
showErrorToast('git-pull-error', String(err));
},
});
if (result.receivedObjects > 0) {
showToast({
id: 'git-pull-success',
message: `Pulled ${result.receivedObjects} objects`,
color: 'success',
});
await sync({ force: true });
} else {
showToast({ id: 'git-pull-success', message: 'Already up to date', color: 'info' });
}
},
},
{
label: 'Commit',
leftSlot: <Icon icon="git_branch" />,
leftSlot: <Icon icon="git_commit_vertical" />,
onSelect() {
showDialog({
id: 'commit',

View File

@@ -0,0 +1,67 @@
import { useGit } from '@yaakapp-internal/git';
import { showDialog } from '../../lib/dialog';
import { Button } from '../core/Button';
import { IconButton } from '../core/IconButton';
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table';
import { gitCallbacks } from './callbacks';
import { addGitRemote } from './showAddRemoteDialog';
interface Props {
dir: string;
onDone: () => void;
}
export function GitRemotesDialog({ dir }: Props) {
const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir));
return (
<div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>
<Button
className="text-text-subtle ml-auto"
size="2xs"
color="primary"
title="Add remote"
variant="border"
onClick={() => addGitRemote(dir)}
>
Add Remote
</Button>
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{remotes.data?.map((r, i) => (
<TableRow key={i}>
<TableCell>{r.name}</TableCell>
<TableCell>{r.url}</TableCell>
<TableCell>
<IconButton
size="sm"
className="text-text-subtle ml-auto"
icon="trash"
title="Remove remote"
onClick={() => rmRemote.mutate({ name: r.name })}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
GitRemotesDialog.show = function (dir: string) {
showDialog({
id: 'git-remotes',
title: 'Manage Remotes',
size: 'md',
render: ({ hide }) => <GitRemotesDialog onDone={hide} dir={dir} />,
});
};

View File

@@ -0,0 +1,48 @@
import type { GitCallbacks } from '@yaakapp-internal/git';
import { showPromptForm } from '../../lib/prompt-form';
import { Banner } from '../core/Banner';
import { InlineCode } from '../core/InlineCode';
import { addGitRemote } from './showAddRemoteDialog';
export function gitCallbacks(dir: string): GitCallbacks {
return {
addRemote: async () => {
return addGitRemote(dir);
},
promptCredentials: async ({ url: remoteUrl, error }) => {
const isGitHub = /github\.com/i.test(remoteUrl);
const userLabel = isGitHub ? 'GitHub Username' : 'Username';
const passLabel = isGitHub ? 'GitHub Personal Access Token' : 'Password / Token';
const userDescription = isGitHub ? 'Use your GitHub username (not your email).' : undefined;
const passDescription = isGitHub
? 'GitHub requires a Personal Access Token (PAT) for write operations over HTTPS. Passwords are not supported.'
: 'Enter your password or access token for this Git server.';
const r = await showPromptForm({
id: 'git-credentials',
title: 'Credentials Required',
description: error ? (
<Banner color="danger">{error}</Banner>
) : (
<>
Enter credentials for <InlineCode>{remoteUrl}</InlineCode>
</>
),
inputs: [
{ type: 'text', name: 'username', label: userLabel, description: userDescription },
{
type: 'text',
name: 'password',
label: passLabel,
description: passDescription,
password: true,
},
],
});
if (r == null) throw new Error('Cancelled credentials prompt');
const username = String(r.username || '');
const password = String(r.password || '');
return { username, password };
},
};
}

View File

@@ -0,0 +1,30 @@
import type { PullResult, PushResult } from '@yaakapp-internal/git';
import { showToast } from '../../lib/toast';
export function handlePushResult(r: PushResult) {
switch (r.type) {
case 'needs_credentials':
showToast({ id: 'push-error', message: 'Credentials not found', color: 'danger' });
break;
case 'success':
showToast({ id: 'push-success', message: r.message, color: 'success' });
break;
case 'up_to_date':
showToast({ id: 'push-nothing', message: 'Already up-to-date', color: 'info' });
break;
}
}
export function handlePullResult(r: PullResult) {
switch (r.type) {
case 'needs_credentials':
showToast({ id: 'pull-error', message: 'Credentials not found', color: 'danger' });
break;
case 'success':
showToast({ id: 'pull-success', message: r.message, color: 'success' });
break;
case 'up_to_date':
showToast({ id: 'pull-nothing', message: 'Already up-to-date', color: 'info' });
break;
}
}

View File

@@ -0,0 +1,20 @@
import type { GitRemote } from '@yaakapp-internal/git';
import { gitMutations } from '@yaakapp-internal/git';
import { showPromptForm } from '../../lib/prompt-form';
import { gitCallbacks } from './callbacks';
export async function addGitRemote(dir: string): Promise<GitRemote> {
const r = await showPromptForm({
id: 'add-remote',
title: 'Add Remote',
inputs: [
{ type: 'text', label: 'Name', name: 'name' },
{ type: 'text', label: 'URL', name: 'url' },
],
});
if (r == null) throw new Error('Cancelled remote prompt');
const name = String(r.name ?? '');
const url = String(r.url ?? '');
return gitMutations(dir, gitCallbacks(dir)).addRemote.mutateAsync({ name, url });
}

View File

@@ -24,7 +24,13 @@ type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> &
request: HttpRequest;
};
export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) {
export function GraphQLEditor(props: Props) {
// There's some weirdness with stale onChange being called when switching requests, so we'll
// key on the request ID as a workaround for now.
return <GraphQLEditorInner key={props.request.id} {...props} />;
}
function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProps }: Props) {
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
Record<string, boolean>
>('graphQLAutoIntrospectDisabled', {});