# Raft Chat — Tasks

> The Tasks destination — its title and count, the Channel, Assignee and Creator filters, and the status groups.

Source: https://raft-design.botiverse.build/products/raft-chat/tasks. Generated from the current release; where it differs from what you remember, this page is right.

Tasks is the top-level page that lists every task in the workspace. The shared task rules (statuses, board and list, details, changing status) are in [Tasks](https://raft-design.botiverse.build/patterns/tasks.md).

## Structure

From top to bottom on a phone:

1. **The frame**: the large title Tasks and, under it, the count ("12 channel tasks"). The page has no workspace pill, so it has no navigation row: the title follows the status bar directly.
2. **The filter bar**: Channel, Assignee and Creator. An active filter is `info-soft` with a count. Board / List is at the right end; on a phone it shows only icons.
3. **Status groups**: Todo, In Progress, In Review, Done, Closed, each with a collapsible heading and a count, holding [TaskCard](https://raft-design.botiverse.build/components/task-card.md)s.

On desktop the page has no sidebar: the board follows the rail directly.

```tsx
import type { ReactNode } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import {
  Tabs,
  TabsLabel,
  TabsList,
  TabsTab,
  TasksPanelRoot,
  TasksPanelToolbar,
  TasksPanelToolbarFilters,
  TasksPanelToolbarView,
  TasksPanelViewport,
} from "@botiverse/raft-ui";
import { ListTodo, SquareKanban } from "lucide-react";

import {
  getTaskStatusLabel,
  moveTaskFixture,
  TASK_FIXTURES,
} from "#/modules/task-card/task-card.fixtures.ts";
import type { TaskFixture, TaskStatus } from "#/modules/task-card/task-card.fixtures.ts";
import { TasksPanelBoardView } from "../tasks-panel-board-view.tsx";
import { TasksPanelEmptyState } from "../tasks-panel-empty-state.tsx";
import { TasksPanelListView } from "../tasks-panel-list-view.tsx";
import { TasksIdentityFiltersComposition } from "./tasks-identity-filters.composition.tsx";

type TaskView = "board" | "list";

export type TasksPanelCompositionProps = {
  tasks?: readonly TaskFixture[];
  toolbarFilters?: ReactNode;
  filterTask?: (task: TaskFixture) => boolean;
  view?: TaskView;
  initialView?: TaskView;
  onViewChange?: (view: TaskView) => void;
  onTasksChange?: (tasks: readonly TaskFixture[]) => void;
  onOpenTask?: (task: TaskFixture) => void;
  blockedDropStatus?: TaskStatus;
  showChannelName?: boolean;
  /** When false, hide the toolbar Board/List control (e.g. mobile page header owns it). */
  showToolbarView?: boolean;
};

export function TasksPanelComposition({
  tasks: controlledTasks,
  toolbarFilters,
  filterTask,
  view: controlledView,
  initialView = "board",
  onViewChange,
  onTasksChange,
  onOpenTask,
  blockedDropStatus,
  showChannelName = false,
  showToolbarView = true,
}: TasksPanelCompositionProps) {
  const portalRef = useRef<HTMLDivElement>(null);
  const [localTasks, setLocalTasks] = useState<readonly TaskFixture[]>(TASK_FIXTURES);
  const [localView, setLocalView] = useState<TaskView>(initialView);
  const [collapsed, setCollapsed] = useState<Record<TaskStatus, boolean>>({
    todo: false,
    "in-progress": false,
    "in-review": false,
    done: true,
    closed: true,
  });
  const [announcement, setAnnouncement] = useState("Ready");

  const tasks = controlledTasks ?? localTasks;
  const visibleTasks = useMemo(
    () => (filterTask ? tasks.filter(filterTask) : tasks),
    [filterTask, tasks],
  );
  const view = controlledView ?? localView;
  const updateTasks = useCallback(
    (nextTasks: readonly TaskFixture[]) => {
      if (controlledTasks === undefined) setLocalTasks(nextTasks);
      onTasksChange?.(nextTasks);
    },
    [controlledTasks, onTasksChange],
  );
  const setView = useCallback(
    (nextView: TaskView) => {
      if (controlledView === undefined) setLocalView(nextView);
      onViewChange?.(nextView);
      setAnnouncement(`${nextView === "board" ? "Board" : "List"} view selected`);
    },
    [controlledView, onViewChange],
  );

  const updateStatus = useCallback(
    (taskId: string, status: TaskStatus) => {
      const task = tasks.find((item) => item.id === taskId);
      if (!task || task.status === status) return;
      updateTasks(moveTaskFixture(tasks, taskId, { status }));
      setAnnouncement(`Task #${task.taskNumber} moved to ${getTaskStatusLabel(status)}`);
    },
    [tasks, updateTasks],
  );
  const moveTask = useCallback(
    (
      taskId: string,
      target: { status: TaskStatus; taskId?: string; placement?: "before" | "after" | null },
    ) => {
      const task = tasks.find((item) => item.id === taskId);
      if (!task) return;
      updateTasks(moveTaskFixture(tasks, taskId, target));
      setAnnouncement(`Task #${task.taskNumber} moved to ${getTaskStatusLabel(target.status)}`);
    },
    [tasks, updateTasks],
  );
  const openTask = useCallback(
    (task: TaskFixture) => {
      onOpenTask?.(task);
      setAnnouncement(`Opened task #${task.taskNumber}: ${task.title}`);
    },
    [onOpenTask],
  );
  const handleCollapsedChange = useCallback((status: TaskStatus, nextOpen: boolean) => {
    setCollapsed((current) => ({ ...current, [status]: !nextOpen }));
  }, []);

  return (
    <div ref={portalRef} className="relative flex size-full min-h-0  min-w-0">
      <TasksPanelRoot aria-label="Tasks panel">
        <TasksPanelToolbar>
          <TasksPanelToolbarFilters>
            {toolbarFilters ?? <TasksIdentityFiltersComposition />}
          </TasksPanelToolbarFilters>
          {showToolbarView ? (
            <TasksPanelToolbarView>
              <Tabs value={view} onValueChange={setView} aria-label="Task view">
                <TabsList>
                  <TabsTab value="board" aria-label="Board" className="max-md:px-2.5">
                    <SquareKanban aria-hidden />
                    <TabsLabel className="max-md:hidden">Board</TabsLabel>
                  </TabsTab>
                  <TabsTab value="list" aria-label="List" className="max-md:px-2.5">
                    <ListTodo aria-hidden />
                    <TabsLabel className="max-md:hidden">List</TabsLabel>
                  </TabsTab>
                </TabsList>
              </Tabs>
            </TasksPanelToolbarView>
          ) : null}
        </TasksPanelToolbar>

        <TasksPanelViewport>
          {tasks.length === 0 ? (
            <TasksPanelEmptyState filtered={false} />
          ) : visibleTasks.length === 0 ? (
            <TasksPanelEmptyState filtered />
          ) : view === "board" ? (
            <TasksPanelBoardView
              tasks={visibleTasks}
              collapsed={collapsed}
              onCollapsedChange={handleCollapsedChange}
              onOpenTask={openTask}
              onStatusChange={updateStatus}
              onMoveTask={moveTask}
              portalRef={portalRef}
              blockedDropStatus={blockedDropStatus}
              showChannelName={showChannelName}
            />
          ) : (
            <TasksPanelListView
              tasks={visibleTasks}
              collapsed={collapsed}
              onCollapsedChange={handleCollapsedChange}
              onOpenTask={openTask}
              onStatusChange={updateStatus}
              portalRef={portalRef}
              showChannelName={showChannelName}
            />
          )}
        </TasksPanelViewport>
      </TasksPanelRoot>

      <span className="sr-only" aria-live="polite">
        {announcement}
      </span>
    </div>
  );
}
```

## Rules

- Don't put a create action on this page. A task always belongs to a channel, so tasks are created in a channel ([Channel](https://raft-design.botiverse.build/products/raft-chat/channel.md#tasks-in-the-conversation)).
- Name the two shortcut rows in the filter lists Created by me and Assigned to me.
- Default to List on a phone, and remember the person's last choice.
- When a channel's task view is empty, say how a task gets there: "No tasks yet", "Tick As Task when you send a message, and it lands here.", and a small ink-bordered Create Task ([EmptyState](https://raft-design.botiverse.build/components/empty-state.md)).
- Don't add a "needs you" group or color. What waits for the person's approval is in Activity, with an action card in the conversation.
- Make the status chip the only thing on a card that changes in place. Selecting the card opens the task details (full screen on a phone, covering the tab bar).
- Make a channel's task view the same panel as this page, without the Channel filter.
