import type { GitStatusEntry } from '@yaakapp-internal/git'; import { useGit } from '@yaakapp-internal/git'; import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { useMemo, useState } from 'react'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { 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 { SplitLayout } from './core/SplitLayout'; import { HStack } from './core/Stacks'; import { EmptyStateText } from './EmptyStateText'; interface Props { syncDir: string; onDone: () => void; workspace: Workspace; } interface TreeNode { model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Environment | Workspace; status: GitStatusEntry; children: TreeNode[]; ancestors: TreeNode[]; } export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { const [{ status }, { commit, add, unstage, push }] = useGit(syncDir); const [message, setMessage] = useState(''); const handleCreateCommit = async () => { await commit.mutateAsync({ message }); onDone(); }; const handleCreateCommitAndPush = async () => { await commit.mutateAsync({ message }); await push.mutateAsync(); showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' }); onDone(); }; const entries = status.data?.entries ?? null; const hasAddedAnything = entries?.find((s) => s.staged) != null; const hasAnythingToAdd = entries?.find((s) => s.status !== 'current') != null; const tree: TreeNode | null = useMemo(() => { if (entries == null) { return null; } const next = (model: TreeNode['model'], ancestors: TreeNode[]): TreeNode | null => { const statusEntry = entries?.find((s) => s.relaPath.includes(model.id)); if (statusEntry == null) { return null; } const node: TreeNode = { model, status: statusEntry, children: [], ancestors, }; for (const entry of entries) { const childModel = entry.next ?? entry.prev; if (childModel == null) return null; // TODO: Is this right? // TODO: Figure out why not all of these show up if ('folderId' in childModel && childModel.folderId != null) { if (childModel.folderId === model.id) { const c = next(childModel, [...ancestors, node]); if (c != null) node.children.push(c); } } else if ('workspaceId' in childModel && childModel.workspaceId === model.id) { const c = next(childModel, [...ancestors, node]); if (c != null) node.children.push(c); } else { // Do nothing } } return node; }; return next(workspace, []); }, [entries, workspace]); if (tree == null) { return null; } if (!hasAnythingToAdd) { return No changes since last commit; } const checkNode = (treeNode: TreeNode) => { const checked = nodeCheckedStatus(treeNode); const newChecked = checked === 'indeterminate' ? true : !checked; setCheckedAndChildren(treeNode, newChecked, unstage.mutate, add.mutate); // TODO: Also ensure parents are added properly }; return (
(
)} secondSlot={({ style }) => (
{commit.error && {commit.error}} {status.data?.headRefShorthand}
)} />
); } function TreeNodeChildren({ node, depth, onCheck, }: { node: TreeNode | null; depth: number; onCheck: (node: TreeNode, checked: boolean) => void; }) { if (node === null) return null; if (!isNodeRelevant(node)) return null; const checked = nodeCheckedStatus(node); return (
0 && 'pl-1 ml-[10px] border-l border-dashed border-border-subtle', )} >
onCheck(node, checked)} title={
{node.model.model !== 'http_request' && node.model.model !== 'grpc_request' && node.model.model !== 'websocket_request' ? ( ) : ( )}
{fallbackRequestName(node.model)} {/*({node.model.model})*/} {/*({node.status.staged ? 'Y' : 'N'})*/}
{node.status.status !== 'current' && ( {node.status.status} )}
} />
{node.children.map((childNode, i) => { return ( ); })}
); } function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] { let numVisited = 0; let numChecked = 0; let numCurrent = 0; const visitChildren = (n: TreeNode) => { numVisited += 1; if (n.status.status === 'current') { numCurrent += 1; } else if (n.status.staged) { numChecked += 1; } for (const child of n.children) { visitChildren(child); } }; visitChildren(root); if (numVisited === numChecked + numCurrent) { return true; } else if (numChecked === 0) { return false; } else { return 'indeterminate'; } } function setCheckedAndChildren( node: TreeNode, checked: boolean, unstage: (args: { relaPaths: string[] }) => void, add: (args: { relaPaths: string[] }) => void, ) { const toAdd: string[] = []; const toUnstage: string[] = []; const next = (node: TreeNode) => { for (const child of node.children) { next(child); } if (node.status.status === 'current') { // Nothing required } else if (checked && !node.status.staged) { toAdd.push(node.status.relaPath); } else if (!checked && node.status.staged) { toUnstage.push(node.status.relaPath); } }; next(node); if (toAdd.length > 0) add({ relaPaths: toAdd }); if (toUnstage.length > 0) unstage({ relaPaths: toUnstage }); } function isNodeRelevant(node: TreeNode): boolean { if (node.status.status !== 'current') { return true; } // Recursively check children return node.children.some((c) => isNodeRelevant(c)); }