Skip to content

Commit d4b35f6

Browse files
Merge pull request #478 from CodebilityDev/miguel/task-link
Miguel/task link
2 parents 6fb9d65 + 07f01ac commit d4b35f6

10 files changed

Lines changed: 236 additions & 28 deletions

File tree

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/KanbanBoard.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { memo, useCallback, useEffect, useMemo, useState } from "react";
3+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import Link from "next/link";
55
import KanbanBoardsSearch from "@/app/home/kanban/_components/KanbanBoardsSearch";
66
import { getMembers } from "@/app/home/projects/actions";
@@ -18,6 +18,7 @@ import KanbanColumnAddButton from "./kanban_modals/KanbanColumnAddButton";
1818
import KanbanColumnAddModal from "./kanban_modals/KanbanColumnAddModal";
1919
import KanbanBoardColumnContainer from "./KanbanBoardColumnContainer";
2020
import UserTaskFilter from "./UserTaskFilter";
21+
import { useKanbanTaskUrlModal } from "@/hooks/useKanbanTaskUrlModal";
2122

2223
interface KanbanBoardProps {
2324
boardData: KanbanBoardType & { kanban_columns: KanbanColumnType[] };
@@ -32,6 +33,9 @@ interface KanbanBoardProps {
3233
function KanbanBoard({ boardData, projectId }: KanbanBoardProps) {
3334
const user = useUserStore((state) => state.user);
3435

36+
// Sync the Kanban task modal with the URL ?taskId param
37+
useKanbanTaskUrlModal(boardData);
38+
3539
// User permissions - simple boolean checks following YAGNI principle
3640
const canAddColumn = user?.role_id === 1 || user?.role_id === 5;
3741
const canAddMember = canAddColumn;

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/KanbanColumn.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export default function KanbanColumn({
168168
toast.error(response.error || "Failed to update column name");
169169
}
170170
};
171-
171+
172172
return (
173173
<li
174174
ref={setColumnRef}

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/kanban_modals/KanbanTaskViewEditModal.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ReactNode, memo, useCallback } from "react";
44
import { useModal } from "@/hooks/use-modal";
55
import { ExtendedTask } from "@/types/home/codev";
6+
import { usePathname, useRouter } from "next/navigation";
67

78
interface Props {
89
children: ReactNode;
@@ -15,9 +16,12 @@ function KanbanTaskViewEditModal({
1516
task,
1617
onComplete,
1718
}: Props) {
19+
const router = useRouter();
20+
const pathname = usePathname();
1821
const { onOpen } = useModal();
1922

2023
const handleClick = useCallback(() => {
24+
router.push(`${pathname}?taskId=${task.id}`, { scroll: false });
2125
onOpen("taskViewModal", task, onComplete);
2226
}, [onOpen, task, onComplete]);
2327

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/tasks/TaskViewModal.tsx‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,12 @@ const TaskViewModal = ({
691691
<div className="flex items-center justify-between">
692692
<DialogHeader>
693693
<DialogTitle className="text-xl font-bold text-gray-900 dark:text-white">
694-
{task?.title}
694+
{task?.title}{" "}
695+
{task?.ticket_code && (
696+
<span className="ml-1 text-gray-500 dark:text-gray-400 font-normal">
697+
#{task.ticket_code}
698+
</span>
699+
)}
695700
</DialogTitle>
696701
</DialogHeader>
697702
{canModifyTask && (
@@ -968,6 +973,36 @@ const TaskViewModal = ({
968973

969974
<DialogFooter className="mt-4 flex justify-end">
970975
{/*Previous style: flex gap-6 sm:justify-end */}
976+
{/* Copy URL button */}
977+
<Button
978+
onClick={() => {
979+
if (!task?.id) {
980+
toast.error("Cannot copy URL: Task ID is missing");
981+
return;
982+
}
983+
984+
const baseUrl = window.location.origin; // gets the website domain
985+
const url = task.ticket_code
986+
? `${baseUrl}/home/kanban/ticket/${task.ticket_code}`
987+
: new URL(window.location.href).toString();
988+
989+
navigator.clipboard.writeText(url);
990+
toast.success("Task URL copied to clipboard");
991+
}}
992+
style={{
993+
backgroundColor: "#2563EB",
994+
color: "white",
995+
padding: "6px 16px",
996+
fontSize: "14px",
997+
borderRadius: "4px",
998+
border: "none",
999+
minWidth: "auto",
1000+
width: "auto",
1001+
}}
1002+
>
1003+
Copy URL
1004+
</Button>
1005+
9711006
{/* VIEWING MODE: Show Close when just viewing (no changes) */}
9721007
{!hasUnsavedChanges && (
9731008
<Button

‎apps/codebility/app/home/kanban/[projectId]/[id]/_services/query.ts‎

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,34 @@ import { createClientServerComponent } from "@/utils/supabase/server";
55
export const getBoardData = async (boardId: String) => {
66
try {
77
const supabase = await createClientServerComponent();
8-
8+
9+
// Fetch the board and its columns
910
const { data: boardData, error } = await supabase
10-
.from("kanban_boards")
11-
.select(
12-
`
13-
id,
14-
name,
15-
description,
16-
created_at,
17-
updated_at,
18-
project_id,
19-
kanban_columns (
11+
.from("kanban_boards")
12+
.select(`
2013
id,
2114
name,
22-
position,
15+
description,
2316
created_at,
24-
updated_at
25-
)
26-
`,
27-
)
28-
.eq("id", boardId)
29-
.single();
17+
updated_at,
18+
project_id,
19+
kanban_columns (
20+
id,
21+
name,
22+
position,
23+
created_at,
24+
updated_at
25+
)
26+
`)
27+
.eq("id", boardId)
28+
.single();
3029

3130
if (error) throw error;
3231

33-
// Fetch tasks separately with proper filtering
3432
if (boardData?.kanban_columns) {
3533
const columnIds = boardData.kanban_columns.map((col: any) => col.id);
36-
34+
35+
// Fetch tasks
3736
const { data: tasks, error: tasksError } = await supabase
3837
.from("tasks")
3938
.select(`
@@ -72,18 +71,31 @@ export const getBoardData = async (boardId: String) => {
7271

7372
if (tasksError) throw tasksError;
7473

74+
// Fetch ticket codes separately
75+
const taskIds = tasks?.map((task: any) => task.id) || [];
76+
const { data: ticketCodes, error: ticketError } = await supabase
77+
.from("task_ticket_codes")
78+
.select(`task_id, ticket_code`)
79+
.in("task_id", taskIds);
80+
81+
if (ticketError) throw ticketError;
82+
83+
// Merge ticket_code into tasks
84+
const tasksWithTicket = tasks?.map((task: any) => ({
85+
...task,
86+
ticket_code: ticketCodes?.find((t: any) => t.task_id === task.id)?.ticket_code || null,
87+
}));
88+
7589
// Attach tasks to their respective columns
7690
boardData.kanban_columns = boardData.kanban_columns.map((column: any) => ({
7791
...column,
78-
tasks: tasks?.filter((task: any) => task.kanban_column_id === column.id) || []
92+
tasks: tasksWithTicket?.filter((task: any) => task.kanban_column_id === column.id) || [],
7993
}));
8094
}
81-
82-
if (error) throw error;
83-
95+
8496
return boardData;
8597
} catch (error) {
8698
console.error(error);
8799
throw error;
88100
}
89-
};
101+
};

‎apps/codebility/app/home/kanban/[projectId]/[id]/page.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const mapTask = (task: any): Task => ({
3535
skill_category_id: task.skill_category_id,
3636
codev: task.codev,
3737
skill_category: task.skill_category,
38+
ticket_code: task.ticket_code || null,
3839
});
3940

4041
const mapColumn = (column: any): KanbanColumnType => ({
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use server";
2+
3+
import { createClientServerComponent } from "@/utils/supabase/server";
4+
5+
interface GetTaskDetailsResult {
6+
success: boolean;
7+
error?: string;
8+
data?: {
9+
project_id: string;
10+
board_id: string;
11+
task_id: string;
12+
};
13+
}
14+
15+
export const getTaskDetailsByTicketCode = async (
16+
ticket_code: string
17+
): Promise<GetTaskDetailsResult> => {
18+
const supabase = await createClientServerComponent();
19+
20+
if (!ticket_code) {
21+
return { success: false, error: "Ticket code is required" };
22+
}
23+
24+
// 1️⃣ Get task_id from task_ticket_codes
25+
const { data: taskTicket, error: taskTicketError } = await supabase
26+
.from("task_ticket_codes")
27+
.select("task_id")
28+
.eq("ticket_code", ticket_code)
29+
.single();
30+
31+
if (taskTicketError || !taskTicket) {
32+
return { success: false, error: taskTicketError?.message || "Ticket not found" };
33+
}
34+
35+
const task_id = taskTicket.task_id;
36+
37+
// 2️⃣ Get kanban_column_id from tasks
38+
const { data: task, error: taskError } = await supabase
39+
.from("tasks")
40+
.select("kanban_column_id")
41+
.eq("id", task_id)
42+
.single();
43+
44+
if (taskError || !task) {
45+
return { success: false, error: taskError?.message || "Task not found" };
46+
}
47+
48+
const kanban_column_id = task.kanban_column_id;
49+
50+
// 3️⃣ Get board_id and project_id from kanban_columns
51+
const { data: column, error: columnError } = await supabase
52+
.from("kanban_columns")
53+
.select("board_id")
54+
.eq("id", kanban_column_id)
55+
.single();
56+
57+
if (columnError || !column) {
58+
return { success: false, error: columnError?.message || "Column not found" };
59+
}
60+
61+
const board_id = column.board_id;
62+
63+
// 4️⃣ Get project_id from kanban_boards
64+
const { data: board, error: boardError } = await supabase
65+
.from("kanban_boards")
66+
.select("project_id")
67+
.eq("id", board_id)
68+
.single();
69+
70+
if (boardError || !board) {
71+
return { success: false, error: boardError?.message || "Board not found" };
72+
}
73+
74+
const project_id = board.project_id;
75+
76+
return {
77+
success: true,
78+
data: { task_id, board_id, project_id },
79+
};
80+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { redirect } from 'next/navigation';
2+
import { getTaskDetailsByTicketCode } from './actions';
3+
4+
interface PageProps {
5+
params: {
6+
ticketCode: string;
7+
};
8+
}
9+
10+
export default async function TicketPage({ params }: PageProps) {
11+
const { ticketCode } = params;
12+
13+
const ticketInfo = await getTaskDetailsByTicketCode(ticketCode);
14+
15+
// Determine where to redirect
16+
const destination = `/home/kanban/${ticketInfo.data?.project_id}/${ticketInfo.data?.board_id}?taskId=${ticketInfo.data?.task_id}`;
17+
18+
redirect(destination);
19+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useRef } from "react";
4+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
5+
import { useModal } from "@/hooks/use-modal";
6+
import { KanbanBoardType, KanbanColumnType } from "@/types/home/codev";
7+
8+
/**
9+
* Syncs the Kanban task modal with the URL ?taskId param.
10+
*/
11+
export function useKanbanTaskUrlModal(
12+
boardData: KanbanBoardType & { kanban_columns: KanbanColumnType[] }
13+
) {
14+
const { isOpen, type, onOpen } = useModal();
15+
const router = useRouter();
16+
const pathname = usePathname();
17+
const searchParams = useSearchParams();
18+
19+
// Flatten all tasks once
20+
const tasks = useMemo(
21+
() => boardData.kanban_columns.flatMap((col) => col.tasks ?? []),
22+
[boardData.kanban_columns]
23+
);
24+
25+
// Track whether we've already opened the modal from the URL
26+
const openedFromUrlRef = useRef(false);
27+
28+
useEffect(() => {
29+
const taskId = searchParams.get("taskId");
30+
31+
// Open modal only once from URL
32+
if (taskId && !openedFromUrlRef.current) {
33+
const task = tasks.find((t) => t.id === taskId);
34+
if (task) {
35+
onOpen("taskViewModal", task);
36+
openedFromUrlRef.current = true;
37+
}
38+
}
39+
40+
// Clear URL if modal closed
41+
if (!isOpen && searchParams.has("taskId")) {
42+
router.replace(pathname, { scroll: false });
43+
openedFromUrlRef.current = false; // reset so next URL param works
44+
}
45+
46+
// If modal type changes away from taskViewModal, remove param
47+
if (isOpen && type !== "taskViewModal" && searchParams.has("taskId")) {
48+
router.replace(pathname, { scroll: false });
49+
openedFromUrlRef.current = false;
50+
}
51+
}, [isOpen, type, tasks, onOpen, pathname, searchParams, router]);
52+
}

‎apps/codebility/types/home/codev.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ export interface Task {
346346
id: string;
347347
name: string;
348348
};
349+
ticket_code?: string;
349350
}
350351

351352
export interface ExtendedTask extends Task {

0 commit comments

Comments
 (0)