Skip to content

feat(content-preview): support externally-managed preview instance#4693

Open
zhirongwang wants to merge 1 commit into
box:masterfrom
zhirongwang:feat/content-preview-external-preview-instance
Open

feat(content-preview): support externally-managed preview instance#4693
zhirongwang wants to merge 1 commit into
box:masterfrom
zhirongwang:feat/content-preview-external-preview-instance

Conversation

@zhirongwang

@zhirongwang zhirongwang commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an optional previewInstance prop to ContentPreview that lets host applications pass in an existing Box.Preview instance instead of having the component construct its own.
  • On cleanup, when an external instance was provided, ContentPreview detaches only the listeners it attached (load, preload, preview_error, preview_metric) rather than calling destroy() and removeAllListeners(), so the host application retains ownership of the instance lifecycle.
  • When the prop is not provided, behavior is unchanged: ContentPreview creates and destroys its own instance as before.

Test Plan

  • yarn flow check passes (0 errors)
  • All content-preview unit tests pass (180 tests)
  • Verify default behavior is unchanged when previewInstance is not provided
  • Verify that a provided external Preview instance is reused and not destroyed on unmount

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Content previews can now use an externally managed preview instance.
    • Existing preview behavior remains unchanged when no instance is provided.
  • Bug Fixes

    • Prevented externally managed preview instances from being destroyed during component cleanup.

@zhirongwang zhirongwang requested review from a team as code owners July 13, 2026 16:41
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

ContentPreview can reuse an externally managed Box.Preview instance and now distinguishes listener cleanup from instance destruction based on ownership.

Changes

ContentPreview instance reuse

Layer / File(s) Summary
Preview instance contract
src/elements/content-preview/ContentPreview.js
Adds the optional previewInstance prop and reuses it during preview loading when provided.
Preview cleanup ownership
src/elements/content-preview/ContentPreview.js
Removes component-attached listeners from external instances, while internally created instances continue to be destroyed with their listeners removed.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: ready-to-merge

Suggested reviewers: mborkowski-box, jackiejou, ahorowitz123, olehrybak, dlasecki-box

Poem

I reuse the preview, said the rabbit with glee,
“Your instance stays safe and belongs still to thee.”
Listeners hop off when cleanup is near,
New previews are destroyed—ownership is clear.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: supporting an externally managed preview instance.
Description check ✅ Passed The description includes a clear summary and test plan, which is sufficient for the repository's template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.3)
src/elements/content-preview/ContentPreview.js

File contains syntax errors that prevent linting: Line 21: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 36: 'import { type x ident }' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 65: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 66: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 67: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 68: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 69: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 70: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 71: '

... [truncated 18644 characters] ...

nly feature. Convert your file to a TypeScript file or remove the syntax.; Line 1607: type annotation are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 1619: type annotation are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 1626: Type annotations are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 1658: expected ) but instead found :; Line 1658: Expected a JSX attribute but instead found ')'.; Line 1658: Unexpected token. Did you mean {'}'} or }?; Line 1658: Unexpected token. Did you mean {'>'} or >?; Line 1769: 'export type' declarations are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.


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.

@zhirongwang zhirongwang self-assigned this Jul 13, 2026
@zhirongwang zhirongwang force-pushed the feat/content-preview-external-preview-instance branch from 913cd74 to 3d58d73 Compare July 13, 2026 16:46
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


