@@ -28,8 +28,10 @@ import {
2828 ChevronDown ,
2929 ChevronUp ,
3030 Ellipsis ,
31+ ExternalLink ,
3132 Loader2Icon ,
3233 MessageCircle ,
34+ Pencil ,
3335} from "lucide-react" ;
3436import 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