From 6abb39a5fe875a677a016eb95e8b09fce645058b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 10:28:24 -0400 Subject: [PATCH] Phase 2: Refactor Tasks cluster to MCP SDK types Replace ad hoc TaskCardProps data shape with SDK Task type across the Tasks group components and TasksScreen. - TaskCard: accept `task: Task` instead of flat props (taskId, status, method, target, progress, etc.); derive all display fields from the SDK Task object (taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage); remove fields not present on SDK Task (method, target, progress as number, error, completedAt, elapsed) - TaskControls: type statusFilter as `TaskStatus | undefined`; add runtime validation guard on Select onChange - TaskListPanel: accept `Task[]` instead of `TaskCardProps[]`; pass `onCancel(taskId)` through to each TaskCard; use `task.taskId` as key - TasksScreen: accept `Task[]` + `onCancel(taskId)` callback; type internal statusFilter state as `TaskStatus | undefined` - Delete re-export of `TaskStatus` from TaskCard (import directly from SDK where needed) - Update all stories with real SDK `Task` fixtures - Update ConnectedView TasksActive story Co-Authored-By: Claude Opus 4.6 (1M context) --- .../groups/TaskCard/TaskCard.stories.tsx | 116 ++++++++---------- .../components/groups/TaskCard/TaskCard.tsx | 100 +++------------ .../groups/TaskControls/TaskControls.tsx | 12 +- .../TaskListPanel/TaskListPanel.stories.tsx | 47 +++---- .../groups/TaskListPanel/TaskListPanel.tsx | 30 ++--- .../TasksScreen/TasksScreen.stories.tsx | 44 ++----- .../screens/TasksScreen/TasksScreen.tsx | 13 +- .../ConnectedView/ConnectedView.stories.tsx | 40 ++---- 8 files changed, 140 insertions(+), 262 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx index 1c39bfe8..bc077989 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TaskCard } from "./TaskCard"; @@ -14,84 +15,71 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const workingTask: Task = { + taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", + status: "working", + ttl: 300000, + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Synthesizing findings...", +}; + +const inputRequiredTask: Task = { + taskId: "4100b5e0b0ed9cd0023330342d1bf647", + status: "input_required", + ttl: 300000, + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", + statusMessage: "Waiting for user confirmation", +}; + +const completedTask: Task = { + taskId: "d487b49aa39023d907b5a2a5b506cb3", + status: "completed", + ttl: 300000, + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:51Z", + statusMessage: "Report generated successfully", +}; + +const failedTask: Task = { + taskId: "e6ebffd9cca84ddd1646d3c579a4d453", + status: "failed", + ttl: 300000, + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:28Z", + statusMessage: + "Connection refused: upstream server not responding after 3 retries", +}; + +const cancelledTask: Task = { + taskId: "c416bc183cb04468d3f81696f1b868f6", + status: "cancelled", + ttl: 300000, + createdAt: "2026-03-29T20:18:15Z", + lastUpdatedAt: "2026-03-29T20:18:18Z", +}; + export const Working: Story = { - args: { - taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", - status: "working", - method: "tools/call", - target: "generate_report", - progress: 80, - progressDescription: "Synthesizing findings...", - startedAt: "3/29/2026, 8:18:20 PM", - lastUpdated: "3/29/2026, 8:18:22 PM", - ttl: 300000, - }, + args: { task: workingTask }, }; export const InputRequired: Story = { - args: { - taskId: "4100b5e0b0ed9cd0023330342d1bf647", - status: "input_required", - method: "resources/read", - target: "file:///data/report.csv", - startedAt: "3/29/2026, 8:17:55 PM", - lastUpdated: "3/29/2026, 8:17:55 PM", - ttl: 300000, - }, + args: { task: inputRequiredTask }, }; export const Completed: Story = { - args: { - taskId: "d487b49aa39023d907b5a2a5b506cb3", - status: "completed", - method: "tools/call", - target: "generate_summary", - startedAt: "3/29/2026, 8:16:47 PM", - completedAt: "3/29/2026, 8:16:51 PM", - lastUpdated: "3/29/2026, 8:16:51 PM", - ttl: 300000, - progressDescription: "Generating report...", - }, + args: { task: completedTask }, }; export const Failed: Story = { - args: { - taskId: "e6ebffd9cca84ddd1646d3c579a4d453", - status: "failed", - method: "tools/call", - target: "fetch_remote_data", - startedAt: "3/29/2026, 8:17:27 PM", - completedAt: "3/29/2026, 8:17:28 PM", - lastUpdated: "3/29/2026, 8:17:28 PM", - ttl: 300000, - error: "Connection refused: upstream server not responding after 3 retries", - }, + args: { task: failedTask }, }; export const Cancelled: Story = { - args: { - taskId: "c416bc183cb04468d3f81696f1b868f6", - status: "cancelled", - method: "tools/call", - target: "long_running_analysis", - startedAt: "3/29/2026, 8:18:15 PM", - completedAt: "3/29/2026, 8:18:18 PM", - lastUpdated: "3/29/2026, 8:18:18 PM", - ttl: 300000, - }, + args: { task: cancelledTask }, }; export const Collapsed: Story = { - args: { - taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", - status: "working", - method: "tools/call", - target: "generate_report", - progress: 80, - progressDescription: "Synthesizing findings...", - startedAt: "3/29/2026, 8:18:20 PM", - lastUpdated: "3/29/2026, 8:18:22 PM", - ttl: 300000, - isListExpanded: false, - }, + args: { task: workingTask, isListExpanded: false }, }; diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index 1ff14d07..d183dce7 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -8,26 +8,12 @@ import { Stack, Text, } from "@mantine/core"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; -import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; import { TaskStatusBadge } from "../../elements/TaskStatusBadge/TaskStatusBadge"; -import type { TaskStatus } from "@modelcontextprotocol/sdk/types.js"; -export type { TaskStatus }; - export interface TaskCardProps { - taskId: string; - status: TaskStatus; - method: string; - target?: string; - progress?: number; - progressDescription?: string; - startedAt?: string; - completedAt?: string; - lastUpdated?: string; - elapsed?: string; - ttl?: number; - error?: string; + task: Task; isListExpanded: boolean; onCancel: () => void; } @@ -63,11 +49,6 @@ const StatusMessageText = Text.withProps({ fs: "italic", }); -const ErrorText = Text.withProps({ - size: "sm", - c: "red", -}); - const SubtleButton = Button.withProps({ variant: "subtle", size: "xs", @@ -110,37 +91,8 @@ function formatTtl(ttl: number): string { return `${ttl}ms`; } -function buildTaskObject(props: TaskCardProps): string { - const obj: Record = { - taskId: props.taskId, - status: props.status, - method: props.method, - }; - if (props.target) obj.target = props.target; - if (props.ttl !== undefined) obj.ttl = props.ttl; - if (props.startedAt) obj.createdAt = props.startedAt; - if (props.lastUpdated) obj.lastUpdatedAt = props.lastUpdated; - if (props.completedAt) obj.completedAt = props.completedAt; - if (props.progress !== undefined) obj.progress = props.progress; - if (props.progressDescription) obj.statusMessage = props.progressDescription; - if (props.error) obj.error = props.error; - return JSON.stringify(obj); -} - -export function TaskCard(props: TaskCardProps) { - const { - taskId, - status, - progress, - progressDescription, - startedAt, - lastUpdated, - ttl, - error, - isListExpanded, - onCancel, - } = props; - +export function TaskCard({ task, isListExpanded, onCancel }: TaskCardProps) { + const { taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage } = task; const [isExpanded, setIsExpanded] = useState(isListExpanded); const isActive = status === "working" || status === "input_required"; @@ -170,19 +122,15 @@ export function TaskCard(props: TaskCardProps) { Status {status} - {lastUpdated && ( - - Last Updated - {lastUpdated} - - )} - {startedAt && ( - - Created At - {startedAt} - - )} - {ttl !== undefined && ( + + Last Updated + {lastUpdatedAt} + + + Created At + {createdAt} + + {ttl !== null && ( TTL {formatTtl(ttl)} @@ -194,36 +142,24 @@ export function TaskCard(props: TaskCardProps) { - {progress !== undefined && isActive && ( - + {statusMessage && isActive && ( + {statusMessage} )} {isExpanded && ( - {progressDescription && !isActive && ( + {statusMessage && !isActive && ( Status Message - {progressDescription} - - )} - - {error && ( - - Error - {error} + {statusMessage} )} Full Task Object diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.tsx index 720491f6..d2f346b5 100644 --- a/clients/web/src/components/groups/TaskControls/TaskControls.tsx +++ b/clients/web/src/components/groups/TaskControls/TaskControls.tsx @@ -16,9 +16,9 @@ const ToolbarButton = Button.withProps({ export interface TaskControlsProps { searchText: string; - statusFilter?: string; + statusFilter?: TaskStatus; onSearchChange: (text: string) => void; - onStatusFilterChange: (status: string) => void; + onStatusFilterChange: (status: TaskStatus | undefined) => void; onRefresh: () => void; } @@ -47,7 +47,13 @@ export function TaskControls({ placeholder="All statuses" data={STATUS_OPTIONS} value={statusFilter ?? null} - onChange={(value) => onStatusFilterChange(value ?? "")} + onChange={(value) => + onStatusFilterChange( + value && STATUS_OPTIONS.includes(value as TaskStatus) + ? (value as TaskStatus) + : undefined, + ) + } clearable /> diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx index b516b9d2..2a6efa56 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx @@ -1,57 +1,39 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TaskListPanel } from "./TaskListPanel"; -import type { TaskCardProps } from "../TaskCard/TaskCard"; -const sampleTasks: TaskCardProps[] = [ +const sampleTasks: Task[] = [ { taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", status: "working", - method: "tools/call", - target: "generate_report", - progress: 80, - progressDescription: "Synthesizing findings...", - startedAt: "3/29/2026, 8:18:20 PM", - lastUpdated: "3/29/2026, 8:18:22 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Synthesizing findings...", }, { taskId: "4100b5e0b0ed9cd0023330342d1bf647", status: "input_required", - method: "resources/read", - target: "file:///data/report.csv", - startedAt: "3/29/2026, 8:17:55 PM", - lastUpdated: "3/29/2026, 8:17:55 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { taskId: "d487b49aa39023d907b5a2a5b506cb3", status: "completed", - method: "tools/call", - target: "generate_summary", - startedAt: "3/29/2026, 8:16:47 PM", - completedAt: "3/29/2026, 8:16:51 PM", - lastUpdated: "3/29/2026, 8:16:51 PM", ttl: 300000, - progressDescription: "Generating report...", - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:51Z", + statusMessage: "Report generated successfully", }, { taskId: "e6ebffd9cca84ddd1646d3c579a4d453", status: "failed", - method: "tools/call", - target: "fetch_remote_data", - startedAt: "3/29/2026, 8:17:27 PM", - lastUpdated: "3/29/2026, 8:17:28 PM", ttl: 300000, - error: "Connection refused: upstream server not responding", - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:28Z", + statusMessage: "Connection refused: upstream server not responding", }, ]; @@ -61,6 +43,7 @@ const meta: Meta = { args: { tasks: sampleTasks, searchText: "", + onCancel: fn(), onClearCompleted: fn(), }, }; @@ -72,7 +55,7 @@ export const Default: Story = {}; export const FilteredByStatus: Story = { args: { - statusFilter: "running", + statusFilter: "working", }, }; diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx index 57dcad0b..1096081c 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx @@ -8,14 +8,15 @@ import { Text, Title, } from "@mantine/core"; +import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskCard } from "../TaskCard/TaskCard"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; -import type { TaskCardProps } from "../TaskCard/TaskCard"; export interface TaskListPanelProps { - tasks: TaskCardProps[]; + tasks: Task[]; searchText: string; - statusFilter?: string; + statusFilter?: TaskStatus; + onCancel: (taskId: string) => void; onClearCompleted: () => void; } @@ -37,10 +38,6 @@ const EmptyState = Text.withProps({ py: "xl", }); -function taskKey(task: TaskCardProps): string { - return task.taskId; -} - function formatActiveTitle(count: number): string { return `Active (${count})`; } @@ -49,20 +46,20 @@ function formatCompletedTitle(count: number): string { return `Completed (${count})`; } -function isActiveStatus(status: string): boolean { +function isActiveStatus(status: TaskStatus): boolean { return status === "working" || status === "input_required"; } function matchesFilters( - task: TaskCardProps, + task: Task, searchText: string, - statusFilter?: string, + statusFilter?: TaskStatus, ): boolean { if (statusFilter && task.status !== statusFilter) return false; if (searchText) { const term = searchText.toLowerCase(); const searchable = - `${task.taskId} ${task.method} ${task.target ?? ""} ${task.status}`.toLowerCase(); + `${task.taskId} ${task.status} ${task.statusMessage ?? ""}`.toLowerCase(); if (!searchable.includes(term)) return false; } return true; @@ -72,6 +69,7 @@ export function TaskListPanel({ tasks, searchText, statusFilter, + onCancel, onClearCompleted, }: TaskListPanelProps) { const [compact, setCompact] = useState(false); @@ -115,9 +113,10 @@ export function TaskListPanel({ {formatActiveTitle(activeTasks.length)} {activeTasks.map((task) => ( onCancel(task.taskId)} /> ))} @@ -135,9 +134,10 @@ export function TaskListPanel({ {completedTasks.map((task) => ( onCancel(task.taskId)} /> ))} diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index 40085855..1f6883a0 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TasksScreen } from "./TasksScreen"; -import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; const meta: Meta = { title: "Screens/TasksScreen", @@ -10,61 +10,43 @@ const meta: Meta = { args: { onRefresh: fn(), onClearCompleted: fn(), + onCancel: fn(), }, }; export default meta; type Story = StoryObj; -const sampleTasks: TaskCardProps[] = [ +const sampleTasks: Task[] = [ { taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", status: "working", - method: "tools/call", - target: "batch_process", - progress: 65, - progressDescription: "Processing records 650 of 1000...", - startedAt: "3/29/2026, 8:18:20 PM", - lastUpdated: "3/29/2026, 8:18:22 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Processing records 650 of 1000...", }, { taskId: "4100b5e0b0ed9cd0023330342d1bf647", status: "input_required", - method: "resources/read", - target: "file:///data/large-dataset.csv", - startedAt: "3/29/2026, 8:17:55 PM", - lastUpdated: "3/29/2026, 8:17:55 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { taskId: "d487b49aa39023d907b5a2a5b506cb3", status: "completed", - method: "tools/call", - target: "send_message", - startedAt: "3/29/2026, 8:16:47 PM", - completedAt: "3/29/2026, 8:16:49 PM", - lastUpdated: "3/29/2026, 8:16:49 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:49Z", }, { taskId: "e6ebffd9cca84ddd1646d3c579a4d453", status: "failed", - method: "tools/call", - target: "delete_records", - startedAt: "3/29/2026, 8:17:27 PM", - completedAt: "3/29/2026, 8:17:57 PM", - lastUpdated: "3/29/2026, 8:17:57 PM", ttl: 300000, - error: "Timeout waiting for tool response after 30s", - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:57Z", + statusMessage: "Timeout waiting for tool response after 30s", }, ]; diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx index fd7b4417..b6a0730c 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx @@ -1,13 +1,14 @@ import { useState } from "react"; import { Card, Flex, Stack } from "@mantine/core"; +import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskControls } from "../../groups/TaskControls/TaskControls"; import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel"; -import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; export interface TasksScreenProps { - tasks: TaskCardProps[]; + tasks: Task[]; onRefresh: () => void; onClearCompleted: () => void; + onCancel: (taskId: string) => void; } const ScreenLayout = Flex.withProps({ @@ -31,9 +32,10 @@ export function TasksScreen({ tasks, onRefresh, onClearCompleted, + onCancel, }: TasksScreenProps) { const [searchText, setSearchText] = useState(""); - const [statusFilter, setStatusFilter] = useState(); + const [statusFilter, setStatusFilter] = useState(); return ( @@ -43,9 +45,7 @@ export function TasksScreen({ searchText={searchText} statusFilter={statusFilter} onSearchChange={setSearchText} - onStatusFilterChange={(value) => - setStatusFilter(value || undefined) - } + onStatusFilterChange={setStatusFilter} onRefresh={onRefresh} /> @@ -54,6 +54,7 @@ export function TasksScreen({ tasks={tasks} searchText={searchText} statusFilter={statusFilter} + onCancel={onCancel} onClearCompleted={onClearCompleted} /> diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index 5c70c104..09b15693 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -549,55 +549,37 @@ export const TasksActive: Story = { { taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", status: "working", - method: "tools/call", - target: "batch_process", - progress: 65, - progressDescription: "Processing records 650 of 1000...", - startedAt: "3/29/2026, 8:18:20 PM", - lastUpdated: "3/29/2026, 8:18:22 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Processing records 650 of 1000...", }, { taskId: "4100b5e0b0ed9cd0023330342d1bf647", status: "input_required", - method: "resources/read", - target: "file:///data/large-dataset.csv", - startedAt: "3/29/2026, 8:17:55 PM", - lastUpdated: "3/29/2026, 8:17:55 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { taskId: "d487b49aa39023d907b5a2a5b506cb3", status: "completed", - method: "tools/call", - target: "send_message", - startedAt: "3/29/2026, 8:16:47 PM", - completedAt: "3/29/2026, 8:16:49 PM", - lastUpdated: "3/29/2026, 8:16:49 PM", ttl: 300000, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:49Z", }, { taskId: "e6ebffd9cca84ddd1646d3c579a4d453", status: "failed", - method: "tools/call", - target: "delete_records", - startedAt: "3/29/2026, 8:17:27 PM", - completedAt: "3/29/2026, 8:17:57 PM", - lastUpdated: "3/29/2026, 8:17:57 PM", ttl: 300000, - error: "Timeout waiting for tool response after 30s", - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:57Z", + statusMessage: "Timeout waiting for tool response after 30s", }, ]} onRefresh={fn()} onClearCompleted={fn()} + onCancel={fn()} /> ), },