Skip to content

Commit

Permalink
New: Support previews of non-current file versions (#608)
Browse files Browse the repository at this point in the history
This patch adds support for previewing non-current file versions. Specify the file version like so:

```
const preview = new Box.Preview();
preview.show(FILE_ID_1, ACCESS_TOKEN, {
    fileOptions: {
    	[FILE_ID_1]: {
	    fileVersionId: FILE_VERSION_ID_1
	}
    }
});
```

In this patch, we start caching file objects by both file ID and by file version ID, distinguishing between the two with a 'file_' or 'file_version_' prefix as the key to the cache. This allows us to be backwards-compatible and show the current file version if no file version ID is passed in, while always allowing a cached entry of a current version to be overriden when you preview a non-current file version. We abstract this logic to file.js, and rely on the helper methods cacheFile(), uncacheFile(), and getCachedFile() to interact with file objects and the cache. This patch also enables prefetching of files that were previously cached by file ID or file version ID.

To support previews of file versions, we introduce a new Preview option `fileOptions`, which allows us to specific individual options by file ID. This is needed instead of just passing in a `fileVersionId` opion since we support collections. Having this mapping of file ID to file options on that file ID allows us to specify file verison IDs for any number of file IDs to preview and can also be extended to support other file-level options in the future. Example of collection + file version support together:

```
const preview = new Box.Preview();
preview.show(FILE_ID_1, ACCESS_TOKEN, {
    collection: [FILE_ID_1, FILE_ID2],
    fileOptions: {
        [FILE_ID_1]: {
            fileVersionId: FILE_VERSION_ID_1
        },
        [FILE_ID_2]: {
            fileVersionId: FILE_VERSION_ID_2
        }
    }
});
```

Caveats:
- You cannot pass in the current file version ID as an option since the Box API will fail when you make a get file version info API call for the current version
- Preview events do not distinguish between file versions, so a preview of a previous file version will be simply logged as a preview of a file ID

This resolves #570.
  • Loading branch information
tonyjin authored Feb 3, 2018
1 parent 1ad415c commit e7e154c
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 73 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,15 @@ preview.show(fileId, accessToken, {
| collection | | List of file IDs to iterate over for previewing |
| header | 'light' | String value of 'none' or 'dark' or 'light' that controls header visibility and theme |
| logoUrl | | URL of logo to show in header |
| showAnnotations | false | Whether annotations and annotation controls are shown. This option will be overridden by viewer-specific annotation options if they are set. See [Box Annotations](https://github.com/box/box-annotations) for more details. |
| showAnnotations | false | Whether annotations and annotation controls are shown. This option will be overridden by viewer-specific annotation options if they are set. See [Box Annotations](https://github.com/box/box-annotations) for more details |
| showDownload | false | Whether download button is shown |
| useHotkeys | true | Whether hotkeys (keyboard shortcuts) are enabled |
| pauseRequireJS | false | Temporarily disables requireJS to allow Preview's third party dependencies to load |
| fileOptions | {} | Mapping of file ID to file-level option. See below for details |

| File Option | Description |
| --- | --- |
| fileVersionId | File version ID to preview. This must be a valid non-current file version ID. Use [Get Versions](https://developer.box.com/reference#view-versions-of-a-file) to fetch a list of file versions |

Access Token
------------
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"lint-staged": "^5.0.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1",
"mocha": "^4.0.1",
"mock-local-storage": "^1.0.2",
Expand Down
124 changes: 95 additions & 29 deletions src/lib/Preview.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable import/first */
import './polyfill';
import EventEmitter from 'events';
import throttle from 'lodash.throttle';
import cloneDeep from 'lodash.clonedeep';
import getProp from 'lodash.get';
import throttle from 'lodash.throttle';
/* eslint-enable import/first */
import Browser from './Browser';
import Logger from './Logger';
Expand All @@ -29,7 +30,9 @@ import {
checkFileValid,
cacheFile,
uncacheFile,
isWatermarked
isWatermarked,
getCachedFile,
normalizeFileVersion
} from './file';
import {
API_HOST,
Expand All @@ -42,7 +45,8 @@ import {
X_REP_HINT_DOC_THUMBNAIL,
X_REP_HINT_IMAGE,
X_REP_HINT_VIDEO_DASH,
X_REP_HINT_VIDEO_MP4
X_REP_HINT_VIDEO_MP4,
FILE_OPTION_FILE_VERSION_ID
} from './constants';
import { VIEWER_EVENT } from './events';
import './Preview.scss';
Expand Down Expand Up @@ -506,22 +510,23 @@ class Preview extends EventEmitter {
*
* @public
* @param {Object} options - Prefetch options
* @param {string} options.fileId - Box File ID
* @param {string} options.fileId - Box file ID (do not also pass a file version ID)
* @param {string} options.fileVersionId - Box file version ID (do not also pass a file ID)
* @param {string} options.token - Access token
* @param {string} options.sharedLink - Shared link
* @param {string} options.sharedLinkPassword - Shared link password
* @param {boolean} options.preload - Is this prefetch for a preload
* @param {string} token - Access token
* @return {void}
*/
prefetch({ fileId, token, sharedLink = '', sharedLinkPassword = '', preload = false }) {
prefetch({ fileId, fileVersionId, token, sharedLink = '', sharedLinkPassword = '', preload = false }) {
let file;
let loader;
let viewer;

// Determining the viewer could throw an error
try {
file = this.cache.get(fileId);
file = getCachedFile(this.cache, { fileId, fileVersionId });
loader = file ? this.getLoader(file) : null;
viewer = loader ? loader.determineViewer(file) : null;
if (!viewer) {
Expand Down Expand Up @@ -555,7 +560,7 @@ class Preview extends EventEmitter {
viewerInstance.prefetch({
assets: true,
// Prefetch preload if explicitly requested or if viewer has 'preload' option set
preload: preload || viewerInstance.getViewerOption('preload'),
preload: preload || !!viewerInstance.getViewerOption('preload'),
// Don't prefetch file's representation content if this is for preload
content: !preload
});
Expand Down Expand Up @@ -613,27 +618,64 @@ class Preview extends EventEmitter {
// Clear any existing retry timeouts
clearTimeout(this.retryTimeout);

// Save reference to the currently shown file, if any
// Save reference to the currently shown file ID and file version ID, if any
const currentFileId = this.file ? this.file.id : undefined;
const currentFileVersionId = this.file && this.file.file_version ? this.file.file_version.id : undefined;

// Check if file ID or well-formed file object was passed in
// Save reference to file version we want to load, if any
const fileVersionId = this.getFileOption(fileIdOrFile, FILE_OPTION_FILE_VERSION_ID) || '';

// Check what was passed to preview.show()—string file ID or some file object
if (typeof fileIdOrFile === 'string') {
// Use cached file data if available, otherwise create empty file object
this.file = this.cache.get(fileIdOrFile) || { id: fileIdOrFile };
} else if (checkFileValid(fileIdOrFile)) {
const fileId = fileIdOrFile;

// If we want to load by file version ID, use that as key for cache
const cacheKey = fileVersionId ? { fileVersionId } : { fileId };

// If file info is not cached, create a 'bare' file object that we populate with data from the server later
const bareFile = { id: fileId };
if (fileVersionId) {
bareFile.file_version = {
id: fileVersionId
};
}

this.file = getCachedFile(this.cache, cacheKey) || bareFile;

// Use well-formed file object if available
} else if (checkFileValid(fileIdOrFile)) {
this.file = fileIdOrFile;
} else if (!!fileIdOrFile && typeof fileIdOrFile.id === 'string') {
// File is not a well-formed file object but has an id
this.file = { id: fileIdOrFile.id };

// File is not a well-formed file object but has a file ID and/or file version ID (e.g. Content Explorer)
} else if (fileIdOrFile && typeof fileIdOrFile.id === 'string') {
/* eslint-disable camelcase */
const { id, file_version } = fileIdOrFile;

this.file = { id };
if (file_version) {
this.file.file_version = {
id: file_version.id
};
}
/* eslint-enable camelcase */
} else {
throw new Error(
'File is not a well-formed Box File object. See FILE_FIELDS in file.js for a list of required fields.'
);
}

// Retry up to RETRY_COUNT if we are reloading same file
if (this.file.id === currentFileId) {
// Retry up to RETRY_COUNT if we are reloading same file. If load is called during a preview when file version
// ID has been specified, count as a retry only if the current file verison ID matches that specified file
// version ID
if (fileVersionId) {
if (fileVersionId === currentFileVersionId) {
this.retryCount += 1;
} else {
this.retryCount = 0;
}

// Otherwise, count this as a retry if the file ID we are trying to load matches the current file ID
} else if (this.file.id === currentFileId) {
this.retryCount += 1;
} else {
this.retryCount = 0;
Expand Down Expand Up @@ -818,8 +860,9 @@ class Preview extends EventEmitter {
*/
loadFromServer() {
const { apiHost, queryParams } = this.options;
const fileVersionId = this.getFileOption(this.file.id, FILE_OPTION_FILE_VERSION_ID) || '';

const fileInfoUrl = appendQueryParams(getURL(this.file.id, apiHost), queryParams);
const fileInfoUrl = appendQueryParams(getURL(this.file.id, fileVersionId, apiHost), queryParams);
get(fileInfoUrl, this.getRequestHeaders())
.then(this.handleFileInfoResponse)
.catch(this.handleFetchError);
Expand All @@ -829,12 +872,23 @@ class Preview extends EventEmitter {
* Loads the preview from server response.
*
* @private
* @param {Object} file - File object
* @param {Object} response - File object response from API
* @return {void}
*/
handleFileInfoResponse(file) {
handleFileInfoResponse(response) {
let file = response;

// If we are previewing a file version, normalize response to a well-formed file object
if (this.getFileOption(this.file.id, FILE_OPTION_FILE_VERSION_ID)) {
file = normalizeFileVersion(response, this.file.id);
}

// If preview is closed or response comes back for an incorrect file, don't do anything
if (!this.open || (this.file && this.file.id !== file.id)) {
const responseFileVersionId = file.file_version.id;
if (
!this.open ||
(this.file && this.file.file_version && this.file.file_version.id !== responseFileVersionId)
) {
return;
}

Expand All @@ -844,7 +898,7 @@ class Preview extends EventEmitter {
this.logger.setFile(file);

// Keep reference to previously cached file version
const cachedFile = this.cache.get(file.id);
const cachedFile = getCachedFile(this.cache, { fileVersionId: responseFileVersionId });

// Explicitly uncache watermarked files, otherwise update cache
const isFileWatermarked = isWatermarked(file);
Expand Down Expand Up @@ -1148,7 +1202,7 @@ class Preview extends EventEmitter {
}

// Nuke the cache
this.cache.unset(this.file.id);
uncacheFile(this.cache, this.file);

// Check if hit the retry limit
if (this.retryCount > RETRY_COUNT) {
Expand Down Expand Up @@ -1211,7 +1265,7 @@ class Preview extends EventEmitter {
this.open = false;

// Nuke the cache
this.cache.unset(this.file.id);
uncacheFile(this.cache, this.file);

// Destroy anything still showing
this.destroy();
Expand Down Expand Up @@ -1280,11 +1334,12 @@ class Preview extends EventEmitter {
// Get access tokens for all files we should be prefetching
getTokens(filesToPrefetch, this.previewOptions.token)
.then((tokenMap) => {
filesToPrefetch.forEach((id) => {
const token = tokenMap[id];
filesToPrefetch.forEach((fileId) => {
const token = tokenMap[fileId];

// Append optional query params
const fileInfoUrl = appendQueryParams(getURL(id, apiHost), queryParams);
const fileVersionId = this.getFileOption(fileId, FILE_OPTION_FILE_VERSION_ID) || '';
const fileInfoUrl = appendQueryParams(getURL(fileId, fileVersionId, apiHost), queryParams);

// Prefetch and cache file information and content
get(fileInfoUrl, this.getRequestHeaders(token))
Expand All @@ -1301,7 +1356,7 @@ class Preview extends EventEmitter {
})
.catch((err) => {
/* eslint-disable no-console */
console.error(`Error prefetching file ID ${id} - ${err}`);
console.error(`Error prefetching file ID ${fileId} - ${err}`);
/* eslint-enable no-console */
});
});
Expand Down Expand Up @@ -1413,7 +1468,6 @@ class Preview extends EventEmitter {
/**
* Global keydown handler for preview.
*
*
* @private
* @param {Event} event - keydown event
* @return {void}
Expand Down Expand Up @@ -1466,6 +1520,18 @@ class Preview extends EventEmitter {
event.stopPropagation();
}
}

/**
* Helper to get specific file option for a file.
*
* @param {string|Object} fileIdOrFile - File ID or file object to get file version ID for
* @param {string} optionName - Name of option, e.g. fileVersionId
* @return {Object|undefined} Specific file option
*/
getFileOption(fileIdOrFile, optionName) {
const fileId = typeof fileIdOrFile === 'string' ? fileIdOrFile : fileIdOrFile.id;
return getProp(this.previewOptions, `fileOptions.${fileId}.${optionName}`);
}
}

global.Box = global.Box || {};
Expand Down
Loading

0 comments on commit e7e154c

Please sign in to comment.