fix: better icon buttons for git
This commit is contained in:
@@ -36,6 +36,61 @@
|
|||||||
padding: 0 12px 8px;
|
padding: 0 12px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.git-sidebar-icon-button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-sidebar-icon-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-branch-line,
|
||||||
|
.git-action-stem,
|
||||||
|
.git-action-arrow,
|
||||||
|
.git-action-prune-mark {
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-branch-line {
|
||||||
|
stroke-width: 1.3;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-branch-dot {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-stem {
|
||||||
|
stroke-width: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-stem--dotted {
|
||||||
|
stroke-dasharray: 1.4 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-stem--solid {
|
||||||
|
stroke-dasharray: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-arrow {
|
||||||
|
stroke-width: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-action-prune-mark {
|
||||||
|
stroke-width: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-sidebar-icon-button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.git-sidebar-section {
|
.git-sidebar-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -491,35 +491,93 @@ export const GitSidebar: React.FC = () => {
|
|||||||
<div className="git-sidebar-actions" role="group" aria-label={tr('gitSidebar.aria.repoActions')}>
|
<div className="git-sidebar-actions" role="group" aria-label={tr('gitSidebar.aria.repoActions')}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="git-sidebar-button"
|
className="sidebar-action git-sidebar-icon-button"
|
||||||
onClick={() => handleRepoAction('fetch')}
|
onClick={() => handleRepoAction('fetch')}
|
||||||
disabled={actionLoading !== null}
|
disabled={actionLoading !== null}
|
||||||
|
aria-label={tr('gitSidebar.action.fetch')}
|
||||||
|
title={tr('gitSidebar.action.fetch')}
|
||||||
>
|
>
|
||||||
{actionLoading === 'fetch' ? tr('gitSidebar.action.fetching') : tr('gitSidebar.action.fetch')}
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
data-testid="git-action-icon-fetch"
|
||||||
|
>
|
||||||
|
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
|
||||||
|
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
|
||||||
|
<line x1="8" y1="4.5" x2="8" y2="10.2" className="git-action-stem git-action-stem--dotted" data-testid="git-action-stem-fetch" />
|
||||||
|
<path d="M8 10.2l-2-2" className="git-action-arrow git-action-arrow--towards-branch" data-testid="git-action-arrow-fetch" />
|
||||||
|
<path d="M8 10.2l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="git-sidebar-button"
|
className="sidebar-action git-sidebar-icon-button"
|
||||||
onClick={() => handleRepoAction('pull')}
|
onClick={() => handleRepoAction('pull')}
|
||||||
disabled={actionLoading !== null}
|
disabled={actionLoading !== null}
|
||||||
|
aria-label={tr('gitSidebar.action.pull')}
|
||||||
|
title={tr('gitSidebar.action.pull')}
|
||||||
>
|
>
|
||||||
{actionLoading === 'pull' ? tr('gitSidebar.action.pulling') : tr('gitSidebar.action.pull')}
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
data-testid="git-action-icon-pull"
|
||||||
|
>
|
||||||
|
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
|
||||||
|
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
|
||||||
|
<line x1="8" y1="4.5" x2="8" y2="10.2" className="git-action-stem git-action-stem--solid" data-testid="git-action-stem-pull" />
|
||||||
|
<path d="M8 10.2l-2-2" className="git-action-arrow git-action-arrow--towards-branch" data-testid="git-action-arrow-pull" />
|
||||||
|
<path d="M8 10.2l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="git-sidebar-button"
|
className="sidebar-action git-sidebar-icon-button"
|
||||||
onClick={() => handleRepoAction('push')}
|
onClick={() => handleRepoAction('push')}
|
||||||
disabled={actionLoading !== null}
|
disabled={actionLoading !== null}
|
||||||
|
aria-label={tr('gitSidebar.action.push')}
|
||||||
|
title={tr('gitSidebar.action.push')}
|
||||||
>
|
>
|
||||||
{actionLoading === 'push' ? tr('gitSidebar.action.pushing') : tr('gitSidebar.action.push')}
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
data-testid="git-action-icon-push"
|
||||||
|
>
|
||||||
|
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
|
||||||
|
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
|
||||||
|
<line x1="8" y1="10.2" x2="8" y2="4.5" className="git-action-stem git-action-stem--solid" data-testid="git-action-stem-push" />
|
||||||
|
<path d="M8 4.5l-2 2" className="git-action-arrow git-action-arrow--away-branch" data-testid="git-action-arrow-push" />
|
||||||
|
<path d="M8 4.5l2 2" className="git-action-arrow git-action-arrow--away-branch" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="git-sidebar-button"
|
className="sidebar-action git-sidebar-icon-button"
|
||||||
onClick={() => handleRepoAction('prune-lfs')}
|
onClick={() => handleRepoAction('prune-lfs')}
|
||||||
disabled={actionLoading !== null}
|
disabled={actionLoading !== null}
|
||||||
|
aria-label={tr('gitSidebar.action.pruneLfs')}
|
||||||
|
title={tr('gitSidebar.action.pruneLfs')}
|
||||||
>
|
>
|
||||||
{actionLoading === 'prune-lfs' ? tr('gitSidebar.action.pruning') : tr('gitSidebar.action.pruneLfs')}
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false">
|
||||||
|
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" />
|
||||||
|
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" />
|
||||||
|
<line x1="8" y1="4.5" x2="8" y2="9.8" className="git-action-stem git-action-stem--solid" />
|
||||||
|
<path d="M8 9.8l-2-2" className="git-action-arrow git-action-arrow--towards-branch" />
|
||||||
|
<path d="M8 9.8l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
|
||||||
|
<path d="M10.8 4.2l2 2" className="git-action-prune-mark" />
|
||||||
|
<path d="M12.8 4.2l-2 2" className="git-action-prune-mark" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{actionLoading && (
|
{actionLoading && (
|
||||||
|
|||||||
@@ -661,6 +661,63 @@ describe('GitSidebar', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders repo actions as icon-only buttons with hover tooltips', async () => {
|
||||||
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
|
isRepo: true,
|
||||||
|
rootPath: '/repo/path',
|
||||||
|
currentBranch: 'main',
|
||||||
|
hasRemote: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<GitSidebar />);
|
||||||
|
|
||||||
|
const repoActions = await screen.findByRole('group', { name: /repository actions/i });
|
||||||
|
const fetchButton = within(repoActions).getByRole('button', { name: /^fetch$/i });
|
||||||
|
const pullButton = within(repoActions).getByRole('button', { name: /^pull$/i });
|
||||||
|
const pushButton = within(repoActions).getByRole('button', { name: /^push$/i });
|
||||||
|
const pruneButton = within(repoActions).getByRole('button', { name: /^prune lfs$/i });
|
||||||
|
|
||||||
|
expect(fetchButton).toHaveAttribute('title', 'Fetch');
|
||||||
|
expect(pullButton).toHaveAttribute('title', 'Pull');
|
||||||
|
expect(pushButton).toHaveAttribute('title', 'Push');
|
||||||
|
expect(pruneButton).toHaveAttribute('title', 'Prune LFS');
|
||||||
|
|
||||||
|
expect(within(repoActions).queryByText('Fetch')).not.toBeInTheDocument();
|
||||||
|
expect(within(repoActions).queryByText('Pull')).not.toBeInTheDocument();
|
||||||
|
expect(within(repoActions).queryByText('Push')).not.toBeInTheDocument();
|
||||||
|
expect(within(repoActions).queryByText('Prune LFS')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses unified branch baseline icons with action-specific arrow direction styles', async () => {
|
||||||
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
|
isRepo: true,
|
||||||
|
rootPath: '/repo/path',
|
||||||
|
currentBranch: 'main',
|
||||||
|
hasRemote: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<GitSidebar />);
|
||||||
|
|
||||||
|
const repoActions = await screen.findByRole('group', { name: /repository actions/i });
|
||||||
|
const fetchButton = within(repoActions).getByRole('button', { name: /^fetch$/i });
|
||||||
|
const pullButton = within(repoActions).getByRole('button', { name: /^pull$/i });
|
||||||
|
const pushButton = within(repoActions).getByRole('button', { name: /^push$/i });
|
||||||
|
|
||||||
|
const fetchIcon = within(fetchButton).getByTestId('git-action-icon-fetch');
|
||||||
|
expect(within(fetchIcon).getByTestId('git-action-branch-line')).toBeInTheDocument();
|
||||||
|
expect(within(fetchIcon).getByTestId('git-action-branch-dot')).toBeInTheDocument();
|
||||||
|
expect(within(fetchIcon).getByTestId('git-action-stem-fetch')).toHaveClass('git-action-stem--dotted');
|
||||||
|
expect(within(fetchIcon).getByTestId('git-action-arrow-fetch')).toHaveClass('git-action-arrow--towards-branch');
|
||||||
|
|
||||||
|
const pullIcon = within(pullButton).getByTestId('git-action-icon-pull');
|
||||||
|
expect(within(pullIcon).getByTestId('git-action-stem-pull')).toHaveClass('git-action-stem--solid');
|
||||||
|
expect(within(pullIcon).getByTestId('git-action-arrow-pull')).toHaveClass('git-action-arrow--towards-branch');
|
||||||
|
|
||||||
|
const pushIcon = within(pushButton).getByTestId('git-action-icon-push');
|
||||||
|
expect(within(pushIcon).getByTestId('git-action-stem-push')).toHaveClass('git-action-stem--solid');
|
||||||
|
expect(within(pushIcon).getByTestId('git-action-arrow-push')).toHaveClass('git-action-arrow--away-branch');
|
||||||
|
});
|
||||||
|
|
||||||
it('shows in-progress feedback while prune lfs is running', async () => {
|
it('shows in-progress feedback while prune lfs is running', async () => {
|
||||||
let resolvePrune: ((value: { success: boolean; dryRun: boolean; verifyRemote: boolean; recentCommitsToKeep: number }) => void) | null = null;
|
let resolvePrune: ((value: { success: boolean; dryRun: boolean; verifyRemote: boolean; recentCommitsToKeep: number }) => void) | null = null;
|
||||||
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
|
||||||
@@ -684,7 +741,7 @@ describe('GitSidebar', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByRole('status')).toHaveTextContent(/pruning local git lfs cache/i);
|
expect(screen.getByRole('status')).toHaveTextContent(/pruning local git lfs cache/i);
|
||||||
expect(screen.getByRole('button', { name: /pruning/i })).toBeDisabled();
|
expect(screen.getByRole('button', { name: /prune lfs/i })).toBeDisabled();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
resolvePrune?.({ success: true, dryRun: false, verifyRemote: true, recentCommitsToKeep: 2 });
|
resolvePrune?.({ success: true, dryRun: false, verifyRemote: true, recentCommitsToKeep: 2 });
|
||||||
@@ -818,7 +875,7 @@ describe('GitSidebar', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByRole('status')).toHaveTextContent(/pushing commits to remote/i);
|
expect(screen.getByRole('status')).toHaveTextContent(/pushing commits to remote/i);
|
||||||
expect(screen.getByRole('button', { name: /pushing/i })).toBeDisabled();
|
expect(screen.getByRole('button', { name: /^push$/i })).toBeDisabled();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
resolvePush?.({ success: true });
|
resolvePush?.({ success: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user