Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
9035bc2644 fix(issues): thread sort through load-more so appended pages render (MUL-2678)
The recent server-side-sort change (#3228) keyed the issue-list cache by
sort but did not update the load-more hooks: useLoadMoreByStatus used a
prefix-match that could pick a stale cache variant, and neither hook
forwarded sort to the API request. As a result, scroll-to-load-more
fired its request, but the response was either appended to a cache no
useQuery was subscribed to, or it appended rows in an unsorted order
into a sorted bucket.

Pass `sort` explicitly through Board/List/Swimlane and into the hooks.
The hook now targets the full sorted key via setQueryData and forwards
sort to the listIssues / listGroupedIssues calls so the appended page
lines up with the existing items.

Also adds focused tests for both load-more hooks: stale-sort cache is
untouched, sort is forwarded to the API, and sort-less callers still hit
the {} key path used by actor-issues-panel.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 15:56:44 +08:00
9 changed files with 393 additions and 17 deletions

View File

@@ -0,0 +1,314 @@
/**
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { setApiInstance } from "../api";
import type { ApiClient } from "../api/client";
import { useLoadMoreByAssigneeGroup, useLoadMoreByStatus } from "./mutations";
import {
issueKeys,
type IssueSortParam,
} from "./queries";
import type {
GroupedIssuesResponse,
Issue,
ListIssuesCache,
ListIssuesParams,
ListGroupedIssuesParams,
ListIssuesResponse,
} from "../types";
vi.mock("../hooks", () => ({
useWorkspaceId: () => "ws-1",
}));
const WS_ID = "ws-1";
function makeIssue(idx: number, overrides: Partial<Issue> = {}): Issue {
return {
id: `issue-${idx}`,
workspace_id: WS_ID,
number: idx,
identifier: `MUL-${idx}`,
title: `Issue ${idx}`,
description: null,
status: "todo",
priority: "none",
assignee_type: null,
assignee_id: null,
creator_type: "member",
creator_id: "user-1",
parent_issue_id: null,
project_id: null,
position: idx,
start_date: null,
due_date: null,
labels: [],
metadata: {},
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-01T00:00:00Z",
...overrides,
};
}
function createWrapper(qc: QueryClient) {
return function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
};
}
describe("useLoadMoreByStatus", () => {
let qc: QueryClient;
let listIssues: ReturnType<typeof vi.fn<(p?: ListIssuesParams) => Promise<ListIssuesResponse>>>;
beforeEach(() => {
qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
listIssues = vi.fn();
setApiInstance({ listIssues } as unknown as ApiClient);
});
afterEach(() => {
qc.clear();
vi.restoreAllMocks();
});
it("targets the sorted cache key and forwards sort to the API", async () => {
const sort: IssueSortParam = { sort_by: "priority", sort_direction: "desc" };
const activeKey = issueKeys.listSorted(WS_ID, sort);
const seed: ListIssuesCache = {
byStatus: {
todo: { issues: [makeIssue(1)], total: 3 },
},
};
qc.setQueryData<ListIssuesCache>(activeKey, seed);
listIssues.mockResolvedValue({
issues: [makeIssue(2), makeIssue(3)],
total: 3,
});
const { result } = renderHook(
() => useLoadMoreByStatus("todo", undefined, sort),
{ wrapper: createWrapper(qc) },
);
expect(result.current.hasMore).toBe(true);
expect(result.current.total).toBe(3);
await act(async () => {
await result.current.loadMore();
});
expect(listIssues).toHaveBeenCalledWith({
status: "todo",
limit: 50,
offset: 1,
sort_by: "priority",
sort_direction: "desc",
});
const updated = qc.getQueryData<ListIssuesCache>(activeKey);
expect(updated?.byStatus.todo?.issues).toHaveLength(3);
expect(updated?.byStatus.todo?.issues.map((i) => i.id)).toEqual([
"issue-1",
"issue-2",
"issue-3",
]);
});
it("ignores a stale cache entry under a different sort", async () => {
// Stale entry from a previous sort lingers (kept by gcTime / keepPreviousData).
const staleSort: IssueSortParam = { sort_by: "priority", sort_direction: "desc" };
qc.setQueryData<ListIssuesCache>(issueKeys.listSorted(WS_ID, staleSort), {
byStatus: { todo: { issues: [makeIssue(99)], total: 99 } },
});
// The active sort cache has its own bucket — load-more must target THIS one.
const activeSort: IssueSortParam = { sort_by: "position", sort_direction: undefined };
const activeKey = issueKeys.listSorted(WS_ID, activeSort);
qc.setQueryData<ListIssuesCache>(activeKey, {
byStatus: { todo: { issues: [makeIssue(1)], total: 2 } },
});
listIssues.mockResolvedValue({
issues: [makeIssue(2)],
total: 2,
});
const { result } = renderHook(
() => useLoadMoreByStatus("todo", undefined, activeSort),
{ wrapper: createWrapper(qc) },
);
// total derives from the active key, not the stale one.
expect(result.current.total).toBe(2);
await act(async () => {
await result.current.loadMore();
});
expect(listIssues).toHaveBeenCalledWith(
expect.objectContaining({ offset: 1, sort_by: "position" }),
);
const active = qc.getQueryData<ListIssuesCache>(activeKey);
expect(active?.byStatus.todo?.issues.map((i) => i.id)).toEqual([
"issue-1",
"issue-2",
]);
// Stale cache is untouched.
const stale = qc.getQueryData<ListIssuesCache>(issueKeys.listSorted(WS_ID, staleSort));
expect(stale?.byStatus.todo?.issues.map((i) => i.id)).toEqual(["issue-99"]);
});
it("targets the myList scoped cache when myIssues is provided", async () => {
const sort: IssueSortParam = { sort_by: "title", sort_direction: "asc" };
const myIssues = { scope: "assigned", filter: { assignee_id: "user-1" } };
const activeKey = issueKeys.myListSorted(WS_ID, myIssues.scope, myIssues.filter, sort);
qc.setQueryData<ListIssuesCache>(activeKey, {
byStatus: { in_progress: { issues: [makeIssue(1, { status: "in_progress" })], total: 2 } },
});
listIssues.mockResolvedValue({
issues: [makeIssue(2, { status: "in_progress" })],
total: 2,
});
const { result } = renderHook(
() => useLoadMoreByStatus("in_progress", myIssues, sort),
{ wrapper: createWrapper(qc) },
);
await act(async () => {
await result.current.loadMore();
});
expect(listIssues).toHaveBeenCalledWith({
status: "in_progress",
limit: 50,
offset: 1,
sort_by: "title",
sort_direction: "asc",
assignee_id: "user-1",
});
const updated = qc.getQueryData<ListIssuesCache>(activeKey);
expect(updated?.byStatus.in_progress?.issues).toHaveLength(2);
});
it("works with no sort (matches the {} key used by sort-less callers)", async () => {
const myIssues = { scope: "actor", filter: { assignee_id: "user-2" } };
const activeKey = issueKeys.myListSorted(WS_ID, myIssues.scope, myIssues.filter, undefined);
qc.setQueryData<ListIssuesCache>(activeKey, {
byStatus: { todo: { issues: [makeIssue(1)], total: 2 } },
});
listIssues.mockResolvedValue({ issues: [makeIssue(2)], total: 2 });
const { result } = renderHook(
() => useLoadMoreByStatus("todo", myIssues),
{ wrapper: createWrapper(qc) },
);
expect(result.current.total).toBe(2);
expect(result.current.hasMore).toBe(true);
await act(async () => {
await result.current.loadMore();
});
const updated = qc.getQueryData<ListIssuesCache>(activeKey);
expect(updated?.byStatus.todo?.issues).toHaveLength(2);
});
});
describe("useLoadMoreByAssigneeGroup", () => {
let qc: QueryClient;
let listGroupedIssues: ReturnType<
typeof vi.fn<(p: ListGroupedIssuesParams) => Promise<GroupedIssuesResponse>>
>;
beforeEach(() => {
qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
listGroupedIssues = vi.fn();
setApiInstance({ listGroupedIssues } as unknown as ApiClient);
});
afterEach(() => {
qc.clear();
vi.restoreAllMocks();
});
it("forwards sort to the grouped API and appends into the right group", async () => {
const sort: IssueSortParam = { sort_by: "priority", sort_direction: "desc" };
const queryKey = ["custom", "assignee-groups", "ws-1"] as const;
const seed: GroupedIssuesResponse = {
groups: [
{
id: "assignee:member:user-1",
assignee_type: "member",
assignee_id: "user-1",
issues: [makeIssue(1, { assignee_type: "member", assignee_id: "user-1" })],
total: 2,
},
],
};
qc.setQueryData<GroupedIssuesResponse>(queryKey, seed);
listGroupedIssues.mockResolvedValue({
groups: [
{
id: "assignee:member:user-1",
assignee_type: "member",
assignee_id: "user-1",
issues: [makeIssue(2, { assignee_type: "member", assignee_id: "user-1" })],
total: 2,
},
],
});
const { result } = renderHook(
() =>
useLoadMoreByAssigneeGroup(
{
id: "assignee:member:user-1",
assignee_type: "member",
assignee_id: "user-1",
},
queryKey,
{ statuses: ["todo"] },
sort,
),
{ wrapper: createWrapper(qc) },
);
expect(result.current.hasMore).toBe(true);
expect(result.current.total).toBe(2);
await act(async () => {
await result.current.loadMore();
});
expect(listGroupedIssues).toHaveBeenCalledWith({
group_by: "assignee",
limit: 50,
offset: 1,
sort_by: "priority",
sort_direction: "desc",
statuses: ["todo"],
group_assignee_type: "member",
group_assignee_id: "user-1",
});
const updated = qc.getQueryData<GroupedIssuesResponse>(queryKey);
expect(updated?.groups[0]?.issues.map((i) => i.id)).toEqual([
"issue-1",
"issue-2",
]);
});
});

View File

@@ -5,6 +5,7 @@ import {
issueKeys,
ISSUE_PAGE_SIZE,
type AssigneeGroupedIssuesFilter,
type IssueSortParam,
type MyIssuesFilter,
} from "./queries";
import {
@@ -58,33 +59,40 @@ export type ToggleIssueReactionVars = {
* Paginate one status column into the cache. Works for both the workspace
* issue list and per-scope My Issues lists (pass `myIssues` to target the
* latter).
*
* `sort` must match the sort the consuming `useQuery` was called with —
* the query key embeds it (see `listSorted` / `myListSorted`), so a load-more
* with the wrong sort would patch a stale cache entry that nobody is
* subscribed to. It is also threaded into the API request so the appended
* page lines up with the server-side ordering of the existing items.
*/
export function useLoadMoreByStatus(
status: IssueStatus,
myIssues?: { scope: string; filter: MyIssuesFilter },
sort?: IssueSortParam,
) {
const qc = useQueryClient();
const wsId = useWorkspaceId();
const [isLoading, setIsLoading] = useState(false);
const prefixKey = myIssues
? issueKeys.myList(wsId, myIssues.scope, myIssues.filter)
: issueKeys.list(wsId);
const queries = qc.getQueriesData<ListIssuesCache>({ queryKey: prefixKey });
const [activeKey, cache] = queries[0] ?? [undefined, undefined];
const activeKey = myIssues
? issueKeys.myListSorted(wsId, myIssues.scope, myIssues.filter, sort)
: issueKeys.listSorted(wsId, sort);
const cache = qc.getQueryData<ListIssuesCache>(activeKey);
const bucket = cache?.byStatus[status];
const loaded = bucket?.issues.length ?? 0;
const total = bucket?.total ?? 0;
const hasMore = loaded < total;
const loadMore = useCallback(async () => {
if (isLoading || !hasMore || !activeKey) return;
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const res = await api.listIssues({
status,
limit: ISSUE_PAGE_SIZE,
offset: loaded,
...sort,
...myIssues?.filter,
});
qc.setQueryData<ListIssuesCache>(activeKey, (old) => {
@@ -100,15 +108,23 @@ export function useLoadMoreByStatus(
} finally {
setIsLoading(false);
}
}, [qc, activeKey, status, loaded, hasMore, isLoading, myIssues?.filter]);
}, [qc, activeKey, status, loaded, hasMore, isLoading, myIssues?.filter, sort]);
return { loadMore, hasMore, isLoading, total };
}
/**
* Paginate one assignee-grouped board column into the cache. `queryKey`
* already pins the active cache entry (it's the same object the consuming
* `useQuery` registered), so the cache lookup and `setQueryData` target the
* right row. `sort` is threaded into the API request so the appended page
* lines up with the server-side ordering of the existing items.
*/
export function useLoadMoreByAssigneeGroup(
group: Pick<IssueAssigneeGroup, "id" | "assignee_type" | "assignee_id">,
queryKey: QueryKey,
filter: AssigneeGroupedIssuesFilter,
sort?: IssueSortParam,
) {
const qc = useQueryClient();
const [isLoading, setIsLoading] = useState(false);
@@ -127,6 +143,7 @@ export function useLoadMoreByAssigneeGroup(
group_by: "assignee",
limit: ISSUE_PAGE_SIZE,
offset: loaded,
...sort,
...filter,
group_assignee_type: group.assignee_type ?? "none",
group_assignee_id: group.assignee_id ?? undefined,
@@ -152,7 +169,7 @@ export function useLoadMoreByAssigneeGroup(
} finally {
setIsLoading(false);
}
}, [filter, group.assignee_id, group.assignee_type, hasMore, isLoading, loaded, qc, queryKey]);
}, [filter, group.assignee_id, group.assignee_type, hasMore, isLoading, loaded, qc, queryKey, sort]);
return { loadMore, hasMore, isLoading, total };
}

