fix(tmux): collapse installer banner by default
This commit is contained in:
parent
51aac9b7a1
commit
80221884ed
2 changed files with 383 additions and 284 deletions
|
|
@ -12,6 +12,8 @@ import {
|
|||
|
||||
import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner';
|
||||
|
||||
const SUMMARY_TITLE = 'tmux is not installed';
|
||||
|
||||
const SourceLink = ({
|
||||
label,
|
||||
url,
|
||||
|
|
@ -35,8 +37,10 @@ const SourceLink = ({
|
|||
export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
||||
const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } =
|
||||
useTmuxInstallerBanner();
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const [manualHintsExpanded, setManualHintsExpanded] = React.useState(false);
|
||||
const previousPhaseRef = React.useRef(viewModel.phase);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!viewModel.acceptsInput) {
|
||||
|
|
@ -50,6 +54,21 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
}
|
||||
}, [viewModel.manualHintsCollapsible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const previousPhase = previousPhaseRef.current;
|
||||
const becameActive =
|
||||
previousPhase === 'idle' &&
|
||||
viewModel.phase !== 'idle' &&
|
||||
viewModel.phase !== 'completed' &&
|
||||
viewModel.phase !== 'cancelled';
|
||||
|
||||
if (becameActive) {
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
previousPhaseRef.current = viewModel.phase;
|
||||
}, [viewModel.phase]);
|
||||
|
||||
if (!viewModel.visible) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -66,9 +85,13 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
borderColor: 'rgba(245, 158, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="min-w-0 max-w-4xl">
|
||||
<div className="flex items-start gap-2 text-base font-semibold leading-6">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
className="flex w-full items-center justify-between gap-3 text-left"
|
||||
>
|
||||
<span className="flex min-w-0 items-start gap-2 text-base font-semibold leading-6">
|
||||
<span className="pt-0.5">
|
||||
{viewModel.error ? (
|
||||
<AlertTriangle className="size-4 text-red-300" />
|
||||
|
|
@ -76,8 +99,26 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
<Wrench className="size-4 text-amber-300" />
|
||||
)}
|
||||
</span>
|
||||
<span>{viewModel.title}</span>
|
||||
<span className="truncate">{SUMMARY_TITLE}</span>
|
||||
</span>
|
||||
{expanded ? (
|
||||
<ChevronUp className="size-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="size-4 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="min-w-0 max-w-4xl">
|
||||
{viewModel.title !== SUMMARY_TITLE && (
|
||||
<div
|
||||
className="text-sm font-medium leading-6"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
{viewModel.title}
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className="mt-2 text-[15px] leading-7"
|
||||
style={{ color: 'var(--color-text-secondary)' }}
|
||||
|
|
@ -235,10 +276,9 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewModel.progressPercent !== null && (
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between text-[11px]">
|
||||
<span style={{ color: 'var(--color-text-muted)' }}>Installer progress</span>
|
||||
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||||
|
|
@ -261,7 +301,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
)}
|
||||
|
||||
{viewModel.acceptsInput && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="space-y-2">
|
||||
<form
|
||||
className="flex flex-col gap-2 sm:flex-row sm:items-center"
|
||||
onSubmit={(event) => {
|
||||
|
|
@ -297,15 +337,15 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
</form>
|
||||
{viewModel.inputSecret && (
|
||||
<div className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
||||
Password input is sent directly to the installer terminal and is not added to the log
|
||||
output.
|
||||
Password input is sent directly to the installer terminal and is not added to
|
||||
the log output.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{manualHintsVisible && (
|
||||
<div className="mt-3 grid gap-2 lg:grid-cols-2">
|
||||
<div className="grid gap-2 lg:grid-cols-2">
|
||||
{viewModel.manualHints.map((hint) => (
|
||||
<div
|
||||
key={`${hint.title}-${hint.command ?? hint.url ?? hint.description}`}
|
||||
|
|
@ -337,7 +377,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
)}
|
||||
|
||||
{(viewModel.logs.length > 0 || viewModel.error) && (
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDetails}
|
||||
|
|
@ -361,5 +401,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,16 +93,32 @@ describe('TmuxInstallerBannerView', () => {
|
|||
it('keeps Windows setup steps collapsed by default and expands them on demand', async () => {
|
||||
const { host, root } = renderBanner(baseViewModel);
|
||||
|
||||
expect(host.textContent).toContain('Show setup steps (2)');
|
||||
expect(host.textContent).toContain('tmux is not installed');
|
||||
expect(host.textContent).not.toContain('WSL is available, but no Linux distribution is installed yet.');
|
||||
expect(host.textContent).not.toContain('Show setup steps (2)');
|
||||
expect(host.textContent).not.toContain('wsl --install --no-distribution');
|
||||
|
||||
const toggleButton = [...host.querySelectorAll('button')].find((button) =>
|
||||
button.textContent?.includes('Show setup steps')
|
||||
const summaryButton = [...host.querySelectorAll('button')].find((button) =>
|
||||
button.textContent?.includes('tmux is not installed')
|
||||
);
|
||||
expect(toggleButton).toBeDefined();
|
||||
expect(summaryButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
toggleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
summaryButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('WSL is available, but no Linux distribution is installed yet.');
|
||||
expect(host.textContent).toContain('Show setup steps (2)');
|
||||
expect(host.textContent).not.toContain('Hide setup steps');
|
||||
|
||||
const setupToggle = [...host.querySelectorAll('button')].find((button) =>
|
||||
button.textContent?.includes('Show setup steps')
|
||||
);
|
||||
expect(setupToggle).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
setupToggle?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
|
|
@ -125,6 +141,15 @@ describe('TmuxInstallerBannerView', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const summaryButton = [...host.querySelectorAll('button')].find((button) =>
|
||||
button.textContent?.includes('tmux is not installed')
|
||||
);
|
||||
expect(summaryButton).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
summaryButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('brew install tmux');
|
||||
expect(host.textContent).not.toContain('Show setup steps');
|
||||
|
||||
|
|
@ -132,4 +157,36 @@ describe('TmuxInstallerBannerView', () => {
|
|||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('auto-expands when installer flow becomes active', async () => {
|
||||
const { host, root } = renderBanner(baseViewModel);
|
||||
|
||||
mockUseTmuxInstallerBanner.mockReturnValue({
|
||||
viewModel: {
|
||||
...baseViewModel,
|
||||
title: 'tmux needs a restart',
|
||||
body: 'Restart Windows before continuing.',
|
||||
phase: 'needs_restart',
|
||||
progressPercent: 96,
|
||||
},
|
||||
install: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
submitInput: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
toggleDetails: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(React.createElement(TmuxInstallerBannerView));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Restart Windows before continuing.');
|
||||
expect(host.textContent).toContain('96%');
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue