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;