View File

@@ -18,7 +18,7 @@ import type { QueryKey } from "@tanstack/react-query";
import { arrayMove } from "@dnd-kit/sortable";
import type { Issue, IssueAssigneeGroup, IssueAssigneeType, IssueStatus, UpdateIssueRequest } from "@multica/core/types";
import { useLoadMoreByAssigneeGroup, useLoadMoreByStatus } from "@multica/core/issues/mutations";
import type { AssigneeGroupedIssuesFilter, MyIssuesFilter } from "@multica/core/issues/queries";
import type { AssigneeGroupedIssuesFilter, IssueSortParam, MyIssuesFilter } from "@multica/core/issues/queries";
import { useViewStore } from "@multica/core/issues/stores/view-store-context";
import type { IssueGrouping } from "@multica/core/issues/stores/view-store";
import { useActorName } from "@multica/core/workspace/hooks";
@@ -205,6 +205,7 @@ export function BoardView({
childProgressMap = EMPTY_PROGRESS_MAP,
myIssuesScope,
myIssuesFilter,
sort,
projectId,
}: {
issues: Issue[];
@@ -218,6 +219,8 @@ export function BoardView({
/** When set, per-status load-more targets the scoped cache instead of the workspace one. */
myIssuesScope?: string;
myIssuesFilter?: MyIssuesFilter;
/** Must match the sort the page queried with — embedded in the cache key. */
sort?: IssueSortParam;
/** When set, the per-column "+" pre-fills the project on the create form. */
projectId?: string;
}) {
@@ -493,6 +496,7 @@ export function BoardView({
issueMap={issueMapRef.current}
childProgressMap={childProgressMap}
myIssuesOpts={myIssuesOpts}
sort={sort}
projectId={projectId}
sortLabel={sortLabel}
/>
@@ -506,6 +510,7 @@ export function BoardView({
childProgressMap={childProgressMap}
queryKey={assigneeGroupQueryKey}
filter={assigneeGroupFilter}
sort={sort}
projectId={projectId}
sortLabel={sortLabel}
/>
@@ -529,6 +534,7 @@ export function BoardView({
<BoardHiddenColumnsPanel
hiddenStatuses={hiddenStatuses}
myIssuesOpts={myIssuesOpts}
sort={sort}
/>
)}
</div>
@@ -551,6 +557,7 @@ const PaginatedAssigneeBoardColumn = memo(function PaginatedAssigneeBoardColumn(
childProgressMap,
queryKey,
filter,
sort,
projectId,
sortLabel,
}: {
@@ -560,6 +567,7 @@ const PaginatedAssigneeBoardColumn = memo(function PaginatedAssigneeBoardColumn(
childProgressMap?: Map<string, ChildProgress>;
queryKey: QueryKey;
filter: AssigneeGroupedIssuesFilter;
sort?: IssueSortParam;
projectId?: string;
sortLabel?: string | null;
}) {
@@ -571,6 +579,7 @@ const PaginatedAssigneeBoardColumn = memo(function PaginatedAssigneeBoardColumn(
},
queryKey,
filter,
sort,
);
return (
<BoardColumn
@@ -596,6 +605,7 @@ const PaginatedBoardColumn = memo(function PaginatedBoardColumn({
issueMap,
childProgressMap,
myIssuesOpts,
sort,
projectId,
sortLabel,
}: {
@@ -604,12 +614,14 @@ const PaginatedBoardColumn = memo(function PaginatedBoardColumn({
issueMap: Map<string, Issue>;
childProgressMap?: Map<string, ChildProgress>;
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
projectId?: string;
sortLabel?: string | null;
}) {
const { loadMore, hasMore, isLoading, total } = useLoadMoreByStatus(
group.status,
myIssuesOpts,
sort,
);
return (
<BoardColumn
@@ -639,20 +651,24 @@ const PaginatedBoardColumn = memo(function PaginatedBoardColumn({
function BoardHiddenColumnRow({
status,
myIssuesOpts,
sort,
}: {
status: IssueStatus;
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
}) {
const { total } = useLoadMoreByStatus(status, myIssuesOpts);
const { total } = useLoadMoreByStatus(status, myIssuesOpts, sort);
return <HiddenColumnRow status={status} total={total} />;
}
function BoardHiddenColumnsPanel({
hiddenStatuses,
myIssuesOpts,
sort,
}: {
hiddenStatuses: IssueStatus[];
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
}) {
return (
<HiddenColumnsPanel
@@ -662,6 +678,7 @@ function BoardHiddenColumnsPanel({
key={status}
status={status}
myIssuesOpts={myIssuesOpts}
sort={sort}
/>
)}
/>

View File

@@ -223,6 +223,7 @@ export function IssuesPage() {
hiddenStatuses={hiddenStatuses}
onMoveIssue={handleMoveIssue}
childProgressMap={childProgressMap}
sort={sort}
/>
) : viewMode === "swimlane" ? (
<SwimLaneView
@@ -232,9 +233,10 @@ export function IssuesPage() {
hiddenStatuses={hiddenStatuses}
onMoveIssue={handleMoveIssue}
childProgressMap={childProgressMap}
sort={sort}
/>
) : (
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} sort={sort} />
)}
</div>
)}

View File

@@ -7,7 +7,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/
import { Button } from "@multica/ui/components/ui/button";
import type { Issue, IssueStatus } from "@multica/core/types";
import { useLoadMoreByStatus } from "@multica/core/issues/mutations";
import type { MyIssuesFilter } from "@multica/core/issues/queries";
import type { IssueSortParam, MyIssuesFilter } from "@multica/core/issues/queries";
import { useModalStore } from "@multica/core/modals";
import { useViewStore } from "@multica/core/issues/stores/view-store-context";
import { useIssueSelectionStore } from "@multica/core/issues/stores/selection-store";
@@ -24,6 +24,7 @@ export function ListView({
childProgressMap = EMPTY_PROGRESS_MAP,
myIssuesScope,
myIssuesFilter,
sort,
projectId,
}: {
issues: Issue[];
@@ -32,6 +33,8 @@ export function ListView({
/** When set, per-status load-more targets the scoped cache instead of the workspace one. */
myIssuesScope?: string;
myIssuesFilter?: MyIssuesFilter;
/** Must match the sort the page queried with — embedded in the cache key. */
sort?: IssueSortParam;
/** When set, the per-section "+" pre-fills the project on the create form. */
projectId?: string;
}) {
@@ -86,6 +89,7 @@ export function ListView({
issues={issuesByStatus.get(status) ?? []}
childProgressMap={childProgressMap}
myIssuesOpts={myIssuesOpts}
sort={sort}
projectId={projectId}
/>
))}
@@ -99,12 +103,14 @@ function StatusAccordionItem({
issues,
childProgressMap,
myIssuesOpts,
sort,
projectId,
}: {
status: IssueStatus;
issues: Issue[];
childProgressMap: Map<string, ChildProgress>;
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
projectId?: string;
}) {
const { t } = useT("issues");
@@ -114,6 +120,7 @@ function StatusAccordionItem({
const { loadMore, hasMore, isLoading, total } = useLoadMoreByStatus(
status,
myIssuesOpts,
sort,
);
const issueIds = issues.map((i) => i.id);

View File

@@ -80,7 +80,7 @@ vi.mock("@multica/core/issues/config", () => ({
// as no-op divs and don't pull IntersectionObserver into JSDOM.
const mockLoadMore = vi.fn();
const useLoadMoreByStatusMock = vi.fn(
(_status: string, _opts?: unknown) => ({
(_status: string, _opts?: unknown, _sort?: unknown) => ({
total: 0,
loaded: 0,
hasMore: false,
@@ -92,8 +92,8 @@ vi.mock("@multica/core/issues/mutations", async (importOriginal) => {
const actual = await importOriginal<typeof import("@multica/core/issues/mutations")>();
return {
...actual,
useLoadMoreByStatus: (status: string, opts?: unknown) =>
useLoadMoreByStatusMock(status, opts),
useLoadMoreByStatus: (status: string, opts?: unknown, sort?: unknown) =>
useLoadMoreByStatusMock(status, opts, sort),
};
});

View File

@@ -23,7 +23,7 @@ import type { UpdateIssueRequest } from "@multica/core/types";
import { useViewStore, useViewStoreApi } from "@multica/core/issues/stores/view-store-context";
import { useWorkspacePaths } from "@multica/core/paths";
import { useLoadMoreByStatus } from "@multica/core/issues/mutations";
import type { MyIssuesFilter } from "@multica/core/issues/queries";
import type { IssueSortParam, MyIssuesFilter } from "@multica/core/issues/queries";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -161,6 +161,7 @@ export function SwimLaneView({
childProgressMap = EMPTY_PROGRESS_MAP,
myIssuesScope,
myIssuesFilter,
sort,
projectId,
}: {
issues: Issue[];
@@ -179,6 +180,8 @@ export function SwimLaneView({
childProgressMap?: Map<string, ChildProgress>;
myIssuesScope?: string;
myIssuesFilter?: MyIssuesFilter;
/** Must match the sort the page queried with — embedded in the cache key. */
sort?: IssueSortParam;
/** Pre-fills `project_id` on the create form for the in-cell "+" button. */
projectId?: string;
}) {
@@ -748,6 +751,7 @@ export function SwimLaneView({
sortedStatuses={sortedStatuses}
gridStyle={gridStyle}
myIssuesOpts={myIssuesOpts}
sort={sort}
/>
</div>
</div>
@@ -1031,10 +1035,12 @@ function SwimLaneLoadMoreRow({
sortedStatuses,
gridStyle,
myIssuesOpts,
sort,
}: {
sortedStatuses: IssueStatus[];
gridStyle: React.CSSProperties;
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
}) {
return (
<div style={gridStyle}>
@@ -1043,6 +1049,7 @@ function SwimLaneLoadMoreRow({
key={status}
status={status}
myIssuesOpts={myIssuesOpts}
sort={sort}
/>
))}
</div>
@@ -1052,11 +1059,13 @@ function SwimLaneLoadMoreRow({
function SwimLaneLoadMoreCell({
status,
myIssuesOpts,
sort,
}: {
status: IssueStatus;
myIssuesOpts?: { scope: string; filter: MyIssuesFilter };
sort?: IssueSortParam;
}) {
const { loadMore, hasMore, isLoading } = useLoadMoreByStatus(status, myIssuesOpts);
const { loadMore, hasMore, isLoading } = useLoadMoreByStatus(status, myIssuesOpts, sort);
if (!hasMore) return <div />;
return <InfiniteScrollSentinel onVisible={loadMore} loading={isLoading} />;
}

View File

@@ -273,6 +273,7 @@ export function MyIssuesPage() {
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
/>
) : viewMode === "swimlane" ? (
<SwimLaneView
@@ -284,6 +285,7 @@ export function MyIssuesPage() {
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
/>
) : (
<ListView
@@ -292,6 +294,7 @@ export function MyIssuesPage() {
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
/>
)}
</div>

View File

@@ -18,6 +18,7 @@ import {
projectGanttIssuesOptions,
childIssueProgressOptions,
type AssigneeGroupedIssuesFilter,
type IssueSortParam,
type MyIssuesFilter,
} from "@multica/core/issues/queries";
import { useUpdateIssue } from "@multica/core/issues/mutations";
@@ -116,6 +117,7 @@ function ProjectIssuesContent({
assigneeGroupFilter,
scope,
filter,
sort,
ganttIssues,
}: {
projectId: string;
@@ -125,6 +127,7 @@ function ProjectIssuesContent({
assigneeGroupFilter?: AssigneeGroupedIssuesFilter;
scope: string;
filter: MyIssuesFilter;
sort?: IssueSortParam;
ganttIssues: Issue[];
}) {
const { t } = useT("projects");
@@ -228,6 +231,7 @@ function ProjectIssuesContent({
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
projectId={projectId}
/>
)}
@@ -238,6 +242,7 @@ function ProjectIssuesContent({
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
projectId={projectId}
/>
)}
@@ -252,6 +257,7 @@ function ProjectIssuesContent({
childProgressMap={childProgressMap}
myIssuesScope={scope}
myIssuesFilter={filter}
sort={sort}
projectId={projectId}
/>
)}
@@ -351,6 +357,7 @@ function ProjectIssuesSurface({
assigneeGroupFilter={usesAssigneeBoard ? assigneeGroupFilter : undefined}
scope={scope}
filter={filter}
sort={sort}
ganttIssues={ganttIssues}
/>
<BatchActionToolbar />