diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js
index 88f0b98a2..c2b63df30 100644
--- a/src/dashboard/Data/Views/Views.react.js
+++ b/src/dashboard/Data/Views/Views.react.js
@@ -277,6 +277,9 @@ class Views extends TableView {
}
if (!columns[key]) {
columns[key] = { type, width: Math.min(computeWidth(key), 200) };
+ } else if (type === 'Pointer' && columns[key].type !== 'Pointer') {
+ // If we find a pointer value, upgrade the column type to Pointer
+ columns[key].type = 'Pointer';
}
const width = computeWidth(val);
if (width > columns[key].width && columns[key].width < 200) {
@@ -550,12 +553,41 @@ class Views extends TableView {
}
renderHeaders() {
- return this.state.order.map(({ name, width }, i) => (
-
- {name}
- this.handleResize(i, delta)} />
-
- ));
+ return this.state.order.map(({ name, width }, i) => {
+ const columnType = this.state.columns[name]?.type;
+ const isPointerColumn = columnType === 'Pointer';
+
+ return (
+
+
+ {name}
+ {isPointerColumn && (
+
+ )}
+
+ this.handleResize(i, delta)} />
+
+ );
+ });
}
renderEmpty() {
@@ -823,7 +855,8 @@ class Views extends TableView {
`browser/${className}?filters=${encodeURIComponent(filters)}`,
true
),
- '_blank'
+ '_blank',
+ 'noopener,noreferrer'
);
}
@@ -831,6 +864,67 @@ class Views extends TableView {
this.setState({ viewValue: value });
}
+ handleOpenAllPointers(columnName) {
+ const data = this.tableData();
+ const pointers = data
+ .map(row => row[columnName])
+ .filter(value => value && value.__type === 'Pointer' && value.className && value.objectId);
+
+ // Open each unique pointer in a new tab
+ const uniquePointers = new Map();
+ pointers.forEach(pointer => {
+ // Use a more collision-proof key format with explicit separators
+ const key = `className:${pointer.className}|objectId:${pointer.objectId}`;
+ if (!uniquePointers.has(key)) {
+ uniquePointers.set(key, pointer);
+ }
+ });
+
+ if (uniquePointers.size === 0) {
+ this.showNote('No pointers found in this column', true);
+ return;
+ }
+
+ const pointersArray = Array.from(uniquePointers.values());
+
+ // Confirm for large numbers of tabs to prevent overwhelming the user
+ if (pointersArray.length > 10) {
+ const confirmMessage = `This will open ${pointersArray.length} new tabs. This might overwhelm your browser. Continue?`;
+ if (!confirm(confirmMessage)) {
+ return;
+ }
+ }
+
+ // Open all tabs immediately to maintain user activation context
+ let errorCount = 0;
+
+ pointersArray.forEach((pointer) => {
+ try {
+ const filters = JSON.stringify([{ field: 'objectId', constraint: 'eq', compareTo: pointer.objectId }]);
+ const url = generatePath(
+ this.context,
+ `browser/${pointer.className}?filters=${encodeURIComponent(filters)}`,
+ true
+ );
+ window.open(url, '_blank', 'noopener,noreferrer');
+ // Note: window.open with security attributes may return null even when successful,
+ // so we assume success unless an exception is thrown
+ } catch (error) {
+ console.error('Failed to open tab for pointer:', pointer, error);
+ errorCount++;
+ }
+ });
+
+ // Show result notification
+ if (errorCount === 0) {
+ this.showNote(`Opened ${pointersArray.length} pointer${pointersArray.length > 1 ? 's' : ''} in new tab${pointersArray.length > 1 ? 's' : ''}`, false);
+ } else if (errorCount < pointersArray.length) {
+ this.showNote(`Opened ${pointersArray.length - errorCount} of ${pointersArray.length} tabs. ${errorCount} failed to open.`, true);
+ } else {
+ this.showNote('Unable to open tabs. Please allow popups for this site and try again.', true);
+ }
+ }
+
showNote(message, isError) {
if (!message) {
return;
diff --git a/src/dashboard/Data/Views/Views.scss b/src/dashboard/Data/Views/Views.scss
index 03c679443..83ec89cde 100644
--- a/src/dashboard/Data/Views/Views.scss
+++ b/src/dashboard/Data/Views/Views.scss
@@ -18,6 +18,62 @@
text-overflow: ellipsis;
}
+.headerText {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ min-width: 0; // Enable text truncation in flex containers
+}
+
+.headerLabel {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0; // Enable text truncation
+}
+
+.pointerIcon {
+ // Reset button styles
+ border: none;
+ padding: 0;
+ font: inherit;
+ color: inherit;
+ background: rgba(255, 255, 255, 0.2);
+
+ // Custom styles
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+ z-index: 10;
+ pointer-events: auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 20px;
+ width: 20px;
+ border-radius: 50%;
+ margin-left: 5px;
+ flex-shrink: 0;
+
+ & svg {
+ transform: rotate(316deg);
+ }
+
+ &:hover {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.3);
+ }
+
+ &:focus {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.4);
+ outline: 2px solid rgba(255, 255, 255, 0.8);
+ outline-offset: 1px;
+ }
+}
+
.handle {
position: absolute;
top: 0;