mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 06:54:15 -07:00
feat(plan): create generic Checklist component and refactor Todo (#17741)
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '../../test-utils/render.js';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Checklist } from './Checklist.js';
|
||||||
|
import type { ChecklistItemData } from './ChecklistItem.js';
|
||||||
|
|
||||||
|
describe('<Checklist />', () => {
|
||||||
|
const items: ChecklistItemData[] = [
|
||||||
|
{ status: 'completed', label: 'Task 1' },
|
||||||
|
{ status: 'in_progress', label: 'Task 2' },
|
||||||
|
{ status: 'pending', label: 'Task 3' },
|
||||||
|
{ status: 'cancelled', label: 'Task 4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('renders nothing when list is empty', () => {
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Checklist title="Test List" items={[]} isExpanded={true} />,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders nothing when collapsed and no active items', () => {
|
||||||
|
const inactiveItems: ChecklistItemData[] = [
|
||||||
|
{ status: 'completed', label: 'Task 1' },
|
||||||
|
{ status: 'cancelled', label: 'Task 2' },
|
||||||
|
];
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Checklist title="Test List" items={inactiveItems} isExpanded={false} />,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders summary view correctly (collapsed)', () => {
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Checklist
|
||||||
|
title="Test List"
|
||||||
|
items={items}
|
||||||
|
isExpanded={false}
|
||||||
|
toggleHint="toggle me"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders expanded view correctly', () => {
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Checklist
|
||||||
|
title="Test List"
|
||||||
|
items={items}
|
||||||
|
isExpanded={true}
|
||||||
|
toggleHint="toggle me"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders summary view without in-progress item if none exists', () => {
|
||||||
|
const pendingItems: ChecklistItemData[] = [
|
||||||
|
{ status: 'completed', label: 'Task 1' },
|
||||||
|
{ status: 'pending', label: 'Task 2' },
|
||||||
|
];
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Checklist title="Test List" items={pendingItems} isExpanded={false} />,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { theme } from '../semantic-colors.js';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||||
|
|
||||||
|
export interface ChecklistProps {
|
||||||
|
title: string;
|
||||||
|
items: ChecklistItemData[];
|
||||||
|
isExpanded: boolean;
|
||||||
|
toggleHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChecklistTitleDisplay: React.FC<{
|
||||||
|
title: string;
|
||||||
|
items: ChecklistItemData[];
|
||||||
|
toggleHint?: string;
|
||||||
|
}> = ({ title, items, toggleHint }) => {
|
||||||
|
const score = useMemo(() => {
|
||||||
|
let total = 0;
|
||||||
|
let completed = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.status !== 'cancelled') {
|
||||||
|
total += 1;
|
||||||
|
if (item.status === 'completed') {
|
||||||
|
completed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${completed}/${total} completed`;
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" columnGap={2} height={1}>
|
||||||
|
<Text color={theme.text.primary} bold aria-label={`${title} list`}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
{score}
|
||||||
|
{toggleHint ? ` (${toggleHint})` : ''}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChecklistListDisplay: React.FC<{ items: ChecklistItemData[] }> = ({
|
||||||
|
items,
|
||||||
|
}) => (
|
||||||
|
<Box flexDirection="column" aria-role="list">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<ChecklistItem
|
||||||
|
item={item}
|
||||||
|
key={`${index}-${item.label}`}
|
||||||
|
role="listitem"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Checklist: React.FC<ChecklistProps> = ({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
isExpanded,
|
||||||
|
toggleHint,
|
||||||
|
}) => {
|
||||||
|
const inProgress: ChecklistItemData | null = useMemo(
|
||||||
|
() => items.find((item) => item.status === 'in_progress') || null,
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasActiveItems = useMemo(
|
||||||
|
() =>
|
||||||
|
items.some(
|
||||||
|
(item) => item.status === 'pending' || item.status === 'in_progress',
|
||||||
|
),
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length === 0 || (!isExpanded && !hasActiveItems)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderStyle="single"
|
||||||
|
borderBottom={false}
|
||||||
|
borderRight={false}
|
||||||
|
borderLeft={false}
|
||||||
|
borderColor={theme.border.default}
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<Box flexDirection="column" rowGap={1}>
|
||||||
|
<ChecklistTitleDisplay
|
||||||
|
title={title}
|
||||||
|
items={items}
|
||||||
|
toggleHint={toggleHint}
|
||||||
|
/>
|
||||||
|
<ChecklistListDisplay items={items} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box flexDirection="row" columnGap={1} height={1}>
|
||||||
|
<Box flexShrink={0} flexGrow={0}>
|
||||||
|
<ChecklistTitleDisplay
|
||||||
|
title={title}
|
||||||
|
items={items}
|
||||||
|
toggleHint={toggleHint}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{inProgress && (
|
||||||
|
<Box flexShrink={1} flexGrow={1}>
|
||||||
|
<ChecklistItem item={inProgress} wrap="truncate" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '../../test-utils/render.js';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js';
|
||||||
|
import { Box } from 'ink';
|
||||||
|
|
||||||
|
describe('<ChecklistItem />', () => {
|
||||||
|
it.each([
|
||||||
|
{ status: 'pending', label: 'Do this' },
|
||||||
|
{ status: 'in_progress', label: 'Doing this' },
|
||||||
|
{ status: 'completed', label: 'Done this' },
|
||||||
|
{ status: 'cancelled', label: 'Skipped this' },
|
||||||
|
] as ChecklistItemData[])('renders %s item correctly', (item) => {
|
||||||
|
const { lastFrame } = render(<ChecklistItem item={item} />);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates long text when wrap="truncate"', () => {
|
||||||
|
const item: ChecklistItemData = {
|
||||||
|
status: 'in_progress',
|
||||||
|
label:
|
||||||
|
'This is a very long text that should be truncated because the wrap prop is set to truncate',
|
||||||
|
};
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Box width={30}>
|
||||||
|
<ChecklistItem item={item} wrap="truncate" />
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps long text by default', () => {
|
||||||
|
const item: ChecklistItemData = {
|
||||||
|
status: 'in_progress',
|
||||||
|
label:
|
||||||
|
'This is a very long text that should wrap because the default behavior is wrapping',
|
||||||
|
};
|
||||||
|
const { lastFrame } = render(
|
||||||
|
<Box width={30}>
|
||||||
|
<ChecklistItem item={item} />
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { theme } from '../semantic-colors.js';
|
||||||
|
import { checkExhaustive } from '../../utils/checks.js';
|
||||||
|
|
||||||
|
export type ChecklistStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'in_progress'
|
||||||
|
| 'completed'
|
||||||
|
| 'cancelled';
|
||||||
|
|
||||||
|
export interface ChecklistItemData {
|
||||||
|
status: ChecklistStatus;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChecklistStatusDisplay: React.FC<{ status: ChecklistStatus }> = ({
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return (
|
||||||
|
<Text color={theme.status.success} aria-label="Completed">
|
||||||
|
✓
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
case 'in_progress':
|
||||||
|
return (
|
||||||
|
<Text color={theme.text.accent} aria-label="In Progress">
|
||||||
|
»
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
case 'pending':
|
||||||
|
return (
|
||||||
|
<Text color={theme.text.secondary} aria-label="Pending">
|
||||||
|
☐
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
case 'cancelled':
|
||||||
|
return (
|
||||||
|
<Text color={theme.status.error} aria-label="Cancelled">
|
||||||
|
✗
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
checkExhaustive(status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ChecklistItemProps {
|
||||||
|
item: ChecklistItemData;
|
||||||
|
wrap?: 'truncate';
|
||||||
|
role?: 'listitem';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChecklistItem: React.FC<ChecklistItemProps> = ({
|
||||||
|
item,
|
||||||
|
wrap,
|
||||||
|
role: ariaRole,
|
||||||
|
}) => {
|
||||||
|
const textColor = (() => {
|
||||||
|
switch (item.status) {
|
||||||
|
case 'in_progress':
|
||||||
|
return theme.text.accent;
|
||||||
|
case 'completed':
|
||||||
|
case 'cancelled':
|
||||||
|
return theme.text.secondary;
|
||||||
|
case 'pending':
|
||||||
|
return theme.text.primary;
|
||||||
|
default:
|
||||||
|
checkExhaustive(item.status);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const strikethrough = item.status === 'cancelled';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" columnGap={1} aria-role={ariaRole}>
|
||||||
|
<ChecklistStatusDisplay status={item.status} />
|
||||||
|
<Box flexShrink={1}>
|
||||||
|
<Text color={textColor} wrap={wrap} strikethrough={strikethrough}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<Checklist /> > renders expanded view correctly 1`] = `
|
||||||
|
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Test List 1/3 completed (toggle me)
|
||||||
|
|
||||||
|
✓ Task 1
|
||||||
|
» Task 2
|
||||||
|
☐ Task 3
|
||||||
|
✗ Task 4"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Checklist /> > renders summary view correctly (collapsed) 1`] = `
|
||||||
|
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Test List 1/3 completed (toggle me) » Task 2"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Checklist /> > renders summary view without in-progress item if none exists 1`] = `
|
||||||
|
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Test List 1/2 completed"
|
||||||
|
`;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > renders { status: 'cancelled', label: 'Skipped this' } item correctly 1`] = `"✗ Skipped this"`;
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > renders { status: 'completed', label: 'Done this' } item correctly 1`] = `"✓ Done this"`;
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > renders { status: 'in_progress', label: 'Doing this' } item correctly 1`] = `"» Doing this"`;
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > renders { status: 'pending', label: 'Do this' } item correctly 1`] = `"☐ Do this"`;
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > truncates long text when wrap="truncate" 1`] = `"» This is a very long text th…"`;
|
||||||
|
|
||||||
|
exports[`<ChecklistItem /> > wraps long text by default 1`] = `
|
||||||
|
"» This is a very long text
|
||||||
|
that should wrap because the
|
||||||
|
default behavior is wrapping"
|
||||||
|
`;
|
||||||
@@ -5,101 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { type TodoList } from '@google/gemini-cli-core';
|
||||||
import {
|
|
||||||
type Todo,
|
|
||||||
type TodoList,
|
|
||||||
type TodoStatus,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
|
||||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { HistoryItemToolGroup } from '../../types.js';
|
import type { HistoryItemToolGroup } from '../../types.js';
|
||||||
|
import { Checklist } from '../Checklist.js';
|
||||||
const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
|
import type { ChecklistItemData } from '../ChecklistItem.js';
|
||||||
const score = useMemo(() => {
|
|
||||||
let total = 0;
|
|
||||||
let completed = 0;
|
|
||||||
for (const todo of todos.todos) {
|
|
||||||
if (todo.status !== 'cancelled') {
|
|
||||||
total += 1;
|
|
||||||
if (todo.status === 'completed') {
|
|
||||||
completed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${completed}/${total} completed`;
|
|
||||||
}, [todos]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection="row" columnGap={2} height={1}>
|
|
||||||
<Text color={theme.text.primary} bold aria-label="Todo list">
|
|
||||||
Todo
|
|
||||||
</Text>
|
|
||||||
<Text color={theme.text.secondary}>{score} (ctrl+t to toggle)</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return (
|
|
||||||
<Text color={theme.status.success} aria-label="Completed">
|
|
||||||
✓
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
case 'in_progress':
|
|
||||||
return (
|
|
||||||
<Text color={theme.text.accent} aria-label="In Progress">
|
|
||||||
»
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
case 'pending':
|
|
||||||
return (
|
|
||||||
<Text color={theme.text.secondary} aria-label="Pending">
|
|
||||||
☐
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
case 'cancelled':
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Text color={theme.status.error} aria-label="Cancelled">
|
|
||||||
✗
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const TodoItemDisplay: React.FC<{
|
|
||||||
todo: Todo;
|
|
||||||
wrap?: 'truncate';
|
|
||||||
role?: 'listitem';
|
|
||||||
}> = ({ todo, wrap, role: ariaRole }) => {
|
|
||||||
const textColor = (() => {
|
|
||||||
switch (todo.status) {
|
|
||||||
case 'in_progress':
|
|
||||||
return theme.text.accent;
|
|
||||||
case 'completed':
|
|
||||||
case 'cancelled':
|
|
||||||
return theme.text.secondary;
|
|
||||||
default:
|
|
||||||
return theme.text.primary;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
const strikethrough = todo.status === 'cancelled';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection="row" columnGap={1} aria-role={ariaRole}>
|
|
||||||
<TodoStatusDisplay status={todo.status} />
|
|
||||||
<Box flexShrink={1}>
|
|
||||||
<Text color={textColor} wrap={wrap} strikethrough={strikethrough}>
|
|
||||||
{todo.description}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TodoTray: React.FC = () => {
|
export const TodoTray: React.FC = () => {
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
@@ -125,68 +36,26 @@ export const TodoTray: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}, [uiState.history]);
|
}, [uiState.history]);
|
||||||
|
|
||||||
const inProgress: Todo | null = useMemo(() => {
|
const checklistItems: ChecklistItemData[] = useMemo(() => {
|
||||||
if (todos === null) {
|
if (!todos || !todos.todos) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
|
return todos.todos.map((todo) => ({
|
||||||
|
status: todo.status,
|
||||||
|
label: todo.description,
|
||||||
|
}));
|
||||||
}, [todos]);
|
}, [todos]);
|
||||||
|
|
||||||
const hasActiveTodos = useMemo(() => {
|
if (!todos || !todos.todos) {
|
||||||
if (!todos || !todos.todos) return false;
|
|
||||||
return todos.todos.some(
|
|
||||||
(todo) => todo.status === 'pending' || todo.status === 'in_progress',
|
|
||||||
);
|
|
||||||
}, [todos]);
|
|
||||||
|
|
||||||
if (
|
|
||||||
todos === null ||
|
|
||||||
!todos.todos ||
|
|
||||||
todos.todos.length === 0 ||
|
|
||||||
(!uiState.showFullTodos && !hasActiveTodos)
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Checklist
|
||||||
borderStyle="single"
|
title="Todo"
|
||||||
borderBottom={false}
|
items={checklistItems}
|
||||||
borderRight={false}
|
isExpanded={uiState.showFullTodos}
|
||||||
borderLeft={false}
|
toggleHint="ctrl+t to toggle"
|
||||||
borderColor={theme.border.default}
|
/>
|
||||||
paddingLeft={1}
|
|
||||||
paddingRight={1}
|
|
||||||
>
|
|
||||||
{uiState.showFullTodos ? (
|
|
||||||
<Box flexDirection="column" rowGap={1}>
|
|
||||||
<TodoTitleDisplay todos={todos} />
|
|
||||||
<TodoListDisplay todos={todos} />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box flexDirection="row" columnGap={1} height={1}>
|
|
||||||
<Box flexShrink={0} flexGrow={0}>
|
|
||||||
<TodoTitleDisplay todos={todos} />
|
|
||||||
</Box>
|
|
||||||
{inProgress && (
|
|
||||||
<Box flexShrink={1} flexGrow={1}>
|
|
||||||
<TodoItemDisplay todo={inProgress} wrap="truncate" />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TodoListDisplayProps {
|
|
||||||
todos: TodoList;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TodoListDisplay: React.FC<TodoListDisplayProps> = ({ todos }) => (
|
|
||||||
<Box flexDirection="column" aria-role="list">
|
|
||||||
{todos.todos.map((todo: Todo, index: number) => (
|
|
||||||
<TodoItemDisplay todo={todo} key={index} role="listitem" />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user