Skip to content

Conversation

mtrezza
Copy link
Member

@mtrezza mtrezza commented Sep 9, 2025

New Pull Request Checklist

Issue Description

If a user wants to open each pointer in a view table column in a new browser tab, the user needs to click on each pointer individually.

Approach

Add icon to batch-open pointers in a view table column in new browser tabs.

Summary by CodeRabbit

  • New Features

    • Pointer-type columns are now auto-detected and promoted when pointer values appear.
    • Headers display a pointer icon for pointer-type columns; clicking it opens each unique referenced record in new tabs and shows success/partial/failure/no-results feedback.
  • Style

    • Updated header layout, truncation behavior, and an interactive pointer icon with hover/focus affordances.

Copy link

parse-github-assistant bot commented Sep 9, 2025

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Copy link

coderabbitai bot commented Sep 9, 2025

📝 Walkthrough

Walkthrough

Adds pointer-aware column typing, renders a pointer action icon in column headers, and implements handleOpenAllPointers(columnName) to open unique pointer targets in new tabs with deduplication and user feedback. Adds SCSS for header layout and pointer icon styling. No changes to query/cloud-function flows.

Changes

Cohort / File(s) Summary of Changes
Views logic and header UI
src/dashboard/Data/Views/Views.react.js
Upgrade column type to Pointer when pointer values are detected; render a pointer icon in pointer-typed headers that calls handleOpenAllPointers(name) on click; add handleOpenAllPointers(columnName) to collect unique pointers, generate per-pointer URLs, open them in new tabs (try/catch per open, noopener,noreferrer), and show success/partial/none/error notes; small adjustments to handlePointerCmdClick security flags.
Styles for header and pointer icon
src/dashboard/Data/Views/Views.scss
Add .headerText, .headerLabel, and .pointerIcon selectors for header layout, truncation, and an interactive circular pointer button with hover/focus states; no existing rules removed.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant V as Views Component
  participant D as Data Rows
  participant P as Path Generator
  participant B as Browser
  participant N as Notification

  rect rgba(230,240,255,0.5)
    Note over V: Render headers (Pointer columns show icon)
    U->>V: Click pointer icon (columnName)
    V->>D: Collect values for columnName
    D-->>V: Values (may include Pointers)
    V->>V: Deduplicate by className-objectId
  end

  alt Any pointers found
    loop For each unique pointer
      V->>P: generatePath(className, filter on objectId)
      P-->>V: URL
      V->>B: window.open(URL, "_blank", "noopener,noreferrer")
      B-->>V: Opened window or blocked
    end
    alt All opened
      V->>N: showNote(success)
    else Some blocked
      V->>N: showNote(partial)
    end
  else None found
    V->>N: showNote(none)
  end

  Note over V,B: Each open wrapped in try/catch with console.error on failure
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description includes the required checklist and gives a brief issue description and approach, but it omits the mandatory “Closes:” line referencing the issue identifier and entirely lacks the “TODOs before merging” section with entries for adding tests and updating documentation as specified by the template. Please add the “Closes:” statement with the related issue number under Issue Description and include the “TODOs before merging” section listing tasks such as adding tests and updating documentation.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarizes the primary feature added—batch-opening pointers in a view table column into new browser tabs—and directly reflects the implemented changes (the pointer icon and associated handler). It is concise, specific to the main change, and omits unnecessary details. By focusing on the key functionality, it enables teammates to understand the purpose of the PR at a glance.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Sep 9, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

Copy link

uffizzi-cloud bot commented Sep 9, 2025

Uffizzi Ephemeral Environment deployment-65248

⌚ Updated Sep 09, 2025, 14:41 UTC

☁️ https://app.uffizzi.com/github.com/parse-community/parse-dashboard/pull/2976

📄 View Application Logs etc.

What is Uffizzi? Learn more

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/dashboard/Data/Views/Views.react.js (1)

862-913: Batch-open guardrails: confirm on large counts, safer dedup key, and noopener

  • Ask for confirmation when opening many tabs.
  • Use a collision-proof dedup key.
  • Add noopener/noreferrer for safety (also null the opener).
   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 => {
-      const key = `${pointer.className}-${pointer.objectId}`;
+      const key = JSON.stringify([pointer.className, 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());
+    const MAX_TABS_WITHOUT_CONFIRM = 10;
+    if (pointersArray.length > MAX_TABS_WITHOUT_CONFIRM) {
+      const ok = window.confirm(`Open ${pointersArray.length} tabs?`);
+      if (!ok) return;
+    }

     // Open all tabs immediately to maintain user activation context
     let successCount = 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
         );
-        const newWindow = window.open(url, '_blank');
+        const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
 
         if (newWindow) {
+          try { newWindow.opener = null; } catch(e) {} // best-effort
           successCount++;
         }
       } catch (error) {
         console.error('Failed to open tab for pointer:', pointer, error);
       }
     });

