Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/native/src/components/DiffViewer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { act, type ReactElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { FileFetcher, GitProvider } from '@codeam/ide-core';

vi.mock('./ResilientWebView', () => ({ ResilientWebView: () => null }));
vi.mock('@expo/vector-icons', () => ({ Ionicons: () => null }));

import { DiffViewer } from './DiffViewer';

let root: Root | null = null;
let container: HTMLDivElement | null = null;

afterEach(async () => {
await act(async () => root?.unmount());
container?.remove();
root = null;
container = null;
});

async function render(element: ReactElement) {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => root!.render(element));
}

const git = { diff: vi.fn().mockResolvedValue(null) } as unknown as GitProvider;
const fetcher = { readFile: vi.fn().mockResolvedValue('x') } as unknown as FileFetcher;

describe('DiffViewer badge (native)', () => {
it('defaults to WORKING TREE', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} />);
expect(container!.textContent).toContain('WORKING TREE');
});

it('shows a caller badge instead — a PR diff is not the working tree', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} badge="PR #212" />);
expect(container!.textContent).toContain('PR #212');
expect(container!.textContent).not.toContain('WORKING TREE');
});

it('hides the badge on null', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} badge={null} />);
expect(container!.textContent).not.toContain('WORKING TREE');
});
});
17 changes: 13 additions & 4 deletions packages/native/src/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ interface Props {
git: GitProvider;
fetcher: FileFetcher;
staged?: boolean;
/**
* Header badge. Omitted → "STAGED" / "WORKING TREE" from `staged`; a string
* replaces it (e.g. "PR #212" — a pull-request diff is neither); `null`
* hides it.
*/
badge?: string | null;
onClose?: () => void;
}

Expand All @@ -40,7 +46,8 @@ interface DiffState {
* viewports because Monaco's DiffEditor handles the rebreak
* automatically when its renderer detects the available width.
*/
export function DiffViewer({ path, git, fetcher, staged, onClose }: Props) {
export function DiffViewer({ path, git, fetcher, staged, badge, onClose }: Props) {
const badgeText = badge === undefined ? (staged ? 'STAGED' : 'WORKING TREE') : badge;
const theme = useIDETheme();
const generationRef = useRef(0);
const [readyGeneration, setReadyGeneration] = useState<number | undefined>(undefined);
Expand Down Expand Up @@ -119,9 +126,11 @@ export function DiffViewer({ path, git, fetcher, staged, onClose }: Props) {
{path}
</Text>
<View style={styles.headerRight}>
<View style={styles.stagedBadge}>
<Text style={styles.stagedBadgeText}>{staged ? 'STAGED' : 'WORKING TREE'}</Text>
</View>
{badgeText ? (
<View style={styles.stagedBadge}>
<Text style={styles.stagedBadgeText}>{badgeText}</Text>
</View>
) : null}
{onClose ? (
<Pressable
onPress={onClose}
Expand Down
47 changes: 47 additions & 0 deletions packages/web/src/components/DiffViewer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { act, type ReactElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { FileFetcher, GitProvider } from '@codeam/ide-core';

vi.mock('@monaco-editor/react', () => ({ DiffEditor: () => null }));

import { DiffViewer } from './DiffViewer';

let root: Root | null = null;
let container: HTMLDivElement | null = null;

afterEach(async () => {
await act(async () => root?.unmount());
container?.remove();
root = null;
container = null;
});

async function render(element: ReactElement) {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => root!.render(element));
}

const git = { diff: vi.fn().mockResolvedValue(null) } as unknown as GitProvider;
const fetcher = { readFile: vi.fn().mockResolvedValue('x') } as unknown as FileFetcher;

describe('DiffViewer badge (web)', () => {
it('defaults to WORKING TREE / STAGED', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} />);
expect(container!.textContent).toContain('WORKING TREE');
});

it('shows a caller badge instead — a PR diff is not the working tree', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} badge="PR #212" />);
expect(container!.textContent).toContain('PR #212');
expect(container!.textContent).not.toContain('WORKING TREE');
});

it('hides the badge on null', async () => {
await render(<DiffViewer path="a.ts" git={git} fetcher={fetcher} badge={null} />);
expect(container!.textContent).not.toContain('WORKING TREE');
expect(container!.textContent).not.toContain('STAGED');
});
});
11 changes: 9 additions & 2 deletions packages/web/src/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ interface Props {
/** When `true`, diff the staged version vs HEAD instead of
* working-tree vs HEAD. */
staged?: boolean;
/**
* Header badge. Omitted → "STAGED" / "WORKING TREE" from `staged`; a string
* replaces it (e.g. "PR #212" — a pull-request diff is neither); `null`
* hides it.
*/
badge?: string | null;
/** Close button handler (rendered top-right when supplied). */
onClose?: () => void;
}
Expand Down Expand Up @@ -47,7 +53,8 @@ interface DiffState {
* specially; for those the viewer renders the raw diff text in a
* read-only Monaco buffer.
*/
export function DiffViewer({ path, git, fetcher, staged, onClose }: Props) {
export function DiffViewer({ path, git, fetcher, staged, badge, onClose }: Props) {
const badgeText = badge === undefined ? (staged ? 'STAGED' : 'WORKING TREE') : badge;
const [state, setState] = useState<DiffState>({
loading: true,
error: null,
Expand Down Expand Up @@ -98,7 +105,7 @@ export function DiffViewer({ path, git, fetcher, staged, onClose }: Props) {
<div className="flex-1 flex flex-col min-w-0 bg-[#0d1117]">
<div className="flex items-center justify-between px-3 py-1 bg-[#161b22] border-b border-gray-800/60 text-[11px]">
<span className="font-mono text-gray-300 truncate flex-1">
{path} <span className="text-gray-500 ml-2">{staged ? 'STAGED' : 'WORKING TREE'}</span>
{path} {badgeText ? <span className="text-gray-500 ml-2">{badgeText}</span> : null}
</span>
{onClose ? (
<button
Expand Down
Loading