Skip to content

Commit b8033a4

Browse files
authored
feat: Add button to view table to open all pointers of a column in new browser tabs (#2976)
1 parent 52a858b commit b8033a4

File tree

2 files changed

+157
-7
lines changed

2 files changed

+157
-7
lines changed

src/dashboard/Data/Views/Views.react.js

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

src/dashboard/Data/Views/Views.scss

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,62 @@
1818
text-overflow: ellipsis;
1919
}
2020

21+
.headerText {
22+
display: flex;
23+
align-items: center;
24+
justify-content: space-between;
25+
width: 100%;
26+
min-width: 0; // Enable text truncation in flex containers
27+
}
28+
29+
.headerLabel {
30+
overflow: hidden;
31+
text-overflow: ellipsis;
32+
white-space: nowrap;
33+
flex: 1;
34+
min-width: 0; // Enable text truncation
35+
}
36+
37+
.pointerIcon {
38+
// Reset button styles
39+
border: none;
40+
padding: 0;
41+
font: inherit;
42+
color: inherit;
43+
background: rgba(255, 255, 255, 0.2);
44+
45+
// Custom styles
46+
cursor: pointer;
47+
opacity: 0.7;
48+
transition: opacity 0.2s ease;
49+
z-index: 10;
50+
pointer-events: auto;
51+
display: inline-flex;
52+
align-items: center;
53+
justify-content: center;
54+
height: 20px;
55+
width: 20px;
56+
border-radius: 50%;
57+
margin-left: 5px;
58+
flex-shrink: 0;
59+
60+
& svg {
61+
transform: rotate(316deg);
62+
}
63+
64+
&:hover {
65+
opacity: 1;
66+
background: rgba(255, 255, 255, 0.3);
67+
}
68+
69+
&:focus {
70+
opacity: 1;
71+
background: rgba(255, 255, 255, 0.4);
72+
outline: 2px solid rgba(255, 255, 255, 0.8);
73+
outline-offset: 1px;
74+
}
75+
}
76+
2177
.handle {
2278
position: absolute;
2379
top: 0;

0 commit comments

Comments
 (0)