mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-14 00:16:32 +00:00
Add line reorder move mode and fix GitWorktree tests (#154)
* fix(test): replace vi.mocked with direct mock reference in GitWorktree tests vi.mocked(execSync) was undefined because the vi.mock factory runs before imports resolve. Use a top-level vi.fn() reference instead. * feat(tui): add move mode to LineSelector for reordering lines Press 'm' to enter move mode, use arrow keys to swap lines, Enter/Escape to exit. Visual feedback mirrors ItemsEditor pattern with blue diamond marker and [MOVE MODE] label. * chore: update bun.lock * Fix for model in statusLine JSON being passed as string vs object, closes #157, version bump * fix(test): make GitWorktree execSync mock hoist-safe --------- Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
This commit is contained in:
1 parent
d457e55913
commit
879255523b
3 files changed
+73
-24
No files matched your search
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ccstatusline",
|
||||
|
||||
@@ -40,6 +40,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
}) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [moveMode, setMoveMode] = useState(false);
|
||||
const [localLines, setLocalLines] = useState(lines);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,6 +91,33 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveMode) {
|
||||
if (key.upArrow && selectedIndex > 0) {
|
||||
const newLines = [...localLines];
|
||||
const temp = newLines[selectedIndex];
|
||||
const prev = newLines[selectedIndex - 1];
|
||||
if (temp && prev) {
|
||||
[newLines[selectedIndex], newLines[selectedIndex - 1]] = [prev, temp];
|
||||
}
|
||||
setLocalLines(newLines);
|
||||
onLinesUpdate(newLines);
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
} else if (key.downArrow && selectedIndex < localLines.length - 1) {
|
||||
const newLines = [...localLines];
|
||||
const temp = newLines[selectedIndex];
|
||||
const next = newLines[selectedIndex + 1];
|
||||
if (temp && next) {
|
||||
[newLines[selectedIndex], newLines[selectedIndex + 1]] = [next, temp];
|
||||
}
|
||||
setLocalLines(newLines);
|
||||
onLinesUpdate(newLines);
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
} else if (key.escape || key.return) {
|
||||
setMoveMode(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (input) {
|
||||
case 'a':
|
||||
if (allowEditing) {
|
||||
@@ -101,6 +129,11 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
setShowDeleteDialog(true);
|
||||
}
|
||||
return;
|
||||
case 'm':
|
||||
if (allowEditing && localLines.length > 1 && selectedIndex < localLines.length) {
|
||||
setMoveMode(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
@@ -198,17 +231,27 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection='column'>
|
||||
<Text bold>{title ?? 'Select Line to Edit'}</Text>
|
||||
<Box>
|
||||
<Text bold>
|
||||
{title ?? 'Select Line to Edit'}
|
||||
{' '}
|
||||
</Text>
|
||||
{moveMode && <Text color='blue'>[MOVE MODE]</Text>}
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Choose which status line to configure
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{allowEditing ? (
|
||||
localLines.length > 1
|
||||
? '(a) to append new line, (d) to delete line, ESC to go back'
|
||||
: '(a) to append new line, ESC to go back'
|
||||
) : 'ESC to go back'}
|
||||
</Text>
|
||||
{moveMode ? (
|
||||
<Text dimColor>↑↓ to move line, ESC or Enter to exit move mode</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
{allowEditing ? (
|
||||
localLines.length > 1
|
||||
? '(a) to append new line, (d) to delete line, (m) to move line, ESC to go back'
|
||||
: '(a) to append new line, ESC to go back'
|
||||
) : 'ESC to go back'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{localLines.map((line, index) => {
|
||||
@@ -219,8 +262,8 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text color={isSelected ? 'green' : undefined}>
|
||||
<Text>{isSelected ? '▶ ' : ' '}</Text>
|
||||
<Text color={isSelected ? (moveMode ? 'blue' : 'green') : undefined}>
|
||||
<Text>{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}</Text>
|
||||
<Text>
|
||||
<Text>
|
||||
☰ Line
|
||||
@@ -239,12 +282,14 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
);
|
||||
})}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === localLines.length ? 'green' : undefined}>
|
||||
{selectedIndex === localLines.length ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
{!moveMode && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === localLines.length ? 'green' : undefined}>
|
||||
{selectedIndex === localLines.length ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
@@ -7,13 +6,17 @@ import {
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import type {
|
||||
RenderContext,
|
||||
WidgetItem
|
||||
} from '../../types';
|
||||
import { GitWorktreeWidget } from '../GitWorktree';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
const mockExecSync = vi.mocked(execSync);
|
||||
|
||||
function render(rawValue = false, isPreview = false) {
|
||||
const widget = new GitWorktreeWidget();
|
||||
@@ -47,32 +50,32 @@ describe('GitWorktreeWidget', () => {
|
||||
});
|
||||
|
||||
it('should render with worktree', () => {
|
||||
vi.mocked(execSync).mockReturnValue('/some/path/.git/worktrees/some-worktree');
|
||||
mockExecSync.mockReturnValue('/some/path/.git/worktrees/some-worktree');
|
||||
|
||||
expect(render()).toBe('𖠰 some-worktree');
|
||||
});
|
||||
|
||||
it('should render with nested worktree', () => {
|
||||
vi.mocked(execSync).mockReturnValue('/some/path/.git/worktrees/some-dir/some-worktree');
|
||||
mockExecSync.mockReturnValue('/some/path/.git/worktrees/some-dir/some-worktree');
|
||||
|
||||
expect(render()).toBe('𖠰 some-dir/some-worktree');
|
||||
});
|
||||
|
||||
it('should render with no worktree', () => {
|
||||
vi.mocked(execSync).mockReturnValue('.git');
|
||||
mockExecSync.mockReturnValue('.git');
|
||||
|
||||
expect(render()).toBe('𖠰 main');
|
||||
});
|
||||
|
||||
it('should render with no git', () => {
|
||||
vi.mocked(execSync).mockRejectedValue(new Error('No git'));
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('𖠰 no git');
|
||||
});
|
||||
|
||||
it('should render with invalid git dir', () => {
|
||||
vi.mocked(execSync).mockReturnValue('');
|
||||
mockExecSync.mockReturnValue('');
|
||||
|
||||
expect(render()).toBe('𖠰 no git');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user