Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/src/components/projects/docs/doc-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const DocEditor = forwardRef<DocEditorHandle, DocEditorProps>(
ref,
) {
const { resolvedMode } = useThemeMode();
const { teamMembers, tasks, documents } = useMentionData(projectId);
const { teamMembers, documents } = useMentionData(projectId);

const lastSavedRef = useRef<string | null>(null);
const initializedRef = useRef(false);
Expand Down Expand Up @@ -188,7 +188,7 @@ export const DocEditor = forwardRef<DocEditorHandle, DocEditorProps>(
<MentionSuggestionMenus
editor={editor}
teamMembers={teamMembers}
tasks={tasks}
projectId={projectId}
documents={documents}
/>
)}
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/projects/interactions/board-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
buildColumnDropUpdate,
type ColumnGroupDef,
DEFAULT_VISIBLE_FIELDS,
type EpicsPagination,
getColumnGroupDefs,
getSwimlaneDefs,
getTaskColumnKeys,
Expand Down Expand Up @@ -55,6 +56,8 @@ interface BoardViewProps {
) => Promise<void>;
onTaskClick: (task: Task) => void;
epics?: Task[];
/** Load-more state for the epic picker, shared by every card in this view. */
epicsPagination?: EpicsPagination;
onUpdateTask?: (taskId: string, payload: TaskFieldUpdate) => void;
onMoveToColumn?: (taskId: string, update: TaskFieldUpdate) => void;
/** Opens the delete-confirmation dialog for a task — wired to the
Expand Down Expand Up @@ -91,6 +94,7 @@ export function BoardView({
canEdit,
tasksQueryKey,
epics = [],
epicsPagination,
onCreateTask,
onTaskClick,
onUpdateTask,
Expand Down Expand Up @@ -560,6 +564,7 @@ export function BoardView({
taskTypes={taskTypes}
members={members}
epics={epics}
epicsPagination={epicsPagination}
canEdit={!!canEdit}
onOpen={() => onTaskClick(task)}
onUpdate={handleInlineUpdate}
Expand All @@ -577,6 +582,7 @@ export function BoardView({
members={members}
customFields={customFields}
epics={epics}
epicsPagination={epicsPagination}
visibleFields={visibleFields}
canEdit={canEdit}
isDragging={draggingId === task.id}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
useInfiniteQuery,
useMutation,
useQueries,
useQuery,
Expand Down Expand Up @@ -53,7 +54,7 @@ import {
createViewByContext,
deleteTask,
deleteViewById,
epicTasksQueryOptions,
epicTasksInfiniteQueryOptions,
type FilterConfig,
type InteractionView,
type ListTasksOptions,
Expand Down Expand Up @@ -97,6 +98,7 @@ import { RoadmapView } from "./roadmap-view";
import { TaskDetailModal } from "./task-detail-modal";
import { UNASSIGNED_FILTER_ID, ViewSettingsPanel } from "./view-settings-panel";
import {
type EpicsPagination,
getColumnGroupDefs,
getDefaultInitialPageSize,
getDefaultPageSize,
Expand Down Expand Up @@ -550,12 +552,33 @@ export function InteractionLayout({

const { data: sprints = [] } = useQuery(sprintsQueryOptions(projectId));

// Fetch Epic tasks for display in the epic field on task cards/rows
// Fetch Epic tasks for display in the epic field on task cards/rows.
// Paginated 20-per-page (see epicTasksInfiniteQueryOptions) — every epic
// picker app-wide (task detail, board/list-view cards & rows, the
// right-click context menu) shares this single query and its
// fetchNextPage/hasNextPage via the epicsPagination object below.
const epicType = findEpicType(taskTypes);
const { data: epicTasks = [] } = useQuery({
...epicTasksQueryOptions(projectId, epicType?.id ?? ""),
const {
data: epicPages,
fetchNextPage: fetchNextEpicsPage,
hasNextPage: hasMoreEpics,
isFetchingNextPage: isFetchingMoreEpics,
} = useInfiniteQuery({
...epicTasksInfiniteQueryOptions(projectId, epicType?.id ?? ""),
enabled: !!epicType?.id,
});
const epicTasks = useMemo(
() => epicPages?.pages.flatMap((page) => page.items) ?? [],
[epicPages],
);
const epicsPagination: EpicsPagination = useMemo(
() => ({
hasMore: !!hasMoreEpics,
isLoadingMore: isFetchingMoreEpics,
onLoadMore: () => void fetchNextEpicsPage(),
}),
[hasMoreEpics, isFetchingMoreEpics, fetchNextEpicsPage],
);

const isRealView = !!activeViewId && !activeViewId.startsWith("__default-");
const effectiveViewId = isManualSort && isRealView ? activeViewId : undefined;
Expand Down Expand Up @@ -1702,6 +1725,7 @@ export function InteractionLayout({
customFields={customFields}
sprints={sprints}
epics={epicTasks}
epicsPagination={epicsPagination}
viewConfig={activeViewConfig}
canCreate={canCreate}
canEdit={canEdit}
Expand Down Expand Up @@ -1752,6 +1776,7 @@ export function InteractionLayout({
members={members}
customFields={customFields}
epics={epicTasks}
epicsPagination={epicsPagination}
viewConfig={activeViewConfig}
canCreate={canCreate}
columnPagination={columnPagination}
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/projects/interactions/list-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getRowColConfig, TaskRow } from "./task-row";
import {
buildColumnDropUpdate,
type ColumnGroupDef,
type EpicsPagination,
getTaskSwimlaneKey,
type TaskFieldUpdate,
} from "./view-utils";
Expand All @@ -32,6 +33,8 @@ export interface ListGroupProps {
members: ProjectMember[];
customFields: CustomFieldDefinition[];
epics?: Task[];
/** Load-more state for the epic picker, shared by every row in this group. */
epicsPagination?: EpicsPagination;
canCreate: boolean;
defaultCollapsed?: boolean;
fieldSum?: string;
Expand Down Expand Up @@ -99,6 +102,7 @@ export function ListGroup({
members,
customFields,
epics = [],
epicsPagination,
canCreate,
defaultCollapsed,
fieldSum,
Expand Down Expand Up @@ -426,6 +430,7 @@ export function ListGroup({
taskTypes={taskTypes}
members={members}
epics={epics}
epicsPagination={epicsPagination}
canEdit={!!canEdit}
onOpen={() => onTaskClick(task)}
onUpdate={onUpdateTaskField}
Expand All @@ -442,6 +447,7 @@ export function ListGroup({
taskTypes={taskTypes}
members={members}
epics={epics}
epicsPagination={epicsPagination}
customFields={customFields}
visibleFields={visibleFields}
onClick={() => onTaskClick(task)}
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/projects/interactions/list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
applyStatusFilterToColumnDefs,
type ColumnGroupDef,
DEFAULT_VISIBLE_FIELDS,
type EpicsPagination,
getColumnGroupDefs,
getSwimlaneDefs,
getTaskColumnKeys,
Expand All @@ -31,6 +32,8 @@ export interface ListViewProps {
members?: ProjectMember[];
customFields?: CustomFieldDefinition[];
epics?: Task[];
/** Load-more state for the epic picker, shared by every group/row/card. */
epicsPagination?: EpicsPagination;
viewConfig?: ViewConfig;
canCreate: boolean;
onCreateTask: (
Expand Down Expand Up @@ -84,6 +87,7 @@ export function ListView({
members = [],
customFields = [],
epics = [],
epicsPagination,
viewConfig,
canCreate,
onCreateTask,
Expand Down Expand Up @@ -237,6 +241,7 @@ export function ListView({
members={members}
customFields={customFields}
epics={epics}
epicsPagination={epicsPagination}
canCreate={canCreate}
defaultCollapsed={defaultCollapsed}
fieldSum={fieldSum}
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/projects/interactions/task-card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import type { Task } from "@/lib/interaction-api";
import type { TaskStatus, TaskType } from "@/lib/project-api";
import { TaskCard } from "./task-card";

// TaskCard's epic-picker field now calls useEpicSearch (useInfiniteQuery)
// unconditionally, so it needs a QueryClientProvider ancestor even though the
// query stays disabled (epicOpen is never toggled true) in these tests.
function wrapper({ children }: { children: ReactNode }) {
const qc = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}

// ── Fixtures ──────────────────────────────────────────────────────────────────

const makeTask = (overrides: Partial<Task> = {}): Task => ({
Expand Down Expand Up @@ -51,6 +63,7 @@ describe("TaskCard", () => {
statuses={NO_STATUSES}
taskTypes={NO_TYPES}
/>,
{ wrapper },
);
expect(screen.getByText("Fix the login bug")).toBeInTheDocument();
});
Expand All @@ -62,6 +75,7 @@ describe("TaskCard", () => {
statuses={NO_STATUSES}
taskTypes={[bugType]}
/>,
{ wrapper },
);
expect(screen.queryByText("Bug")).not.toBeInTheDocument();
});
Expand All @@ -73,6 +87,7 @@ describe("TaskCard", () => {
statuses={NO_STATUSES}
taskTypes={[bugType]}
/>,
{ wrapper },
);
expect(screen.getByText("Bug")).toBeInTheDocument();
});
Expand All @@ -86,6 +101,7 @@ describe("TaskCard", () => {
taskTypes={NO_TYPES}
onClick={onClick}
/>,
{ wrapper },
);
fireEvent.click(screen.getByText("Fix the login bug"));
expect(onClick).toHaveBeenCalledOnce();
Expand All @@ -99,6 +115,7 @@ describe("TaskCard", () => {
taskTypes={NO_TYPES}
canEdit={true}
/>,
{ wrapper },
);
const card = container.querySelector("[data-task-id='task-1']");
expect(card).toHaveAttribute("draggable", "true");
Expand All @@ -112,6 +129,7 @@ describe("TaskCard", () => {
taskTypes={NO_TYPES}
canEdit={false}
/>,
{ wrapper },
);
const card = container.querySelector("[data-task-id='task-1']");
expect(card).toHaveAttribute("draggable", "false");
Expand All @@ -127,6 +145,7 @@ describe("TaskCard", () => {
canEdit={true}
onDragStart={onDragStart}
/>,
{ wrapper },
);
const card = container.querySelector("[data-task-id='task-1']") as Element;
fireEvent.dragStart(card);
Expand All @@ -143,6 +162,7 @@ describe("TaskCard", () => {
canEdit={true}
onDragEnd={onDragEnd}
/>,
{ wrapper },
);
const card = container.querySelector("[data-task-id='task-1']") as Element;
fireEvent.dragEnd(card);
Expand All @@ -157,6 +177,7 @@ describe("TaskCard", () => {
taskTypes={NO_TYPES}
isDragging={true}
/>,
{ wrapper },
);
const card = container.querySelector("[data-task-id='task-1']") as Element;
// isDragging adds opacity-50 class
Expand All @@ -170,6 +191,7 @@ describe("TaskCard", () => {
statuses={NO_STATUSES}
taskTypes={NO_TYPES}
/>,
{ wrapper },
);
// assigned state: filled avatar circle with the primary gradient
const assigneeEl = container.querySelector(".from-primary\\/20");
Expand Down
Loading
Loading