diff --git a/src/i18n/en-US.properties b/src/i18n/en-US.properties index 385937ffe..8950c6537 100644 --- a/src/i18n/en-US.properties +++ b/src/i18n/en-US.properties @@ -59,7 +59,9 @@ error_password_protected=We're sorry the preview didn't load. This document is p # Preview error message shown when conversion was unable to process the file at the given time. error_try_again_later=We're sorry the preview didn't load. Please try again later. # Preview error message shown when conversion failed due to file contents -error_bad_file=We're sorry the preivew didn't load. This file may be corrupted. +error_bad_file=We're sorry the preview didn't load. This file could not be converted. +# Preview error message shown when the file cannot be downloaded +error_not_downloadable=Oops! It looks like something is wrong with this file. We recommend that you upload a new version of this file or roll back to a previous version. Please contact us for more details. We apologize for the inconvenience. # Media Preview # Label for autoplay in media player @@ -105,7 +107,6 @@ media_audio=Audio # Label for alternate audio tracks in media player track=Track - # 3D Preview # Button tooltip for showing/hiding the list of animation clips box3d_animation_clips=Animation clips @@ -168,7 +169,6 @@ annotations_delete_error=We're sorry, the annotation could not be deleted. # Text for when the authorization token is invalid annotations_authorization_error=Your session has expired. Please refresh the page. - # Notifications # Default text for notification button that dismisses notification notification_button_default_text=Okay @@ -177,6 +177,9 @@ notification_annotation_point_mode=Click anywhere to add a comment to the docume # Notification message shown when user enters drawing annotation mode notification_annotation_draw_mode=Press down and drag the pointer to draw on the document +# Link Text +link_contact_us=Contact Us + # File Types # 360 degree video file type 360_videos=360-degree videos diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 771d454f5..414ebcf23 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -62,6 +62,7 @@ const KEYDOWN_EXCEPTIONS = ['INPUT', 'SELECT', 'TEXTAREA']; // Ignore keydown ev const LOG_RETRY_TIMEOUT_MS = 500; // retry interval for logging preview event const LOG_RETRY_COUNT = 3; // number of times to retry logging preview event const MS_IN_S = 1000; // ms in a sec +const SUPPORT_URL = 'https://support.box.com'; // All preview assets are relative to preview.js. Here we create a location // object that mimics the window location object and points to where @@ -923,6 +924,16 @@ class Preview extends EventEmitter { this.file = file; this.logger.setFile(file); + // If file is not downloadable, trigger an error + if (file.is_download_available === false) { + const error = createPreviewError(ERROR_CODE.notDownloadable, __('error_not_downloadable'), null, { + linkText: __('link_contact_us'), + linkUrl: SUPPORT_URL + }); + this.triggerError(error); + return; + } + // Keep reference to previously cached file version const cachedFile = getCachedFile(this.cache, { fileVersionId: responseFileVersionId }); @@ -1497,7 +1508,7 @@ class Preview extends EventEmitter { console.error(message); /* eslint-enable no-console */ - const error = createPreviewError(ERROR_CODE, message, filesToPrefetch); + const error = createPreviewError(ERROR_CODE.prefetchFile, message, filesToPrefetch); this.emitPreviewError(error); }); } diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 432b14cd2..9f8380072 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -170,7 +170,8 @@ describe('lib/Preview', () => { extension: 'pdf', representations: {}, watermark_info: {}, - authenticated_download_url: 'url' + authenticated_download_url: 'url', + is_download_available: true } preview.show(file, 'foken'); @@ -238,7 +239,8 @@ describe('lib/Preview', () => { extension: 'docx', representations: {}, watermark_info: {}, - authenticated_download_url: 'url' + authenticated_download_url: 'url', + is_download_available: true }; }); @@ -772,7 +774,8 @@ describe('lib/Preview', () => { extension: 'pdf', representations: {}, watermark_info: {}, - authenticated_download_url: 'url' + authenticated_download_url: 'url', + is_download_available: true }; stubs.promise = Promise.resolve({ @@ -1165,6 +1168,10 @@ describe('lib/Preview', () => { setFile: sandbox.stub(), setCacheStale: sandbox.stub() }; + preview.open = true; + preview.file = { + id: 0 + }; stubs.getCachedFile = sandbox.stub(file, 'getCachedFile'); stubs.set = sandbox.stub(preview.cache, 'set'); @@ -1216,7 +1223,6 @@ describe('lib/Preview', () => { }); it('should do nothing if response comes back for an incorrect file', () => { - preview.open = true; preview.file = { id: '123', file_version: { @@ -1230,22 +1236,19 @@ describe('lib/Preview', () => { }); it('should save a reference to the file and update the logger', () => { - preview.open = true; - preview.file = { - id: 0 - }; - preview.handleFileInfoResponse(stubs.file); expect(preview.file).to.equal(stubs.file); expect(preview.logger.setFile).to.be.called; }); - it('should get the latest cache, then update it with the new file', () => { - preview.open = true; - preview.file = { - id: 0 - }; + it('should trigger an error if file is not downloadable', () => { + stubs.file.is_download_available = false; + preview.handleFileInfoResponse(stubs.file); + expect(stubs.triggerError).to.be.called; + expect(stubs.loadViewer).to.not.be.called; + }); + it('should get the latest cache, then update it with the new file', () => { stubs.getCachedFile.returns({ file_version: { sha1: 0 @@ -1262,11 +1265,6 @@ describe('lib/Preview', () => { it('should uncache the file if the file is watermarked', () => { stubs.isWatermarked.returns(true); - preview.open = true; - preview.file = { - id: 0 - }; - stubs.getCachedFile.returns({ file_version: { sha1: 0 @@ -1280,11 +1278,6 @@ describe('lib/Preview', () => { }); it('should load the viewer if the file is not in the cache', () => { - preview.open = true; - preview.file = { - id: 0 - }; - stubs.getCachedFile.returns(null); preview.handleFileInfoResponse(stubs.file); @@ -1292,11 +1285,6 @@ describe('lib/Preview', () => { }); it('should load the viewer if the cached file is not valid', () => { - preview.open = true; - preview.file = { - id: 0 - }; - stubs.checkFileValid.returns(false); preview.handleFileInfoResponse(stubs.file); @@ -1304,11 +1292,6 @@ describe('lib/Preview', () => { }); it('should set the cache stale and re-load the viewer if the cached sha1 does not match the files sha1', () => { - preview.open = true; - preview.file = { - id: 0 - }; - stubs.getCachedFile.returns({ file_version: { sha1: 0 @@ -1323,11 +1306,6 @@ describe('lib/Preview', () => { }); it('should set the cache stale and re-load the viewer if the file is watermarked', () => { - preview.open = true; - preview.file = { - id: 0 - }; - stubs.isWatermarked.returns(true); stubs.getCachedFile.returns({ file_version: { @@ -1343,11 +1321,6 @@ describe('lib/Preview', () => { }); it('should trigger an error if any cache or load operations fail', () => { - preview.open = true; - preview.file = { - id: 0 - }; - stubs.getCachedFile.throws(new Error()); preview.handleFileInfoResponse(stubs.file); diff --git a/src/lib/__tests__/file-test.js b/src/lib/__tests__/file-test.js index 2ec981672..ab07cfd45 100644 --- a/src/lib/__tests__/file-test.js +++ b/src/lib/__tests__/file-test.js @@ -25,14 +25,14 @@ describe('lib/file', () => { it('should return the correct api url', () => { assert.equal( getURL('id', '', 'api'), - 'api/2.0/files/id?fields=id,permissions,shared_link,sha1,file_version,name,size,extension,representations,watermark_info,authenticated_download_url' + 'api/2.0/files/id?fields=id,permissions,shared_link,sha1,file_version,name,size,extension,representations,watermark_info,authenticated_download_url,is_download_available' ); }); it('should return the correct API url for file version', () => { assert.equal( getURL('id', 'versionId', 'api'), - 'api/2.0/files/id/versions/versionId?fields=id,permissions,shared_link,sha1,file_version,name,size,extension,representations,watermark_info,authenticated_download_url' + 'api/2.0/files/id/versions/versionId?fields=id,permissions,shared_link,sha1,file_version,name,size,extension,representations,watermark_info,authenticated_download_url,is_download_available' ); }); }); @@ -121,7 +121,8 @@ describe('lib/file', () => { extension: 'blah', representations: {}, watermark_info: {}, - authenticated_download_url: 'blah' + authenticated_download_url: 'blah', + is_download_available: true }; assert.ok(checkFileValid(file)); }); @@ -139,7 +140,8 @@ describe('lib/file', () => { extension: 'exe', representations: {}, watermark_info: {}, - authenticated_download_url: 'blah?version=file_version_123' + authenticated_download_url: 'blah?version=file_version_123', + is_download_available: true }; const file = normalizeFileVersion(fileVersion, fileId); diff --git a/src/lib/__tests__/logUtils-test.js b/src/lib/__tests__/logUtils-test.js index 9c9421799..cf1b1994d 100644 --- a/src/lib/__tests__/logUtils-test.js +++ b/src/lib/__tests__/logUtils-test.js @@ -80,5 +80,15 @@ describe('lib/logUtils', () => { expect(err.displayMessage).to.equal(errRefresh); }); + + it('should append optional properties to error if provided', () => { + const err = createPreviewError('', '', null, { + foo: 'bar', + some: 'value' + }); + + expect(err.foo).to.equal('bar'); + expect(err.some).to.equal('value'); + }); }); }); diff --git a/src/lib/events.js b/src/lib/events.js index 1c08eaf5b..fb0dde7cf 100644 --- a/src/lib/events.js +++ b/src/lib/events.js @@ -19,7 +19,8 @@ export const ERROR_CODE = { prefetchFile: 'error_prefetch_file', rateLimit: 'error_rate_limit', retriesExceeded: 'error_retries_exceeded', - browserError: 'error_browser_thrown' + browserError: 'error_browser_thrown', + notDownloadable: 'error_file_not_downloadable' }; export const PREVIEW_LOAD_EVENT = ''; diff --git a/src/lib/file.js b/src/lib/file.js index 151af9107..c2aa3b8c3 100644 --- a/src/lib/file.js +++ b/src/lib/file.js @@ -15,7 +15,8 @@ const FILE_FIELDS = [ 'extension', 'representations', 'watermark_info', - 'authenticated_download_url' + 'authenticated_download_url', + 'is_download_available' ]; /** diff --git a/src/lib/logUtils.js b/src/lib/logUtils.js index 57650fc35..edb198a19 100644 --- a/src/lib/logUtils.js +++ b/src/lib/logUtils.js @@ -50,13 +50,20 @@ export function getClientLogDetails() { * @param {string} code - Code associated with the error that occurred. * @param {string} [displayMessage] - Optional string to display, if applicable. * @param {*} [details] - Additional details about the error. IE) File id, reason, etc. + * @param {*} [properties] - Additional optional properties on the error * @return {Error} An error object with an associated error code. */ -export function createPreviewError(code, displayMessage = __('error_refresh'), details = null) { +export function createPreviewError(code, displayMessage = __('error_refresh'), details = null, properties) { const error = new Error(code); error.code = code; error.message = details || displayMessage; error.displayMessage = displayMessage; + if (properties) { + Object.keys(properties).forEach((key) => { + error[key] = properties[key]; + }); + } + return error; } diff --git a/src/lib/viewers/error/PreviewError.scss b/src/lib/viewers/error/PreviewError.scss index ab9e73be4..cfba5cfa9 100644 --- a/src/lib/viewers/error/PreviewError.scss +++ b/src/lib/viewers/error/PreviewError.scss @@ -6,7 +6,11 @@ height: 247px; // Error icon + text + optional download button text-align: center; - .bp-error-download { - padding-top: 20px; + .bp-error-text { + padding: 0 80px; + } + + .bp-btn { + margin-top: 20px; } } diff --git a/src/lib/viewers/error/PreviewErrorViewer.js b/src/lib/viewers/error/PreviewErrorViewer.js index 5b3fe233c..31b86cc5c 100644 --- a/src/lib/viewers/error/PreviewErrorViewer.js +++ b/src/lib/viewers/error/PreviewErrorViewer.js @@ -26,10 +26,13 @@ class PreviewErrorViewer extends BaseViewer { super.setup(); this.infoEl = this.containerEl.appendChild(document.createElement('div')); + this.infoEl.className = 'bp-error'; + this.iconEl = this.infoEl.appendChild(document.createElement('div')); this.iconEl.className = 'bp-icon bp-icon-file'; + this.messageEl = this.infoEl.appendChild(document.createElement('div')); - this.infoEl.className = 'bp-error'; + this.messageEl.className = 'bp-error-text'; } /** @@ -91,8 +94,11 @@ class PreviewErrorViewer extends BaseViewer { this.iconEl.innerHTML = this.icon; this.messageEl.textContent = displayMessage; - // Add optional download button - if (checkPermission(file, PERMISSION_DOWNLOAD) && showDownload && Browser.canDownload()) { + // Add optional link or download button + const { linkText, linkUrl } = err; + if (linkText && linkUrl) { + this.addLinkButton(linkText, linkUrl); + } else if (checkPermission(file, PERMISSION_DOWNLOAD) && showDownload && Browser.canDownload()) { this.addDownloadButton(); } @@ -111,17 +117,29 @@ class PreviewErrorViewer extends BaseViewer { } /** - * Adds optional download button + * Adds a link button underneath error message. + * + * @param {string} linkText - Translated button message + * @param {string} linkUrl - URL for link + * @return {void} + */ + addLinkButton(linkText, linkUrl) { + const linkBtnEl = this.infoEl.appendChild(document.createElement('a')); + linkBtnEl.className = 'bp-btn bp-btn-primary'; + linkBtnEl.target = '_blank'; + linkBtnEl.textContent = linkText; + linkBtnEl.href = linkUrl; + } + + /** + * Adds a download file button underneath error message. * * @private * @return {void} */ addDownloadButton() { - this.downloadEl = this.infoEl.appendChild(document.createElement('div')); - this.downloadEl.classList.add('bp-error-download'); - this.downloadBtnEl = this.downloadEl.appendChild(document.createElement('button')); - this.downloadBtnEl.classList.add('bp-btn'); - this.downloadBtnEl.classList.add('bp-btn-primary'); + this.downloadBtnEl = this.infoEl.appendChild(document.createElement('button')); + this.downloadBtnEl.className = 'bp-btn bp-btn-primary'; this.downloadBtnEl.textContent = __('download'); this.downloadBtnEl.addEventListener('click', this.download); } diff --git a/src/lib/viewers/error/__tests__/PreviewErrorViewer-test.js b/src/lib/viewers/error/__tests__/PreviewErrorViewer-test.js index 78013993a..899241c62 100644 --- a/src/lib/viewers/error/__tests__/PreviewErrorViewer-test.js +++ b/src/lib/viewers/error/__tests__/PreviewErrorViewer-test.js @@ -80,6 +80,20 @@ describe('lib/viewers/error/PreviewErrorViewer', () => { }); }); + it('should add link button if error has linkText and linkUrl defined', () => { + sandbox.stub(error, 'addLinkButton'); + sandbox.stub(error, 'addDownloadButton'); + + const err = new Error('reason'); + err.linkText = 'test'; + err.linkUrl = 'someUrl'; + + error.load(err); + + expect(error.addLinkButton).to.be.calledWith('test', 'someUrl'); + expect(error.addDownloadButton).to.not.be.called; + }); + it('should add download button if file has permissions and showDownload option is set', () => { sandbox.stub(error, 'addDownloadButton'); sandbox.stub(file, 'checkPermission').withArgs(error.options.file, PERMISSION_DOWNLOAD).returns(true); @@ -179,6 +193,19 @@ describe('lib/viewers/error/PreviewErrorViewer', () => { }); }); + describe('addLinkButton()', () => { + it('should add a link button with the appropriate message and URL', () => { + error.setup(); + error.addLinkButton('test', 'someUrl'); + const linkBtnEl = error.infoEl.querySelector('a'); + + expect(linkBtnEl instanceof HTMLElement).to.be.true; + expect(linkBtnEl.target).to.equal('_blank'); + expect(linkBtnEl.textContent).to.equal('test'); + expect(linkBtnEl.href).to.have.string('someUrl'); + }); + }); + describe('addDownloadButton()', () => { it('should add a download button and attach a download click handler', () => { error.setup(); @@ -186,9 +213,8 @@ describe('lib/viewers/error/PreviewErrorViewer', () => { error.addDownloadButton(); - expect(error.downloadEl instanceof HTMLElement).to.be.true; - expect(error.downloadEl.parentNode).to.equal(error.infoEl); expect(error.downloadBtnEl instanceof HTMLElement).to.be.true; + expect(error.downloadBtnEl.parentNode).to.equal(error.infoEl); expect(error.downloadBtnEl.classList.contains('bp-btn')).to.be.true; expect(error.downloadBtnEl.classList.contains('bp-btn-primary')).to.be.true; expect(error.downloadBtnEl.textContent).to.equal('Download');