zhirongwang seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/elements/content-preview/ContentPreview.js`:
- Line 1025: Update ContentPreview’s componentDidUpdate lifecycle to detect
changes to props.previewInstance, destroy the previous preview instance and its
listeners, then assign the new instance or create the default Preview and rebind
the required listeners. Ensure clearing or swapping the prop immediately leaves
the component wired to the current preview instance, while preserving the
existing behavior when the instance is unchanged.
- Around line 436-444: Update the previewInstance cleanup branch in
ContentPreview to remove every listener registered by ContentPreview, including
thumbnailsOpen, thumbnailsClose, annotator_create, and advanced_insights_report,
in addition to the existing handlers. Keep the shared external preview instance
intact and ensure no ContentPreview listeners remain attached after cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e463323-196a-4a2e-bbf0-667d09c2b119

📥 Commits

Reviewing files that changed from the base of the PR and between 8a2b608 and 3d58d73.

📒 Files selected for processing (1)
  • src/elements/content-preview/ContentPreview.js

Comment on lines +436 to +444
if (this.props.previewInstance) {
this.preview.removeListener('load', this.onPreviewLoad);
this.preview.removeListener('preload', this.endLoadingSession);
this.preview.removeListener('preview_error', this.onPreviewError);
this.preview.removeListener('preview_metric', this.onPreviewMetric);
} else {
this.preview.destroy();
this.preview.removeAllListeners();
}

@coderabbitai coderabbitai Bot Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all listener attachments on this.preview in ContentPreview.js
rg -n 'this\.preview\.(addListener|on|addListener)\(' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant sections around listener registration and teardown.
sed -n '1000,1060p' src/elements/content-preview/ContentPreview.js
printf '\n---\n'
sed -n '420,460p' src/elements/content-preview/ContentPreview.js
printf '\n---\n'
rg -n 'componentDidUpdate|previewInstance|destroyPreview|removeAllListeners|removeListener' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 4732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect update handling around previewInstance and destroyPreview calls.
sed -n '520,560p' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 2070


Detach all listeners when previewInstance is supplied
src/elements/content-preview/ContentPreview.js:436-444 only removes 4 handlers for external instances, but ContentPreview also registers thumbnailsOpen, thumbnailsClose, annotator_create, and advanced_insights_report. Those stay attached to the shared preview instance after unmount/reload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-preview/ContentPreview.js` around lines 436 - 444,
Update the previewInstance cleanup branch in ContentPreview to remove every
listener registered by ContentPreview, including thumbnailsOpen,
thumbnailsClose, annotator_create, and advanced_insights_report, in addition to
the existing handlers. Keep the shared external preview instance intact and
ensure no ContentPreview listeners remain attached after cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1, why do we not use removeAllListeners?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good question, @JChan106 — since previewInstance is externally owned by the host application when it's supplied, removeAllListeners() would strip out any listeners the host app itself registered on that shared instance (and any listeners other consumers might have attached), not just the ones ContentPreview added. That would be a destructive side effect on an object the component doesn't own.

So the intent is correct: only remove the specific listeners ContentPreview itself attached, leaving everything else on the shared instance intact for the host to manage.

That said, the current cleanup only detaches 4 of the listeners registered in loadPreview — it's missing thumbnailsOpen, thumbnailsClose, annotator_create (when showAnnotationsControls is set), and advanced_insights_report (when advancedContentInsights is set). To avoid leaking those onto the shared instance, it'd be worth removing all of them symmetrically in destroyPreview, e.g.:

if (this.props.previewInstance) {
    this.preview.removeListener('load', this.onPreviewLoad);
    this.preview.removeListener('preload', this.endLoadingSession);
    this.preview.removeListener('preview_error', this.onPreviewError);
    this.preview.removeListener('preview_metric', this.onPreviewMetric);
    this.preview.removeListener('thumbnailsOpen', this.onThumbnailsOpen);
    this.preview.removeListener('thumbnailsClose', this.onThumbnailsClose);
    this.preview.removeListener('annotator_create', onAnnotator);
    this.preview.removeListener('advanced_insights_report', onContentInsightsEventReport);
} else {
    ...
}

Note that the thumbnailsOpen/thumbnailsClose listeners and annotator_create/advanced_insights_report are currently registered as inline anonymous functions or closures in loadPreview, so they'd need to be hoisted to stable instance methods/references to be removable later.

};
const { Preview } = global.Box;
this.preview = new Preview();
this.preview = this.props.previewInstance || new Preview();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check componentDidUpdate and related lifecycle for previewInstance handling
rg -n 'componentDidUpdate|previewInstance' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 385


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline src/elements/content-preview/ContentPreview.js --view expanded
sed -n '480,580p' src/elements/content-preview/ContentPreview.js
sed -n '990,1060p' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 7054


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '580,760p' src/elements/content-preview/ContentPreview.js
rg -n 'previewInstance|destroyPreview|componentWillUnmount|removeListener|destroy\(' src/elements/content-preview/ContentPreview.js

Repository: box/box-ui-elements

Length of output: 6664


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '420,450p' src/elements/content-preview/ContentPreview.js | cat -n

Repository: box/box-ui-elements

Length of output: 1393


Handle previewInstance swaps in componentDidUpdate. destroyPreview() keys cleanup off the current prop, but there’s no branch that tears down the old instance and rebinds listeners when previewInstance changes. If the host swaps or clears the instance, the old listeners can stick around and the new instance won’t be wired up until the next load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-preview/ContentPreview.js` at line 1025, Update
ContentPreview’s componentDidUpdate lifecycle to detect changes to
props.previewInstance, destroy the previous preview instance and its listeners,
then assign the new instance or create the default Preview and rebind the
required listeners. Ensure clearing or swapping the prop immediately leaves the
component wired to the current preview instance, while preserving the existing
behavior when the instance is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants