feat: add task detail URL handling and utility functions for taskId management
This commit is contained in:
@@ -3,7 +3,12 @@
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
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 { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +36,7 @@ import {
|
|||||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||||
import { BoardChatComposer } from "@/components/BoardChatComposer";
|
import { BoardChatComposer } from "@/components/BoardChatComposer";
|
||||||
import { TaskCustomFieldsEditor } from "./TaskCustomFieldsEditor";
|
import { TaskCustomFieldsEditor } from "./TaskCustomFieldsEditor";
|
||||||
|
import { buildUrlWithTaskId } from "./task-detail-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -702,6 +708,7 @@ LiveFeedCard.displayName = "LiveFeedCard";
|
|||||||
export default function BoardDetailPage() {
|
export default function BoardDetailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const boardIdParam = params?.boardId;
|
const boardIdParam = params?.boardId;
|
||||||
const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam;
|
const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam;
|
||||||
@@ -781,6 +788,7 @@ export default function BoardDetailPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [hasLoadedBoardSnapshot, setHasLoadedBoardSnapshot] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||||
const selectedTaskIdRef = useRef<string | null>(null);
|
const selectedTaskIdRef = useRef<string | null>(null);
|
||||||
@@ -1172,6 +1180,7 @@ export default function BoardDetailPage() {
|
|||||||
|
|
||||||
const loadBoard = useCallback(async () => {
|
const loadBoard = useCallback(async () => {
|
||||||
if (!isSignedIn || !boardId) return;
|
if (!isSignedIn || !boardId) return;
|
||||||
|
setHasLoadedBoardSnapshot(false);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsApprovalsLoading(true);
|
setIsApprovalsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -1225,6 +1234,7 @@ export default function BoardDetailPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsApprovalsLoading(false);
|
setIsApprovalsLoading(false);
|
||||||
|
setHasLoadedBoardSnapshot(true);
|
||||||
}
|
}
|
||||||
}, [boardId, isSignedIn]);
|
}, [boardId, isSignedIn]);
|
||||||
|
|
||||||
@@ -2339,12 +2349,21 @@ export default function BoardDetailPage() {
|
|||||||
setIsLiveFeedOpen(false);
|
setIsLiveFeedOpen(false);
|
||||||
const fullTask = tasksRef.current.find((item) => item.id === task.id);
|
const fullTask = tasksRef.current.find((item) => item.id === task.id);
|
||||||
if (!fullTask) return;
|
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;
|
selectedTaskIdRef.current = fullTask.id;
|
||||||
setSelectedTask(fullTask);
|
setSelectedTask(fullTask);
|
||||||
setIsDetailOpen(true);
|
setIsDetailOpen(true);
|
||||||
void loadComments(task.id);
|
void loadComments(task.id);
|
||||||
},
|
},
|
||||||
[loadComments],
|
[loadComments, pathname, router, searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedTaskDependencies = useMemo<DependencyBannerDependency[]>(() => {
|
const selectedTaskDependencies = useMemo<DependencyBannerDependency[]>(() => {
|
||||||
@@ -2398,15 +2417,38 @@ export default function BoardDetailPage() {
|
|||||||
}, [openComments, selectedTask, tasks]);
|
}, [openComments, selectedTask, tasks]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!taskIdFromUrl) return;
|
if (!hasLoadedBoardSnapshot) return;
|
||||||
|
if (!taskIdFromUrl) {
|
||||||
|
openedTaskIdFromUrlRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return;
|
if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return;
|
||||||
const exists = tasks.some((task) => task.id === taskIdFromUrl);
|
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;
|
openedTaskIdFromUrlRef.current = taskIdFromUrl;
|
||||||
openComments({ id: taskIdFromUrl });
|
openComments({ id: taskIdFromUrl });
|
||||||
}, [openComments, taskIdFromUrl, tasks]);
|
}, [
|
||||||
|
hasLoadedBoardSnapshot,
|
||||||
|
openComments,
|
||||||
|
pathname,
|
||||||
|
router,
|
||||||
|
searchParams,
|
||||||
|
taskIdFromUrl,
|
||||||
|
tasks,
|
||||||
|
]);
|
||||||
|
|
||||||
const closeComments = () => {
|
const closeComments = () => {
|
||||||
|
openedTaskIdFromUrlRef.current = null;
|
||||||
|
if (searchParams.get("taskId")) {
|
||||||
|
router.replace(buildUrlWithTaskId(pathname, searchParams, null), {
|
||||||
|
scroll: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsDetailOpen(false);
|
setIsDetailOpen(false);
|
||||||
selectedTaskIdRef.current = null;
|
selectedTaskIdRef.current = null;
|
||||||
setSelectedTask(null);
|
setSelectedTask(null);
|
||||||
|
|||||||
27
frontend/src/app/boards/[boardId]/task-detail-query.test.ts
Normal file
27
frontend/src/app/boards/[boardId]/task-detail-query.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
21
frontend/src/app/boards/[boardId]/task-detail-query.ts
Normal file
21
frontend/src/app/boards/[boardId]/task-detail-query.ts
Normal 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)}`;
|
||||||
Reference in New Issue
Block a user