feat: add task detail URL handling and utility functions for taskId management

This commit is contained in:
Abhimanyu Saharan
2026-02-13 22:06:37 +05:30
parent 372b4e191c
commit 619f77286f
3 changed files with 95 additions and 5 deletions

View File

@@ -3,7 +3,12 @@
export const dynamic = "force-dynamic";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import {
useParams,
usePathname,
useRouter,
useSearchParams,
} from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
import {
@@ -31,6 +36,7 @@ import {
import { DashboardShell } from "@/components/templates/DashboardShell";
import { BoardChatComposer } from "@/components/BoardChatComposer";
import { TaskCustomFieldsEditor } from "./TaskCustomFieldsEditor";
import { buildUrlWithTaskId } from "./task-detail-query";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -702,6 +708,7 @@ LiveFeedCard.displayName = "LiveFeedCard";
export default function BoardDetailPage() {
const router = useRouter();
const params = useParams();
const pathname = usePathname();
const searchParams = useSearchParams();
const boardIdParam = params?.boardId;
const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam;
@@ -781,6 +788,7 @@ export default function BoardDetailPage() {
null,
);
const [isLoading, setIsLoading] = useState(false);
const [hasLoadedBoardSnapshot, setHasLoadedBoardSnapshot] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const selectedTaskIdRef = useRef<string | null>(null);
@@ -1172,6 +1180,7 @@ export default function BoardDetailPage() {
const loadBoard = useCallback(async () => {
if (!isSignedIn || !boardId) return;
setHasLoadedBoardSnapshot(false);
setIsLoading(true);
setIsApprovalsLoading(true);
setError(null);
@@ -1225,6 +1234,7 @@ export default function BoardDetailPage() {
} finally {
setIsLoading(false);
setIsApprovalsLoading(false);
setHasLoadedBoardSnapshot(true);
}
}, [boardId, isSignedIn]);
@@ -2339,12 +2349,21 @@ export default function BoardDetailPage() {
setIsLiveFeedOpen(false);
const fullTask = tasksRef.current.find((item) => item.id === task.id);
if (!fullTask) return;
const currentTaskIdFromUrl = searchParams.get("taskId");
if (currentTaskIdFromUrl !== fullTask.id) {
router.replace(
buildUrlWithTaskId(pathname, searchParams, fullTask.id),
{
scroll: false,
},
);
}
selectedTaskIdRef.current = fullTask.id;
setSelectedTask(fullTask);
setIsDetailOpen(true);
void loadComments(task.id);
},
[loadComments],
[loadComments, pathname, router, searchParams],
);
const selectedTaskDependencies = useMemo<DependencyBannerDependency[]>(() => {
@@ -2398,15 +2417,38 @@ export default function BoardDetailPage() {
}, [openComments, selectedTask, tasks]);
useEffect(() => {
if (!taskIdFromUrl) return;
if (!hasLoadedBoardSnapshot) return;
if (!taskIdFromUrl) {
openedTaskIdFromUrlRef.current = null;
return;
}
if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return;
const exists = tasks.some((task) => task.id === taskIdFromUrl);
if (!exists) return;
if (!exists) {
router.replace(buildUrlWithTaskId(pathname, searchParams, null), {
scroll: false,
});
return;
}
openedTaskIdFromUrlRef.current = taskIdFromUrl;
openComments({ id: taskIdFromUrl });
}, [openComments, taskIdFromUrl, tasks]);
}, [
hasLoadedBoardSnapshot,
openComments,
pathname,
router,
searchParams,
taskIdFromUrl,
tasks,
]);
const closeComments = () => {
openedTaskIdFromUrlRef.current = null;
if (searchParams.get("taskId")) {
router.replace(buildUrlWithTaskId(pathname, searchParams, null), {
scroll: false,
});
}
setIsDetailOpen(false);
selectedTaskIdRef.current = null;
setSelectedTask(null);

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { buildUrlWithTaskId, withTaskIdSearchParam } from "./task-detail-query";
describe("task-detail-query", () => {
it("adds taskId when absent", () => {
expect(withTaskIdSearchParam("", "task-1")).toBe("?taskId=task-1");
});
it("replaces taskId while preserving other params", () => {
expect(withTaskIdSearchParam("view=list&taskId=old", "task-2")).toBe(
"?view=list&taskId=task-2",
);
});
it("removes taskId while preserving other params", () => {
expect(withTaskIdSearchParam("view=list&taskId=old", null)).toBe(
"?view=list",
);
});
it("builds full url with taskId param updates", () => {
expect(
buildUrlWithTaskId("/boards/board-1", "filter=active", "task-1"),
).toBe("/boards/board-1?filter=active&taskId=task-1");
});
});

View File

@@ -0,0 +1,21 @@
type SearchParamsInput = string | { toString(): string };
export const withTaskIdSearchParam = (
searchParams: SearchParamsInput,
taskId: string | null,
): string => {
const params = new URLSearchParams(searchParams.toString());
if (taskId) {
params.set("taskId", taskId);
} else {
params.delete("taskId");
}
const next = params.toString();
return next ? `?${next}` : "";
};
export const buildUrlWithTaskId = (
pathname: string,
searchParams: SearchParamsInput,
taskId: string | null,
): string => `${pathname}${withTaskIdSearchParam(searchParams, taskId)}`;