Adds server-side pagination for the Archived task column, sorted by most-recently-archived first, with a Show more control on the dashboard.
- Add ArchiveDatabase.listPage and TaskStore.listArchivedTasks for a bounded SQL LIMIT/OFFSET read ordered by archivedAt DESC
- Add GET /tasks/archived route for paged archive fetches, leaving the legacy merged listTasks({includeArchived}) path unchanged
- Wire useTasks.loadArchivedTasks to fetch page 1 on first Archived-column expand and loadMoreArchivedTasks for subsequent pages
- Add a "Show more" affordance in Column.tsx/Board.tsx/MainContent.tsx to trigger loading additional archived pages
- Extend taskSorting.ts to keep archived task ordering stable with the new paged data
- Add core and dashboard tests covering archive pagination and store/route behavior
- Add changeset for @runfusion/fusion (minor) and update docs/storage.md and docs/dashboard-guide.md
Files changed:
.changeset/FN-7659-archived-pagination.md | 7 +
docs/dashboard-guide.md | 4 +-
docs/storage.md | 7 +
packages/core/src/__tests__/archive-db-pagination.test.ts | 94 ++++++++
packages/core/src/__tests__/store-archive-search.test.ts | 63 ++++++
packages/core/src/archive-db.ts | 18 ++
packages/core/src/store.ts | 32 +++
packages/dashboard/app/App.tsx | 5 +-
packages/dashboard/app/api/legacy.ts | 19 ++
packages/dashboard/app/components/Board.tsx | 19 +-
packages/dashboard/app/components/Column.tsx | 48 ++++-
packages/dashboard/app/components/__tests__/Column.test.tsx | 41 ++++
packages/dashboard/app/components/__tests__/taskSorting.test.ts | 27 +++
packages/dashboard/app/components/dashboard/MainContent.tsx | 9 +
packages/dashboard/app/components/dashboard/types.ts | 6 +
packages/dashboard/app/components/taskSorting.ts | 16 ++
packages/dashboard/app/hooks/__tests__/useTasks.test.ts | 236 ++++++++++++++++++++-
packages/dashboard/app/hooks/useTasks.ts | 173 ++++++++++++++-
packages/dashboard/app/test/mockApi.ts | 3 +
packages/dashboard/src/routes/__tests__/tasks-archived-pagination.test.ts | 94 ++++++++
packages/dashboard/src/routes/register-task-workflow-routes.ts | 32 +++
21 files changed, 930 insertions(+), 23 deletions(-)
Fusion-Task-Id: FN-7659
Fusion-Task-Lineage: 7a5a1f62-277c-4f29-883c-62e75b269bc5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import type { Task, Column } from "@fusion/core";
|
|
|
|
export type DoneColumnSortMode = "completion-date-desc" | "task-id-desc";
|
|
|
|
function getTaskPriorityRank(priority: Task["priority"] | null | undefined): number {
|
|
if (priority === "urgent") return 3;
|
|
if (priority === "high") return 2;
|
|
if (priority === "low") return 0;
|
|
return 1;
|
|
}
|
|
|
|
function compareTaskPriority(a: Task["priority"] | null | undefined, b: Task["priority"] | null | undefined): number {
|
|
return getTaskPriorityRank(b) - getTaskPriorityRank(a);
|
|
}
|
|
|
|
function getTaskIdNumericToken(id: string): number | null {
|
|
const token = id.slice(id.lastIndexOf("-") + 1);
|
|
if (!/^\d+$/.test(token)) return null;
|
|
const parsed = Number.parseInt(token, 10);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function compareTaskIdNumeric(a: string, b: string): number {
|
|
const aNum = getTaskIdNumericToken(a);
|
|
const bNum = getTaskIdNumericToken(b);
|
|
|
|
if (aNum !== null && bNum !== null && aNum !== bNum) {
|
|
return aNum - bNum;
|
|
}
|
|
|
|
return a.localeCompare(b);
|
|
}
|
|
|
|
function compareTaskIdNumericDesc(a: string, b: string): number {
|
|
const aNum = getTaskIdNumericToken(a);
|
|
const bNum = getTaskIdNumericToken(b);
|
|
|
|
if (aNum !== null && bNum !== null && aNum !== bNum) {
|
|
return bNum - aNum;
|
|
}
|
|
|
|
return b.localeCompare(a);
|
|
}
|
|
|
|
function getDoneSortTimestamp(task: Task): number {
|
|
const timestamp = task.columnMovedAt ?? task.updatedAt ?? task.createdAt;
|
|
const parsed = Date.parse(timestamp);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function isMergeActiveStatus(status: string | null | undefined): boolean {
|
|
return status === "merging" || status === "merging-pr" || status === "merging-fix";
|
|
}
|
|
|
|
export function sortTasksForDisplayColumn(
|
|
tasks: readonly Task[],
|
|
column: Column,
|
|
doneSortMode: DoneColumnSortMode = "completion-date-desc",
|
|
isArchivedColumn: boolean = column === "archived",
|
|
): Task[] {
|
|
/*
|
|
FNXC:ArchivePagination 2026-07-08-00:00:
|
|
FN-7659 — the Archived column must render newest-first (`archivedAt DESC`),
|
|
the exact order the paginated `GET /tasks/archived` read and useTasks'
|
|
page-merge already produce. The generic priority+task-id sort below would
|
|
silently undo that ordering (it ran for every non-todo/non-done column,
|
|
including archived, before this fix), so archived columns pass through
|
|
unsorted — both the legacy literal `"archived"` column id and, for
|
|
workflow-mode custom archived columns, the caller-supplied
|
|
`isArchivedColumn` flag (derived from the column's `archived` trait flag).
|
|
*/
|
|
if (isArchivedColumn) {
|
|
return [...tasks];
|
|
}
|
|
|
|
if (column === "todo") {
|
|
return [...tasks].sort((a, b) => {
|
|
const priorityCmp = compareTaskPriority(a.priority, b.priority);
|
|
if (priorityCmp !== 0) return priorityCmp;
|
|
if (a.createdAt !== b.createdAt) return a.createdAt.localeCompare(b.createdAt);
|
|
return compareTaskIdNumeric(a.id, b.id);
|
|
});
|
|
}
|
|
|
|
return [...tasks].sort((a, b) => {
|
|
if (column === "done") {
|
|
/*
|
|
FNXC:DoneColumnSorting 2026-06-29-14:48:
|
|
Done keeps completion-date descending as the default for existing board, lane, and list callers while supporting an explicit task-id descending mode for users who need newest FN ids first.
|
|
*/
|
|
if (doneSortMode === "task-id-desc") {
|
|
return compareTaskIdNumericDesc(a.id, b.id);
|
|
}
|
|
const timestampCmp = getDoneSortTimestamp(b) - getDoneSortTimestamp(a);
|
|
if (timestampCmp !== 0) return timestampCmp;
|
|
return compareTaskIdNumeric(a.id, b.id);
|
|
}
|
|
|
|
if (column === "in-review") {
|
|
const aIsMerging = isMergeActiveStatus(a.status);
|
|
const bIsMerging = isMergeActiveStatus(b.status);
|
|
if (aIsMerging !== bIsMerging) return aIsMerging ? -1 : 1;
|
|
}
|
|
|
|
const priorityCmp = compareTaskPriority(a.priority, b.priority);
|
|
if (priorityCmp !== 0) return priorityCmp;
|
|
return compareTaskIdNumeric(a.id, b.id);
|
|
});
|
|
}
|