Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const config: StorybookConfig = {
...webpack.resolve.alias,
'box-ui-elements-locale-data': path.resolve(`i18n/bundles/${language}`),
'box-locale-data': path.resolve(`node_modules/@box/cldr-data/locale-data/${language}`),
// box-content-preview (devDependency) imports its box-ui-elements peer via
// es/ paths; inside this repo those resolve to the src/ they are built from.
'box-ui-elements/es': path.resolve('src'),
Comment thread
jackiejou marked this conversation as resolved.
'msw/native': path.resolve('node_modules/msw/lib/native/index.mjs'),
};

Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
"babel-plugin-react-remove-properties": "^0.3.0",
"babel-plugin-rewire": "^1.0.0",
"babel-plugin-styled-components": "^1.10.7",
"box-content-preview": "3.79.0",
"chromatic": "^18.0.1",
"circular-dependency-plugin": "^5.2.2",
"classnames": "^2.5.1",
Expand Down Expand Up @@ -323,6 +324,7 @@
"@hapi/address": "^2.1.4",
"@tanstack/react-virtual": "^3.13.12",
"axios": "^0.33.0",
"box-content-preview": "^3.79.0",
"classnames": "^2.5.1",
"color": "^3.2.1",
"draft-js": "^0.11.7",
Expand Down Expand Up @@ -365,6 +367,11 @@
"tabbable": "^1.1.3",
"uuid": "^8.3.2"
},
"peerDependenciesMeta": {
"box-content-preview": {
"optional": true
}
},
"resolutions": {
"@sigstore/core": "3.2.1",
"draft-js/immutable": "^3.8.3",
Expand Down
22 changes: 21 additions & 1 deletion scripts/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const license = require('./license');
const packageJson = require('../package.json');

// Alias a missing optional box-content-preview peer to an empty stub so webpack
// can resolve loadBoxContentPreview's literal dynamic import at build time.
const getBoxContentPreviewAliases = () => {
try {
require.resolve('box-content-preview');
return {};
} catch (error) {
return {
'box-content-preview': path.resolve('src/elements/content-preview/boxContentPreviewStub.ts'),
'box-content-preview/styles.css': path.resolve('src/elements/content-preview/boxContentPreviewStub.css'),
};
}
};

const isDevBuild = process.env.NODE_ENV === 'development';
const isProdBuild = process.env.NODE_ENV === 'production';
const version = isProdBuild ? packageJson.version : 'dev';
Expand Down Expand Up @@ -50,6 +64,10 @@ const getConfig = isReactBundle => {
alias: {
'box-ui-elements-locale-data': path.resolve(`i18n/bundles/${language}`),
'box-locale-data': path.resolve(`node_modules/@box/cldr-data/locale-data/${language}`),
// box-content-preview (devDependency) imports its box-ui-elements peer via
// es/ paths; inside this repo those resolve to the src/ they are built from.
'box-ui-elements/es': path.resolve('src'),
Comment thread
jackiejou marked this conversation as resolved.
...getBoxContentPreviewAliases(),
},
},

Expand All @@ -72,8 +90,10 @@ const getConfig = isReactBundle => {
{
test: /\.(js|mjs|ts|tsx)$/,
// Exclude node_modules in development mode to improve build performance
// box-content-preview ships a pre-bundled dist with class private methods
// that this babel config cannot parse; it needs no transpilation.
exclude: hasAllBrowserSupport
? /@babel(?:\/|\\{1,2})runtime|pikaday|core-js/
? /@babel(?:\/|\\{1,2})runtime|box-content-preview|pikaday|core-js/
: /node_modules\/(?!@box\/cldr-data)/, // Exclude node_modules except for @box/cldr-data which is needed for i18n
loader: 'babel-loader',
},
Expand Down
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export const DELIMITER_SLASH: 'slash' = 'slash';
export const DELIMITER_CARET: 'caret' = 'caret';

/* ---------------------- Defaults -------------------------- */
export const DEFAULT_PREVIEW_VERSION = '3.0.0';
export const DEFAULT_PREVIEW_VERSION = '3.79.0';
export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_PATH_STATIC = 'platform/elements';
export const DEFAULT_PATH_STATIC_PREVIEW = 'platform/preview';
Expand Down
95 changes: 90 additions & 5 deletions src/elements/content-preview/ContentPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ type Props = {
isLarge: boolean,
isVeryLarge?: boolean,
language: string,
// Required when useNpmBoxContentPreview is on. Import loadBoxContentPreview from its own file and pass it here.
loadPreviewModule?: () => Promise<{ Preview?: any }>,
loadingIndicatorDelayMs?: number,
logoUrl?: string,
measureRef: Function,
Expand All @@ -157,6 +159,9 @@ type Props = {
},
previewLibraryVersion: string,
previewMode?: 'default' | 'shared_file' | 'shared_folder' | 'editable_shared_file' | 'inline_feed',
// npm path only: URL of the pdfjs worker file, resolved by the consumer's bundler.
// Passed to box-content-preview in the show() options as `pdfjs.workerSrc`.
pdfjsWorkerSrc?: string,
resin?: {
recordAction?: (data: Object) => void,
},
Expand Down Expand Up @@ -281,6 +286,10 @@ class ContentPreview extends React.PureComponent<Props, State> {

preview: any;

npmPreviewModule: ?{ Preview: any };

npmPreviewLoadFailed: boolean = false;

api: API;

// Defines a generic type for ContentSidebar, since an import would interfere with code splitting
Expand Down Expand Up @@ -472,10 +481,14 @@ class ContentPreview extends React.PureComponent<Props, State> {
* @return {void}
*/
componentDidMount(): void {
// Always load Box.Preview library assets
// Always load preview library assets (npm module or CDN script)
Comment thread
jackiejou marked this conversation as resolved.
// Even when children are provided, we need assets ready for transitions
this.loadStylesheet();
this.loadScript();
if (this.shouldUseNpmPreview()) {
this.loadNpmPreview();
} else {
this.loadStylesheet();
this.loadScript();
Comment on lines +486 to +490

@reneshen0328 reneshen0328 Jul 15, 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.

[question] Eventually shouldUseNpmPreview feature related code will be cleaned up I assume? In that case, will other consumer that has been leveraging the CDN script also be forced to switch to using npm installed version only?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no, the CDN will stay for legacy users.

}

const { currentFileId } = this.state;
const { loadingIndicatorDelayMs } = this.props;
Expand All @@ -492,6 +505,64 @@ class ContentPreview extends React.PureComponent<Props, State> {
this.focusPreview();
}

shouldUseNpmPreview(): boolean {
return isFeatureEnabled(this.props.features, 'useNpmBoxContentPreview');
}

loadNpmPreview = async (): Promise<void> => {
if (this.npmPreviewModule) {
return;
}

const { loadPreviewModule } = this.props;
if (!loadPreviewModule) {
this.onNpmPreviewLoadError('loadPreviewModule is required when useNpmBoxContentPreview is enabled');
return;
}

let previewModule;
try {
previewModule = await loadPreviewModule();
} catch (error) {
this.onNpmPreviewLoadError(
`Failed to load the box-content-preview module: ${error?.message || String(error)}`,
);
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!previewModule.Preview) {
this.onNpmPreviewLoadError('box-content-preview module has no Preview export');
return;
}

this.npmPreviewModule = previewModule;
this.loadPreview();
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

getNpmPreviewLocation(): { baseURI: string, locale: string, staticBaseURI: string, version: string } {
const { language, previewLibraryVersion, staticHost, staticPath } = this.props;
const trailingSlash = staticHost.endsWith('/') ? '' : '/';
const staticBaseURI = `${staticHost}${trailingSlash}${staticPath}/`;
return {
baseURI: `${staticBaseURI}${previewLibraryVersion}/${language}/`,
locale: language,
staticBaseURI,
version: previewLibraryVersion,
};
}

onNpmPreviewLoadError(message: string): void {
const { onError } = this.props;
const error = {
code: ERROR_CODE_UNKNOWN,
message,
};
this.npmPreviewLoadFailed = true;
this.endLoadingSession();
this.setState({ error });
onError(error, ERROR_CODE_UNKNOWN, { error }, ORIGIN_PREVIEW);
}

static getDerivedStateFromProps(props: Props, state: State) {
const { fileId } = props;

Expand Down Expand Up @@ -628,6 +699,9 @@ class ContentPreview extends React.PureComponent<Props, State> {
* @return {boolean} true if preview is loaded
*/
isPreviewLibraryLoaded(): boolean {
if (this.shouldUseNpmPreview()) {
return !!this.npmPreviewModule;
}
return !!global.Box && !!global.Box.Preview;
}

Expand Down Expand Up @@ -1013,8 +1087,14 @@ class ContentPreview extends React.PureComponent<Props, State> {
showProgress: false,
skipServerUpdate: true,
useHotkeys: false,
...(this.npmPreviewModule && this.props.pdfjsWorkerSrc
? { pdfjs: { workerSrc: this.props.pdfjsWorkerSrc } }
: {}),
...(this.npmPreviewModule ? { location: this.getNpmPreviewLocation() } : {}),
};
const { Preview } = global.Box;

const Preview =
this.shouldUseNpmPreview() && this.npmPreviewModule ? this.npmPreviewModule.Preview : global.Box.Preview;
this.preview = new Preview();
this.preview.addListener('load', this.onPreviewLoad);
this.preview.addListener('preload', this.endLoadingSession);
Expand All @@ -1032,7 +1112,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
this.preview.show(file.id, token, {
...previewOptions,
...omit(rest, Object.keys(previewOptions)),
annotatorToken: tokenOrTokenFunction,
annotatorToken: typeof tokenOrTokenFunction === 'function' ? tokenOrTokenFunction : undefined,
});
if (advancedContentInsights) {
this.preview.addListener('advanced_insights_report', onContentInsightsEventReport);
Expand Down Expand Up @@ -1081,6 +1161,11 @@ class ContentPreview extends React.PureComponent<Props, State> {
fetchFileSuccessCallback = (file: BoxItem): void => {
this.fetchFileEndTime = performance.now();

if (this.npmPreviewLoadFailed) {
this.setState({ file });
return;
}

const { file: currentFile }: State = this.state;
const isExistingFile = currentFile ? currentFile.id === file.id : false;
const isWatermarked = getProp(file, 'watermark_info.is_watermarked', false);
Expand Down
Loading
Loading