Skip to content

Commit 88a2354

Browse files
author
raineer
committed
task view modal PR link improvement
1 parent b5ef898 commit 88a2354

1 file changed

Lines changed: 124 additions & 45 deletions

File tree

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

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import {
2828
ChevronDown,
2929
ChevronUp,
3030
Ellipsis,
31+
ExternalLink,
3132
Loader2Icon,
3233
MessageCircle,
34+
Pencil,
3335
} from "lucide-react";
3436
import toast from "react-hot-toast";
3537

@@ -317,6 +319,11 @@ const TaskViewModal = ({
317319

318320
const [prLink, setPrLink] = useState("");
319321
const [originalPrLink, setOriginalPrLink] = useState("");
322+
323+
// NEW: Controls whether PR Link shows as hyperlink (false) or input (true)
324+
// Starts as false so a saved link renders as a clickable <a> tag immediately
325+
const [isEditingPrLink, setIsEditingPrLink] = useState(false);
326+
320327
const [supabase, setSupabase] = useState<any>(null);
321328
const [boardId, setBoardId] = useState<string>("");
322329

@@ -332,7 +339,9 @@ const TaskViewModal = ({
332339

333340
const [manualSaveChanges, setManualSaveChanges] = useState(false);
334341
const [isSavingChanges, setIsSavingChanges] = useState(false);
335-
const [pendingAssigneeId, setPendingAssigneeId] = useState<string | undefined>(undefined);
342+
const [pendingAssigneeId, setPendingAssigneeId] = useState<
343+
string | undefined
344+
>(undefined);
336345

337346
const isModalOpen = isOpen && type === "taskViewModal";
338347
const task = data as Task | null;
@@ -404,7 +413,7 @@ const TaskViewModal = ({
404413
fetchBoardId();
405414
}, [task?.kanban_column_id, supabase]);
406415

407-
// FIXED: Consolidated state reset logic with single execution flow
416+
// Reset all state when a different task is opened
408417
useEffect(() => {
409418
if (!task?.id || !supabase) return;
410419

@@ -415,7 +424,11 @@ const TaskViewModal = ({
415424
setPendingAssigneeId(undefined);
416425
setForceRefreshKey(Date.now().toString());
417426

418-
// Fetch and set assignee in one go
427+
// NEW: If task already has a PR link, show it as hyperlink (not editing mode)
428+
// If no PR link, go straight to edit mode so user can enter one
429+
setIsEditingPrLink(!taskPrLink);
430+
431+
// Fetch and set assignee
419432
const assigneeId = task.codev_id || task?.codev?.id;
420433
if (assigneeId) {
421434
supabase
@@ -495,18 +508,19 @@ const TaskViewModal = ({
495508
toast.success("PR Link updated successfully");
496509
setOriginalPrLink(prLink);
497510
if (task) task.pr_link = prLink;
511+
512+
// NEW: After successful save, switch back to hyperlink display mode
513+
setIsEditingPrLink(false);
498514
} else {
499515
toast.error(response.error || "Failed to update PR Link");
500516
}
501517

502518
setUpdateLoading(false);
503519
};
504520

505-
// FIXED: Improved task completion with better error handling and data consistency
506521
const handleMarkAsDone = async () => {
507522
if (!task) return;
508523

509-
// Validate required fields before attempting completion
510524
if (!task.id) {
511525
toast.error("Task ID is missing");
512526
return;
@@ -521,17 +535,11 @@ const TaskViewModal = ({
521535

522536
console.log("Attempting to complete task with ID:", task.id);
523537

524-
// Optimistically remove the task from UI
525538
removeTaskOptimistic(task.id);
526-
527-
// Close modal for better UX
528539
onClose();
529-
530-
// Show optimistic success message
531540
toast.success("Completing task...");
532541

533542
try {
534-
// Pass the original task object - let the server action handle field extraction
535543
const result = await completeTask(task);
536544

537545
console.log("Task completion result:", result);
@@ -543,12 +551,10 @@ const TaskViewModal = ({
543551
onComplete(task.id);
544552
}
545553

546-
// Refresh data in background
547554
setTimeout(() => {
548555
fetchBoardData();
549556
}, 1000);
550557
} else {
551-
// Revert optimistic update
552558
const errorMessage = result.error || "Failed to complete task";
553559
console.error("Task completion failed:", errorMessage, result);
554560
toast.error(errorMessage);
@@ -590,14 +596,15 @@ const TaskViewModal = ({
590596
setForceRefreshKey(`${Date.now()}-${Math.random()}`);
591597
};
592598

593-
// FIXED: Simplified close handler with cleaner state reset
594599
const handleClose = () => {
595600
if (hasUnsavedChanges) {
596601
setManualSaveChanges(false);
597602
setPendingAssigneeId(undefined);
598603
setPrLink(originalPrLink);
599604

600-
// Reset assignee to original state
605+
// NEW: Reset edit mode — if original has a link, go back to hyperlink view
606+
setIsEditingPrLink(!originalPrLink);
607+
601608
if (task?.codev_id || task?.codev?.id) {
602609
const assigneeId = task.codev_id || task.codev?.id || "";
603610
if (supabase && assigneeId) {
@@ -626,7 +633,7 @@ const TaskViewModal = ({
626633

627634
try {
628635
if (hasPrLinkChanges) {
629-
await handleUpdate();
636+
await handleUpdate(); // handleUpdate already switches to hyperlink view on success
630637
}
631638

632639
if (manualSaveChanges) {
@@ -793,36 +800,108 @@ const TaskViewModal = ({
793800
/>
794801
</div>
795802

803+
{/* ============================================================
804+
PR LINK SECTION — CBP-58
805+
3 states:
806+
1. Has link, view mode → full-width clickable URL + "Edit PR Link" button below
807+
2. No link → empty state label + "Add PR Link" button
808+
3. Editing → full-width input (stacked), Update + Cancel buttons below
809+
============================================================ */}
796810
<div className="space-y-2">
797811
<Label className="text-sm font-medium">PR Link</Label>
798-
<div
799-
className={`${hasPrLinkChanges ? "flex items-center space-x-2" : ""}`}
800-
>
801-
<Input
802-
value={prLink}
803-
onChange={(e) => setPrLink(e.target.value)}
804-
onBlur={() => {
805-
if (!prLink.trim() && task?.pr_link) {
806-
setPrLink(task.pr_link);
807-
}
808-
}}
809-
className="text-grey-100 bg-light-900 dark:bg-dark-200 dark:text-light-900 flex-1 border border-gray-300 focus:border-blue-500"
810-
placeholder="Enter PR Link..."
811-
/>
812-
{hasPrLinkChanges && (
813-
<Button
814-
variant="outline"
815-
onClick={handleUpdate}
816-
className="flex-1 bg-blue-600 text-white hover:bg-blue-700"
817-
disabled={updateLoading}
812+
813+
{/* STATE 1: Saved PR link exists and NOT in edit mode → show full URL as hyperlink */}
814+
{!isEditingPrLink && prLink ? (
815+
<div className="flex flex-col gap-2">
816+
{/* Full-width link row — user can clearly see and click the URL */}
817+
<a
818+
href={prLink}
819+
target="_blank"
820+
rel="noopener noreferrer"
821+
className="flex w-full items-center gap-2 overflow-hidden rounded-md border border-blue-300 bg-blue-50 px-3 py-2 text-sm text-blue-700 transition-colors hover:border-blue-500 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/40"
822+
title={`Open: ${prLink}`}
818823
>
819-
{updateLoading && (
820-
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
824+
{/* Icon stays fixed width, URL text fills remaining space */}
825+
<ExternalLink className="h-4 w-4 shrink-0 text-blue-500" />
826+
<span className="min-w-0 flex-1 truncate font-medium">{prLink}</span>
827+
<span className="shrink-0 text-xs text-blue-400">↗ Open</span>
828+
</a>
829+
830+
{/* Edit button — only for users with modify permission */}
831+
{canModifyTask && (
832+
<button
833+
type="button"
834+
onClick={() => setIsEditingPrLink(true)}
835+
className="flex w-fit items-center gap-1 text-xs text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
836+
>
837+
<Pencil className="h-3 w-3" />
838+
Edit PR Link
839+
</button>
840+
)}
841+
</div>
842+
) : isEditingPrLink ? (
843+
/* STATE 3: Currently editing — full-width input stacked above action buttons */
844+
<div className="flex flex-col gap-2">
845+
{/* Full-width input — no competition with buttons */}
846+
<Input
847+
autoFocus
848+
value={prLink}
849+
onChange={(e) => setPrLink(e.target.value)}
850+
className="w-full border border-blue-400 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:bg-gray-800 dark:text-white"
851+
placeholder="https://github.com/org/repo/pull/123"
852+
/>
853+
854+
{/* Action buttons row — below the input so input keeps full width */}
855+
<div className="flex items-center gap-2">
856+
<Button
857+
onClick={handleUpdate}
858+
disabled={updateLoading || !prLink.trim()}
859+
className="bg-blue-600 px-4 text-white hover:bg-blue-700 disabled:opacity-50"
860+
size="sm"
861+
>
862+
{updateLoading && (
863+
<Loader2Icon className="mr-2 h-3 w-3 animate-spin" />
864+
)}
865+
Update PR Link
866+
</Button>
867+
868+
{/* Cancel only shown when there was an original link to go back to */}
869+
{originalPrLink && (
870+
<button
871+
type="button"
872+
onClick={() => {
873+
setPrLink(originalPrLink);
874+
setIsEditingPrLink(false);
875+
}}
876+
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
877+
>
878+
Cancel
879+
</button>
821880
)}
822-
Update
823-
</Button>
824-
)}
825-
</div>
881+
</div>
882+
</div>
883+
) : (
884+
/* STATE 2: No PR link set — clear empty state so user knows it's editable */
885+
<div className="flex flex-col gap-2">
886+
{/* Empty state — dashed border signals "add something here" */}
887+
<div className="flex items-center gap-2 rounded-md border border-dashed border-gray-300 bg-gray-50 px-3 py-2 text-sm text-gray-400 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-500">
888+
<ExternalLink className="h-4 w-4 shrink-0" />
889+
<span>No PR link set</span>
890+
</div>
891+
892+
{/* Add button — only for users with modify permission */}
893+
{canModifyTask && (
894+
<button
895+
type="button"
896+
onClick={() => setIsEditingPrLink(true)}
897+
className="flex w-fit items-center gap-1 text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
898+
>
899+
<Pencil className="h-3 w-3" />
900+
Add PR Link
901+
</button>
902+
)}
903+
</div>
904+
)}
826905
</div>
827906

828907
<div className="space-y-2">
@@ -845,7 +924,7 @@ const TaskViewModal = ({
845924
</div>
846925
</div>
847926

848-
{/* FIXED: Primary Assignee - Now allows modification even when assigned */}
927+
{/* Primary Assignee - allows modification even when assigned */}
849928
<div className="space-y-2">
850929
<Label className="text-sm font-medium">Primary Assignee</Label>
851930
{canModifyTask ? (
@@ -926,7 +1005,7 @@ const TaskViewModal = ({
9261005
</div>
9271006
)}
9281007

929-
{/* UPDATED: Description Section with KanbanRichTextDisplay */}
1008+
{/* Description Section */}
9301009
<div className="space-y-2">
9311010
<Label className="text-sm font-medium">Description</Label>
9321011
<div className="rounded-md border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
@@ -965,7 +1044,7 @@ const TaskViewModal = ({
9651044

9661045
<div className="space-y-2">
9671046
<Label className="text-sm font-medium">Created At</Label>
968-
<div className="bg-light-900 rounded-md px-3 py-2 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-200">
1047+
<div className="bg-light-900 rounded-md px-3 py-2 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-200">
9691048
{task?.created_at
9701049
? new Date(task.created_at).toLocaleString()
9711050
: "Unknown"}

0 commit comments

Comments
 (0)