mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-08 20:57:51 +00:00
Merge pull request #1563 from BradGroux/fix/configured-shortcut-help-1534
fix: derive shortcut help from board configuration and platform
This commit is contained in:
commit
103641bfd0
3 changed files with 118 additions and 29 deletions
80
web/src/__tests__/keyboard-shortcut-help.test.tsx
Normal file
80
web/src/__tests__/keyboard-shortcut-help.test.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, screen, within } from '@testing-library/react';
|
||||
import { DEFAULT_FEATURE_SETTINGS } from '@veritas-kanban/shared';
|
||||
import { KeyboardProvider } from '@/hooks/useKeyboard';
|
||||
import { KeyboardShortcutsDialog } from '@/components/layout/KeyboardShortcutsDialog';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
const config = vi.hoisted(() => ({
|
||||
settings: { board: { columns: [] as Array<{ id: string; title: string }> } },
|
||||
}));
|
||||
vi.mock('@/hooks/useFeatureSettings', () => ({ useFeatureSettings: () => config }));
|
||||
|
||||
function Surface() {
|
||||
return (
|
||||
<KeyboardProvider>
|
||||
<KeyboardShortcutsDialog />
|
||||
</KeyboardProvider>
|
||||
);
|
||||
}
|
||||
function keyFor(label: string) {
|
||||
const description = screen.getByText(`Move to ${label}`);
|
||||
const row = description.parentElement;
|
||||
if (!row) throw new Error('Shortcut description has no row');
|
||||
return within(row).getByText(/^\d$/).textContent;
|
||||
}
|
||||
describe('configured shortcut help', () => {
|
||||
beforeEach(() => {
|
||||
config.settings.board.columns = [...DEFAULT_FEATURE_SETTINGS.board.columns];
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('matches default destinations and updates rename, order, additions and removals while open', async () => {
|
||||
const view = renderWithProviders(<Surface />);
|
||||
fireEvent.keyDown(window, { key: '?' });
|
||||
await screen.findByRole('dialog');
|
||||
for (const [i, column] of DEFAULT_FEATURE_SETTINGS.board.columns.entries()) {
|
||||
expect(keyFor(column.title)).toBe(String(i + 1));
|
||||
}
|
||||
expect(screen.queryByText('Move to Planning')).toBeNull();
|
||||
config.settings.board.columns = [
|
||||
{ id: 'done', title: 'Complete' },
|
||||
{ id: 'ready', title: 'Ready' },
|
||||
{ id: 'todo', title: 'To Do' },
|
||||
];
|
||||
view.rerender(<Surface />);
|
||||
expect(keyFor('Complete')).toBe('1');
|
||||
expect(keyFor('Ready')).toBe('2');
|
||||
expect(keyFor('To Do')).toBe('3');
|
||||
expect(screen.queryByText('Move to In Progress')).toBeNull();
|
||||
expect(screen.queryByText('Move to Done')).toBeNull();
|
||||
});
|
||||
|
||||
it('lists only nine numeric destinations and explains the limit', async () => {
|
||||
config.settings.board.columns = Array.from({ length: 11 }, (_, i) => ({
|
||||
id: `stage-${i}`,
|
||||
title: `Stage ${i + 1}`,
|
||||
}));
|
||||
renderWithProviders(<Surface />);
|
||||
fireEvent.keyDown(window, { key: '?' });
|
||||
await screen.findByRole('dialog');
|
||||
expect(keyFor('Stage 9')).toBe('9');
|
||||
expect(screen.queryByText('Move to Stage 10')).toBeNull();
|
||||
expect(screen.getByText(/Number shortcuts cover the first nine columns/)).toBeDefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['MacIntel', '⌘⇧C'],
|
||||
['Win32', 'Ctrl+Shift+C'],
|
||||
['Linux x86_64', 'Ctrl+Shift+C'],
|
||||
])('labels chat modifiers for %s', async (platform, label) => {
|
||||
vi.spyOn(navigator, 'platform', 'get').mockReturnValue(platform);
|
||||
renderWithProviders(<Surface />);
|
||||
fireEvent.keyDown(window, { key: '?' });
|
||||
await screen.findByRole('dialog');
|
||||
expect(screen.getByText(label)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,34 +8,6 @@ interface Shortcut {
|
|||
description: string;
|
||||
}
|
||||
|
||||
const shortcuts: { category: string; items: Shortcut[] }[] = [
|
||||
{
|
||||
category: 'Navigation',
|
||||
items: [
|
||||
{ keys: ['j', '↓'], description: 'Select next task' },
|
||||
{ keys: ['k', '↑'], description: 'Select previous task' },
|
||||
{ keys: ['Enter'], description: 'Open selected task' },
|
||||
{ keys: ['Esc'], description: 'Close panel / Clear selection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Actions',
|
||||
items: [
|
||||
{ keys: ['c'], description: 'Create new task' },
|
||||
{ keys: ['⌘⇧C'], description: 'Open agent chat' },
|
||||
{ keys: ['1'], description: 'Move to To Do' },
|
||||
{ keys: ['2'], description: 'Move to Planning' },
|
||||
{ keys: ['3'], description: 'Move to In Progress' },
|
||||
{ keys: ['4'], description: 'Move to Blocked' },
|
||||
{ keys: ['5'], description: 'Move to Done' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'General',
|
||||
items: [{ keys: ['?'], description: 'Toggle this help' }],
|
||||
},
|
||||
];
|
||||
|
||||
function KeyBadge({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Kbd className="inline-flex min-w-[24px] items-center justify-center px-2 text-xs font-medium">
|
||||
|
|
@ -45,7 +17,34 @@ function KeyBadge({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
|
||||
export function KeyboardShortcutsDialog() {
|
||||
const { isHelpOpen, closeHelpDialog } = useKeyboard();
|
||||
const { isHelpOpen, closeHelpDialog, columns } = useKeyboard();
|
||||
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
||||
const shortcuts: { category: string; items: Shortcut[] }[] = [
|
||||
{
|
||||
category: 'Navigation',
|
||||
items: [
|
||||
{ keys: ['j', '↓'], description: 'Select next task' },
|
||||
{ keys: ['k', '↑'], description: 'Select previous task' },
|
||||
{ keys: ['Enter'], description: 'Open selected task' },
|
||||
{ keys: ['Esc'], description: 'Close panel / Clear selection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Actions',
|
||||
items: [
|
||||
{ keys: ['c'], description: 'Create new task' },
|
||||
{ keys: [isMac ? '⌘⇧C' : 'Ctrl+Shift+C'], description: 'Open agent chat' },
|
||||
...columns.slice(0, 9).map((column, index) => ({
|
||||
keys: [String(index + 1)],
|
||||
description: `Move to ${column.title}`,
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'General',
|
||||
items: [{ keys: ['?'], description: 'Toggle this help' }],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -87,6 +86,12 @@ export function KeyboardShortcutsDialog() {
|
|||
</dl>
|
||||
</section>
|
||||
))}
|
||||
{columns.length > 9 && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Number shortcuts cover the first nine columns. Use the card status control for other
|
||||
columns.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<OverlayFooter>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ interface KeyboardContextValue {
|
|||
selectedTaskId: string | null;
|
||||
setSelectedTaskId: (id: string | null) => void;
|
||||
|
||||
columns: ReturnType<typeof normalizeBoardColumns>;
|
||||
|
||||
// Task list for navigation
|
||||
tasks: Task[];
|
||||
setTasks: (tasks: Task[]) => void;
|
||||
|
|
@ -253,6 +255,7 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
|
|||
isHelpOpen,
|
||||
selectedTaskId,
|
||||
setSelectedTaskId,
|
||||
columns,
|
||||
tasks,
|
||||
setTasks,
|
||||
setOnOpenTask,
|
||||
|
|
@ -268,6 +271,7 @@ export function KeyboardProvider({ children }: { children: ReactNode }) {
|
|||
isHelpOpen,
|
||||
selectedTaskId,
|
||||
setSelectedTaskId,
|
||||
columns,
|
||||
tasks,
|
||||
setTasks,
|
||||
setOnOpenTask,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue