@@ -277,6 +277,9 @@ class Views extends TableView {
277277 }
278278 if ( ! columns [ key ] ) {
279279 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' ;
280283 }
281284 const width = computeWidth ( val ) ;
282285 if ( width > columns [ key ] . width && columns [ key ] . width < 200 ) {
@@ -550,12 +553,41 @@ class Views extends TableView {
550553 }
551554
552555 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+ } ) ;
559591 }
560592
561593 renderEmpty ( ) {
@@ -823,14 +855,76 @@ class Views extends TableView {
823855 `browser/${ className } ?filters=${ encodeURIComponent ( filters ) } ` ,
824856 true
825857 ) ,
826- '_blank'
858+ '_blank' ,
859+ 'noopener,noreferrer'
827860 ) ;
828861 }
829862
830863 handleValueClick ( value ) {
831864 this . setState ( { viewValue : value } ) ;
832865 }
833866
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+
834928 showNote ( message , isError ) {
835929 if ( ! message ) {
836930 return ;
0 commit comments