If you prefer, I can also add an alternative action to open a single Browser tab with a multi-ID filter instead of many tabs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52a858b and 9b2aaac.

📒 Files selected for processing (2)
  • src/dashboard/Data/Views/Views.react.js (3 hunks)
  • src/dashboard/Data/Views/Views.scss (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/dashboard/Data/Views/Views.react.js (2)
src/components/Icon/Icon.react.js (1)
  • Icon (12-29)
src/lib/generatePath.js (2)
  • filters (8-8)
  • generatePath (3-34)
🪛 GitHub Check: Lint
src/dashboard/Data/Views/Views.react.js

[failure] 559-559:
Trailing spaces not allowed

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Docker linux/amd64
🔇 Additional comments (1)
src/dashboard/Data/Views/Views.react.js (1)

280-283: LGTM: upgrading to Pointer when detected

Good call to promote the column type when a pointer value appears.

Comment on lines 21 to 26
.headerText {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure header text truncates correctly with the new flex wrapper

Wrapping the header in a flex container can break the existing ellipsis. Add a dedicated, shrinkable label element and min-width: 0 to the flex container.

 .headerText {
   display: flex;
   align-items: center;
   justify-content: space-between;
   width: 100%;
+  min-width: 0; /* allow children to shrink */
 }
+
+.headerLabel {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 0;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.headerText {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.headerText {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-width: 0; /* allow children to shrink */
}
.headerLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
🤖 Prompt for AI Agents
In src/dashboard/Data/Views/Views.scss around lines 21–26, the new .headerText
flex wrapper prevents the header's text from truncating with an ellipsis; add
min-width: 0 to the .headerText container and introduce a dedicated, shrinkable
label element inside the header (make the label take remaining space and allow
shrinking, then set overflow: hidden, text-overflow: ellipsis and white-space:
nowrap on that label) and update the header markup to put the title text in that
label so the ellipsis works correctly.

Comment on lines 28 to 52
.pointerIcon {
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;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
margin-left: 5px;
flex-shrink: 0;

& svg {
transform: rotate(316deg);
}

&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.3);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make the header icon keyboard-accessible and clearly focusable

The interactive icon is a span with click only; use a button and add focus styles for accessibility.

 .pointerIcon {
   cursor: pointer;
   opacity: 0.7;
   transition: opacity 0.2s ease;
   z-index: 10;
   pointer-events: auto;
-  display: inline-flex;
+  display: inline-flex;
   align-items: center;
   justify-content: center;
   height: 20px;
   width: 20px;
   background: rgba(255, 255, 255, 0.2);
   border-radius: 50%;
   margin-left: 5px;
   flex-shrink: 0;
+  border: 0;            /* button reset */
+  outline: none;        /* rely on :focus-visible below */
+  appearance: none;
 
   & svg {
     transform: rotate(316deg);
   }
 
   &:hover {
     opacity: 1;
     background: rgba(255, 255, 255, 0.3);
   }
+
+  &:focus-visible {
+    outline: 2px solid #fff;
+    outline-offset: 2px;
+    opacity: 1;
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.pointerIcon {
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;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
margin-left: 5px;
flex-shrink: 0;
& svg {
transform: rotate(316deg);
}
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.3);
}
}
.pointerIcon {
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;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
margin-left: 5px;
flex-shrink: 0;
border: 0; /* button reset */
outline: none; /* rely on :focus-visible below */
appearance: none;
& svg {
transform: rotate(316deg);
}
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.3);
}
&:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
opacity: 1;
}
}
🤖 Prompt for AI Agents
In src/dashboard/Data/Views/Views.scss around lines 28 to 52, the interactive
header icon is styled as a non-semantic span and lacks keyboard accessibility
and visible focus; change the markup to a button (or add role/button and
keyboard handlers if markup change isn't possible) and update the CSS selectors
to target button (or [role="button"]) instead of .pointerIcon, include
focus-visible/focus styles (outline or box-shadow and increased
background/opacity) and ensure pointer-events and display remain correct so the
element is operable via keyboard and shows a clear focus indicator.

@mtrezza mtrezza changed the title feat: Add batch-open pointers in a view table column in new browser tabs feat: Add button to view table to open all pointers of a column in new browser tabs Sep 9, 2025
@mtrezza mtrezza merged commit b8033a4 into parse-community:alpha Sep 9, 2025
10 of 11 checks passed
@mtrezza mtrezza deleted the feat/add-batch-open-pointers-in-view-table branch September 9, 2025 16:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/dashboard/Data/Views/Views.react.js (3)

280-283: Upgrade-to-Pointer: set a sane min column width to avoid Pill truncation.
When a column upgrades to Pointer, enforce a minimum width so the Pill isn’t overly clipped.

-            } else if (type === 'Pointer' && columns[key].type !== 'Pointer') {
-              // If we find a pointer value, upgrade the column type to Pointer
-              columns[key].type = 'Pointer';
+            } else if (type === 'Pointer' && columns[key].type !== 'Pointer') {
+              // If we find a pointer value, upgrade the column type to Pointer
+              columns[key].type = 'Pointer';
+              columns[key].width = Math.max(columns[key].width, 120);

556-590: Good a11y on the header button; disable during loading to prevent repeated mass-opens.
Prevents accidental double-clicks while data is still loading.

-              <button
+              <button
                 type="button"
                 className={styles.pointerIcon}
+                disabled={this.state.loading || !Array.isArray(this.state.data) || this.state.data.length === 0}
                 onClick={(e) => {
                   e.stopPropagation();
                   e.preventDefault();
                   this.handleOpenAllPointers(name);
                   // Remove focus after action to follow UX best practices
                   e.currentTarget.blur();
                 }}
                 aria-label={`Open all pointers in ${name} column in new tabs`}
-                title="Open all pointers in new tabs"
+                title={this.state.loading ? 'Loading…' : 'Open all pointers in new tabs'}
               >

867-926: Optional: handle arrays-of-pointers in a cell.
If a view column contains an array of Pointers, include them too.

-    const pointers = data
-      .map(row => row[columnName])
-      .filter(value => value && value.__type === 'Pointer' && value.className && value.objectId);
+    const cellValues = data.map(row => row[columnName]);
+    const pointers = cellValues
+      .reduce((acc, v) => acc.concat(Array.isArray(v) ? v : [v]), [])
+      .filter(value => value && value.__type === 'Pointer' && value.className && value.objectId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2aaac and ebad502.

📒 Files selected for processing (2)
  • src/dashboard/Data/Views/Views.react.js (3 hunks)
  • src/dashboard/Data/Views/Views.scss (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/dashboard/Data/Views/Views.scss
🧰 Additional context used
🧬 Code graph analysis (1)
src/dashboard/Data/Views/Views.react.js (2)
src/components/Icon/Icon.react.js (1)
  • Icon (12-29)
src/lib/generatePath.js (2)
  • filters (8-8)
  • generatePath (3-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Docker linux/amd64
🔇 Additional comments (1)
src/dashboard/Data/Views/Views.react.js (1)

858-860: Nice security hardening with noopener/noreferrer.
Prevents opener hijacking and referrer leakage for Cmd-click behavior.

Comment on lines +867 to +926
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);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Treat popup-blocker null returns as failures; guard on empty/non-array data.
Today you only count exceptions; most blockers return null without throwing, leading to false “Opened N…” notes.

   handleOpenAllPointers(columnName) {
-    const data = this.tableData();
+    const data = this.tableData();
+    if (!Array.isArray(data) || data.length === 0) {
+      this.showNote('No data loaded', true);
+      return;
+    }
@@
-    pointersArray.forEach((pointer) => {
+    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
+        const win = window.open(url, '_blank', 'noopener,noreferrer');
+        if (!win) {
+          // Likely blocked by a popup blocker
+          errorCount++;
+        }
       } catch (error) {
         console.error('Failed to open tab for pointer:', pointer, error);
         errorCount++;
       }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
}
handleOpenAllPointers(columnName) {
const data = this.tableData();
if (!Array.isArray(data) || data.length === 0) {
this.showNote('No data loaded', true);
return;
}
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,
const win = window.open(url, '_blank', 'noopener,noreferrer');
if (!win) {
// Likely blocked by a popup blocker
errorCount++;
}
} 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);
}
}

parseplatformorg pushed a commit that referenced this pull request Sep 9, 2025
# [7.5.0-alpha.1](7.4.0...7.5.0-alpha.1) (2025-09-09)

### Features

* Add button to view table to open all pointers of a column in new browser tabs ([#2976](#2976)) ([b8033a4](b8033a4))
@parseplatformorg
Copy link
Contributor

🎉 This change has been released in version 7.5.0-alpha.1

@parseplatformorg parseplatformorg added the state:released-alpha Released as alpha version label Sep 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
state:released-alpha Released as alpha version
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants