@@ -277,6 +277,9 @@ class Views extends TableView {
277
277
}
278
278
if ( ! columns [ key ] ) {
279
279
columns [ key ] = { type, width : Math . min ( computeWidth ( key ) , 200 ) } ;
280
+ } else if ( type === 'Pointer' && columns [ key ] . type !== 'Pointer' ) {
281
+ // If we find a pointer value, upgrade the column type to Pointer
282
+ columns [ key ] . type = 'Pointer' ;
280
283
}
281
284
const width = computeWidth ( val ) ;
282
285
if ( width > columns [ key ] . width && columns [ key ] . width < 200 ) {
@@ -550,12 +553,41 @@ class Views extends TableView {
550
553
}
551
554
552
555
renderHeaders ( ) {
553
- return this . state . order . map ( ( { name, width } , i ) => (
554
- < div key = { name } className = { styles . headerWrap } style = { { width } } >
555
- { name }
556
- < DragHandle className = { styles . handle } onDrag = { delta => this . handleResize ( i , delta ) } />
557
- </ div >
558
- ) ) ;
556
+ return this . state . order . map ( ( { name, width } , i ) => {
557
+ const columnType = this . state . columns [ name ] ?. type ;
558
+ const isPointerColumn = columnType === 'Pointer' ;
559
+
560
+ return (
561
+ < div key = { name } className = { styles . headerWrap } style = { { width } } >
562
+ < span className = { styles . headerText } >
563
+ < span className = { styles . headerLabel } > { name } </ span >
564
+ { isPointerColumn && (
565
+ < button
566
+ type = "button"
567
+ className = { styles . pointerIcon }
568
+ onClick = { ( e ) => {
569
+ e . stopPropagation ( ) ;
570
+ e . preventDefault ( ) ;
571
+ this . handleOpenAllPointers ( name ) ;
572
+ // Remove focus after action to follow UX best practices
573
+ e . currentTarget . blur ( ) ;
574
+ } }
575
+ aria-label = { `Open all pointers in ${ name } column in new tabs` }
576
+ title = "Open all pointers in new tabs"
577
+ >
578
+ < Icon
579
+ name = "right-outline"
580
+ width = { 20 }
581
+ height = { 20 }
582
+ fill = "white"
583
+ />
584
+ </ button >
585
+ ) }
586
+ </ span >
587
+ < DragHandle className = { styles . handle } onDrag = { delta => this . handleResize ( i , delta ) } />
588
+ </ div >
589
+ ) ;
590
+ } ) ;
559
591
}
560
592
561
593
renderEmpty ( ) {
@@ -823,14 +855,76 @@ class Views extends TableView {
823
855
`browser/${ className } ?filters=${ encodeURIComponent ( filters ) } ` ,
824
856
true
825
857
) ,
826
- '_blank'
858
+ '_blank' ,
859
+ 'noopener,noreferrer'
827
860
) ;
828
861
}
829
862
830
863
handleValueClick ( value ) {
831
864
this . setState ( { viewValue : value } ) ;
832
865
}
833
866
867
+ handleOpenAllPointers ( columnName ) {
868
+ const data = this . tableData ( ) ;
869
+ const pointers = data
870
+ . map ( row => row [ columnName ] )
871
+ . filter ( value => value && value . __type === 'Pointer' && value . className && value . objectId ) ;
872
+
873
+ // Open each unique pointer in a new tab
874
+ const uniquePointers = new Map ( ) ;
875
+ pointers . forEach ( pointer => {
876
+ // Use a more collision-proof key format with explicit separators
877
+ const key = `className:${ pointer . className } |objectId:${ pointer . objectId } ` ;
878
+ if ( ! uniquePointers . has ( key ) ) {
879
+ uniquePointers . set ( key , pointer ) ;
880
+ }
881
+ } ) ;
882
+
883
+ if ( uniquePointers . size === 0 ) {
884
+ this . showNote ( 'No pointers found in this column' , true ) ;
885
+ return ;
886
+ }
887
+
888
+ const pointersArray = Array . from ( uniquePointers . values ( ) ) ;
889
+
890
+ // Confirm for large numbers of tabs to prevent overwhelming the user
891
+ if ( pointersArray . length > 10 ) {
892
+ const confirmMessage = `This will open ${ pointersArray . length } new tabs. This might overwhelm your browser. Continue?` ;
893
+ if ( ! confirm ( confirmMessage ) ) {
894
+ return ;
895
+ }
896
+ }
897
+
898
+ // Open all tabs immediately to maintain user activation context
899
+ let errorCount = 0 ;
900
+
901
+ pointersArray . forEach ( ( pointer ) => {
902
+ try {
903
+ const filters = JSON . stringify ( [ { field : 'objectId' , constraint : 'eq' , compareTo : pointer . objectId } ] ) ;
904
+ const url = generatePath (
905
+ this . context ,
906
+ `browser/${ pointer . className } ?filters=${ encodeURIComponent ( filters ) } ` ,
907
+ true
908
+ ) ;
909
+ window . open ( url , '_blank' , 'noopener,noreferrer' ) ;
910
+ // Note: window.open with security attributes may return null even when successful,
911
+ // so we assume success unless an exception is thrown
912
+ } catch ( error ) {
913
+ console . error ( 'Failed to open tab for pointer:' , pointer , error ) ;
914
+ errorCount ++ ;
915
+ }
916
+ } ) ;
917
+
918
+ // Show result notification
919
+ if ( errorCount === 0 ) {
920
+ this . showNote ( `Opened ${ pointersArray . length } pointer${ pointersArray . length > 1 ? 's' : '' } in new tab${ pointersArray . length > 1 ? 's' : '' } ` , false ) ;
921
+ } else if ( errorCount < pointersArray . length ) {
922
+ this . showNote ( `Opened ${ pointersArray . length - errorCount } of ${ pointersArray . length } tabs. ${ errorCount } failed to open.` , true ) ;
923
+ } else {
924
+ this . showNote ( 'Unable to open tabs. Please allow popups for this site and try again.' , true ) ;
925
+ }
926
+ }
927
+
834
928
showNote ( message , isError ) {
835
929
if ( ! message ) {
836
930
return ;
0 commit comments