1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-22 17:33:14 +01:00

Remove ReactS3FineUploader's dependency on react-router's location

ReactS3FineUploader used to check the current url’s query params to
determine which method it should use to upload, but this decision means
the component is tightly coupled with react-router and history.js. A
major pain point is having to propagate the location prop all the way
down to this component even when it’s not necessary.

Now, ReactS3FineUploader’s parent elements can either parse the current
query params themselves or, if they have a location from react-router,
simply use the location.

Added a few utils to help parse url params.
This commit is contained in:
Brett Sun 2015-10-30 17:43:20 +01:00
parent e145e50228
commit d23331d9b9
14 changed files with 190 additions and 170 deletions

View File

@ -20,8 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({
submitFile: React.PropTypes.func, submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool, editable: React.PropTypes.bool,
multiple: React.PropTypes.bool, multiple: React.PropTypes.bool
location: React.PropTypes.object
}, },
getDefaultProps() { getDefaultProps() {
@ -89,11 +88,10 @@ let FurtherDetailsFileuploader = React.createClass({
}} }}
areAssetsDownloadable={true} areAssetsDownloadable={true}
areAssetsEditable={this.props.editable} areAssetsEditable={this.props.editable}
multiple={this.props.multiple} multiple={this.props.multiple} />
location={this.props.location}/>
</Property> </Property>
); );
} }
}); });
export default FurtherDetailsFileuploader; export default FurtherDetailsFileuploader;

View File

@ -28,8 +28,7 @@ let CreateContractForm = React.createClass({
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string, singular: React.PropTypes.string,
plural: React.PropTypes.string plural: React.PropTypes.string
}), })
location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
@ -87,8 +86,7 @@ let CreateContractForm = React.createClass({
areAssetsEditable={true} areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload} fileClassToUpload={this.props.fileClassToUpload} />
location={this.props.location}/>
</Property> </Property>
<Property <Property
name='name' name='name'
@ -110,4 +108,4 @@ let CreateContractForm = React.createClass({
} }
}); });
export default CreateContractForm; export default CreateContractForm;

View File

@ -26,12 +26,15 @@ let RegisterPieceForm = React.createClass({
isFineUploaderActive: React.PropTypes.bool, isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool, isFineUploaderEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
children: React.PropTypes.element,
onLoggedOut: React.PropTypes.func, onLoggedOut: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it // For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
location: React.PropTypes.object location: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
}, },
getDefaultProps() { getDefaultProps() {
@ -116,7 +119,7 @@ let RegisterPieceForm = React.createClass({
onLoggedOut={this.props.onLoggedOut} onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable} disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing} enableLocalHashing={enableLocalHashing}
location={this.props.location}/> uploadMethod={this.props.location.query.method} />
</Property> </Property>
<Property <Property
name='artist_name' name='artist_name'

View File

@ -37,6 +37,7 @@ let InputFineUploader = React.createClass({
onLoggedOut: React.PropTypes.func, onLoggedOut: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
// provided by Property // provided by Property
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
@ -46,8 +47,7 @@ let InputFineUploader = React.createClass({
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string, singular: React.PropTypes.string,
plural: React.PropTypes.string plural: React.PropTypes.string
}), })
location: React.PropTypes.object
}, },
getInitialState() { getInitialState() {
@ -107,10 +107,10 @@ let InputFineUploader = React.createClass({
}} }}
onInactive={this.props.onLoggedOut} onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing} enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload} uploadMethod={this.props.uploadMethod}
location={this.props.location}/> fileClassToUpload={this.props.fileClassToUpload} />
); );
} }
}); });
export default InputFineUploader; export default InputFineUploader;

View File

@ -20,8 +20,7 @@ import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({ let ContractSettingsUpdateButton = React.createClass({
propTypes: { propTypes: {
contract: React.PropTypes.object, contract: React.PropTypes.object
location: React.PropTypes.object
}, },
submitFile(file) { submitFile(file) {
@ -90,10 +89,9 @@ let ContractSettingsUpdateButton = React.createClass({
plural: getLangText('UPDATE') plural: getLangText('UPDATE')
}} }}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile} submitFile={this.submitFile} />
location={this.props.location}/>
); );
} }
}); });
export default ContractSettingsUpdateButton; export default ContractSettingsUpdateButton;

View File

@ -27,6 +27,7 @@ let FileDragAndDrop = React.createClass({
areAssetsEditable: React.PropTypes.bool, areAssetsEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
// triggers a FileDragAndDrop-global spinner // triggers a FileDragAndDrop-global spinner
hashingProgress: React.PropTypes.number, hashingProgress: React.PropTypes.number,
@ -41,8 +42,7 @@ let FileDragAndDrop = React.createClass({
plural: React.PropTypes.string plural: React.PropTypes.string
}), }),
allowedExtensions: React.PropTypes.string, allowedExtensions: React.PropTypes.string
location: React.PropTypes.object
}, },
handleDragOver(event) { handleDragOver(event) {
@ -137,19 +137,19 @@ let FileDragAndDrop = React.createClass({
}, },
render: function () { render: function () {
let { filesToUpload, const {
dropzoneInactive, filesToUpload,
className, dropzoneInactive,
hashingProgress, className,
handleCancelHashing, hashingProgress,
multiple, handleCancelHashing,
enableLocalHashing, multiple,
fileClassToUpload, enableLocalHashing,
areAssetsDownloadable, uploadMethod,
areAssetsEditable, fileClassToUpload,
allowedExtensions, areAssetsDownloadable,
location areAssetsEditable,
} = this.props; allowedExtensions } = this.props;
// has files only is true if there are files that do not have the status deleted or canceled // has files only is true if there are files that do not have the status deleted or canceled
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
@ -185,8 +185,8 @@ let FileDragAndDrop = React.createClass({
hasFiles={hasFiles} hasFiles={hasFiles}
onClick={this.handleOnClick} onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing} enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload} uploadMethod={uploadMethod}
location={location}/> fileClassToUpload={fileClassToUpload} />
<FileDragAndDropPreviewIterator <FileDragAndDropPreviewIterator
files={filesToUpload} files={filesToUpload}
handleDeleteFile={this.handleDeleteFile} handleDeleteFile={this.handleDeleteFile}

View File

@ -3,30 +3,28 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { getLangText } from '../../../utils/lang_utils';
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils'; import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
import { getLangText } from '../../../utils/lang_utils';
import { getCurrentQueryParams } from '../../../utils/url_utils';
let FileDragAndDropDialog = React.createClass({ let FileDragAndDropDialog = React.createClass({
propTypes: { propTypes: {
hasFiles: React.PropTypes.bool, hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
onClick: React.PropTypes.func,
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
fileClassToUpload: React.PropTypes.shape({ fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string, singular: React.PropTypes.string,
plural: React.PropTypes.string plural: React.PropTypes.string
}), })
location: React.PropTypes.object
}, },
getDragDialog(fileClass) { getDragDialog(fileClass) {
if(dragAndDropAvailable) { if (dragAndDropAvailable) {
return [ return [
<p>{getLangText('Drag %s here', fileClass)}</p>, <p>{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p> <p>{getLangText('or')}</p>
@ -37,26 +35,31 @@ let FileDragAndDropDialog = React.createClass({
}, },
render() { render() {
const queryParams = this.props.location.query; const {
hasFiles,
multipleFiles,
enableLocalHashing,
uploadMethod,
fileClassToUpload,
onClick } = this.props;
if(this.props.hasFiles) { if (hasFiles) {
return null; return null;
} else { } else {
if(this.props.enableLocalHashing && !queryParams.method) { if (enableLocalHashing && !uploadMethod) {
const currentQueryParams = getCurrentQueryParams();
let queryParamsHash = Object.assign({}, queryParams); const queryParamsHash = Object.assign({}, currentQueryParams);
queryParamsHash.method = 'hash'; queryParamsHash.method = 'hash';
let queryParamsUpload = Object.assign({}, queryParams); const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsUpload.method = 'upload'; queryParamsUpload.method = 'upload';
let { location } = this.props;
return ( return (
<div className="file-drag-and-drop-dialog present-options"> <div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p> <p>{getLangText('Would you rather')}</p>
<Link <Link
to={location.pathname} to={window.location.pathname}
query={queryParamsHash}> query={queryParamsHash}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Hash your work')} {getLangText('Hash your work')}
@ -64,9 +67,9 @@ let FileDragAndDropDialog = React.createClass({
</Link> </Link>
<span> or </span> <span> or </span>
<Link <Link
to={location.pathname} to={window.location.pathname}
query={queryParamsUpload}> query={queryParamsUpload}>
<span className="btn btn-default btn-sm"> <span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')} {getLangText('Upload and hash your work')}
@ -75,26 +78,27 @@ let FileDragAndDropDialog = React.createClass({
</div> </div>
); );
} else { } else {
if(this.props.multipleFiles) { if (multipleFiles) {
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.plural)} {this.getDragDialog(fileClassToUpload.plural)}
<span <span
className="btn btn-default" className="btn btn-default"
onClick={this.props.onClick}> onClick={onClick}>
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} {getLangText('choose %s to upload', fileClassToUpload.plural)}
</span> </span>
</span> </span>
); );
} else { } else {
let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular); const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.singular)} {this.getDragDialog(fileClassToUpload.singular)}
<span <span
className="btn btn-default" className="btn btn-default"
onClick={this.props.onClick}> onClick={onClick}>
{dialog} {dialog}
</span> </span>
</span> </span>
@ -105,4 +109,4 @@ let FileDragAndDropDialog = React.createClass({
} }
}); });
export default FileDragAndDropDialog; export default FileDragAndDropDialog;

View File

@ -18,7 +18,6 @@ import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp }
import { getCookie } from '../../utils/fetch_api_utils'; import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let ReactS3FineUploader = React.createClass({ let ReactS3FineUploader = React.createClass({
propTypes: { propTypes: {
keyRoutine: React.PropTypes.shape({ keyRoutine: React.PropTypes.shape({
@ -107,11 +106,14 @@ let ReactS3FineUploader = React.createClass({
// One solution we found in the process of tackling this problem was to hash // One solution we found in the process of tackling this problem was to hash
// the file in the browser using md5 and then uploading the resulting text document instead // the file in the browser using md5 and then uploading the resulting text document instead
// of the actual file. // of the actual file.
// This boolean essentially enables that behavior //
// This boolean and string essentially enable that behavior.
// Right now, we determine which upload method to use by appending a query parameter,
// which should be passed into 'uploadMethod':
// 'hash': upload using the hash
// 'upload': upload full file (default if not specified)
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
// automatically injected by React-Router
query: React.PropTypes.object,
// A class of a file the user has to upload // A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural // Needs to be defined both in singular as well as in plural
@ -126,9 +128,7 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([ fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func, React.PropTypes.func,
React.PropTypes.element React.PropTypes.element
]), ])
location: React.PropTypes.object
}, },
getDefaultProps() { getDefaultProps() {
@ -192,11 +192,11 @@ let ReactS3FineUploader = React.createClass({
filesToUpload: [], filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()), uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
csrfToken: getCookie(AppConstants.csrftoken), csrfToken: getCookie(AppConstants.csrftoken),
// -1: aborted // -1: aborted
// -2: uninitialized // -2: uninitialized
hashingProgress: -2, hashingProgress: -2,
// this is for logging // this is for logging
chunks: {} chunks: {}
}; };
@ -354,7 +354,6 @@ let ReactS3FineUploader = React.createClass({
/* FineUploader specific callback function handlers */ /* FineUploader specific callback function handlers */
onUploadChunk(id, name, chunkData) { onUploadChunk(id, name, chunkData) {
let chunks = this.state.chunks; let chunks = this.state.chunks;
chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = { chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = {
@ -370,10 +369,9 @@ let ReactS3FineUploader = React.createClass({
}, },
onUploadChunkSuccess(id, chunkData, responseJson, xhr) { onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
let chunks = this.state.chunks; let chunks = this.state.chunks;
let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte; let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte;
if(chunks[chunkKey]) { if(chunks[chunkKey]) {
chunks[chunkKey].completed = true; chunks[chunkKey].completed = true;
chunks[chunkKey].responseJson = responseJson; chunks[chunkKey].responseJson = responseJson;
@ -414,7 +412,7 @@ let ReactS3FineUploader = React.createClass({
} else { } else {
console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader'); console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader');
} }
// for explanation, check comment of if statement above // for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload, // also, lets check if after the completion of this upload,
@ -597,7 +595,6 @@ let ReactS3FineUploader = React.createClass({
} else { } else {
throw new Error(getLangText('File upload could not be paused.')); throw new Error(getLangText('File upload could not be paused.'));
} }
}, },
handleResumeFile(fileId) { handleResumeFile(fileId) {
@ -647,16 +644,14 @@ let ReactS3FineUploader = React.createClass({
// md5 hash of a file locally and just upload a txt file containing that hash. // md5 hash of a file locally and just upload a txt file containing that hash.
// //
// In the view this only happens when the user is allowed to do local hashing as well // In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload') // as when the correct method prop is present ('hash' and not 'upload')
let queryParams = this.props.location.query; if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') {
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') { const convertedFilePromises = [];
let convertedFilePromises = [];
let overallFileSize = 0; let overallFileSize = 0;
// "files" is not a classical Javascript array but a Javascript FileList, therefore // "files" is not a classical Javascript array but a Javascript FileList, therefore
// we can not use map to convert values // we can not use map to convert values
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
// for calculating the overall progress of all submitted files // for calculating the overall progress of all submitted files
// we'll need to calculate the overall sum of all files' sizes // we'll need to calculate the overall sum of all files' sizes
overallFileSize += files[i].size; overallFileSize += files[i].size;
@ -668,7 +663,6 @@ let ReactS3FineUploader = React.createClass({
// we're using promises to handle that // we're using promises to handle that
let hashedFilePromise = computeHashOfFile(files[i]); let hashedFilePromise = computeHashOfFile(files[i]);
convertedFilePromises.push(hashedFilePromise); convertedFilePromises.push(hashedFilePromise);
} }
// To react after the computation of all files, we define the resolvement // To react after the computation of all files, we define the resolvement
@ -676,7 +670,6 @@ let ReactS3FineUploader = React.createClass({
// with their txt representative // with their txt representative
Q.all(convertedFilePromises) Q.all(convertedFilePromises)
.progress(({index, value: {progress, reject}}) => { .progress(({index, value: {progress, reject}}) => {
// hashing progress has been aborted from outside // hashing progress has been aborted from outside
// To get out of the executing, we need to call reject from the // To get out of the executing, we need to call reject from the
// inside of the promise's execution. // inside of the promise's execution.
@ -696,18 +689,14 @@ let ReactS3FineUploader = React.createClass({
// currently hashing files // currently hashing files
let overallHashingProgress = 0; let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize; let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress; overallHashingProgress += filesSliceOfOverall * files[i].progress;
} }
// Multiply by 100, since react-progressbar expects decimal numbers // Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100}); this.setState({ hashingProgress: overallHashingProgress * 100});
}) })
.then((convertedFiles) => { .then((convertedFiles) => {
// clear hashing progress, since its done // clear hashing progress, since its done
this.setState({ hashingProgress: -2}); this.setState({ hashingProgress: -2});
@ -823,20 +812,18 @@ let ReactS3FineUploader = React.createClass({
changeSet.status = { $set: status }; changeSet.status = { $set: status };
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
this.setState({ filesToUpload }); this.setState({ filesToUpload });
}, },
isDropzoneInactive() { isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.props.location.query;
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true; return true;
} else { } else {
return false; return false;
} }
}, },
getAllowedExtensions() { getAllowedExtensions() {
@ -850,17 +837,16 @@ let ReactS3FineUploader = React.createClass({
}, },
render() { render() {
let { const {
multiple, multiple,
areAssetsDownloadable, areAssetsDownloadable,
areAssetsEditable, areAssetsEditable,
onInactive, onInactive,
enableLocalHashing, enableLocalHashing,
fileClassToUpload, uploadMethod,
validation, fileClassToUpload,
fileInputElement, validation,
location fileInputElement } = this.props;
} = this.props;
// Here we initialize the template that has been either provided from the outside // Here we initialize the template that has been either provided from the outside
// or the default input that is FileDragAndDrop. // or the default input that is FileDragAndDrop.
@ -870,8 +856,8 @@ let ReactS3FineUploader = React.createClass({
areAssetsEditable, areAssetsEditable,
onInactive, onInactive,
enableLocalHashing, enableLocalHashing,
uploadMethod,
fileClassToUpload, fileClassToUpload,
location,
onDrop: this.handleUploadFile, onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload, filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile, handleDeleteFile: this.handleDeleteFile,

View File

@ -2,8 +2,8 @@
import requests from '../utils/requests'; import requests from '../utils/requests';
import { generateOrderingQueryParams } from '../utils/fetch_api_utils';
import { mergeOptions } from '../utils/general_utils'; import { mergeOptions } from '../utils/general_utils';
import { generateOrderingQueryParams } from '../utils/url_utils';
let EditionListFetcher = { let EditionListFetcher = {
/** /**

View File

@ -3,7 +3,7 @@
import requests from '../utils/requests'; import requests from '../utils/requests';
import { mergeOptions } from '../utils/general_utils'; import { mergeOptions } from '../utils/general_utils';
import { generateOrderingQueryParams } from '../utils/fetch_api_utils'; import { generateOrderingQueryParams } from '../utils/url_utils';
let PieceListFetcher = { let PieceListFetcher = {
/** /**

View File

@ -2,63 +2,10 @@
import Q from 'q'; import Q from 'q';
import { sanitize } from './general_utils';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
// TODO: Create Unittests that test all functions // TODO: Create Unittests that test all functions
/**
* Takes a key-value object of this form:
*
* {
* 'page': 1,
* 'pageSize': 10
* }
*
* and converts it to a query-parameter, which you can append to your URL.
* The return looks like this:
*
* ?page=1&page_size=10
*
* CamelCase gets converted to snake_case!
*
*/
export function argsToQueryParams(obj) {
obj = sanitize(obj);
return Object
.keys(obj)
.map((key, i) => {
let s = '';
if(i === 0) {
s += '?';
} else {
s += '&';
}
let snakeCaseKey = key.replace(/[A-Z]/, (match) => '_' + match.toLowerCase());
return s + snakeCaseKey + '=' + encodeURIComponent(obj[key]);
})
.join('');
}
/**
* Takes a string and a boolean and generates a string query parameter for
* an API call.
*/
export function generateOrderingQueryParams(orderBy, orderAsc) {
let interpolation = '';
if(!orderAsc) {
interpolation += '-';
}
return interpolation + orderBy;
}
export function status(response) { export function status(response) {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response; return response;
@ -68,7 +15,7 @@ export function status(response) {
export function getCookie(name) { export function getCookie(name) {
let parts = document.cookie.split(';'); let parts = document.cookie.split(';');
for(let i = 0; i < parts.length; i++) { for(let i = 0; i < parts.length; i++) {
if(parts[i].indexOf(AppConstants.csrftoken + '=') > -1) { if(parts[i].indexOf(AppConstants.csrftoken + '=') > -1) {
return parts[i].split('=').pop(); return parts[i].split('=').pop();
@ -111,4 +58,4 @@ export function fetchImageAsBlob(url) {
xhr.send(); xhr.send();
}); });
} }

View File

@ -2,11 +2,11 @@
import Q from 'q'; import Q from 'q';
import { argsToQueryParams, getCookie } from '../utils/fetch_api_utils';
import AppConstants from '../constants/application_constants'; import AppConstants from '../constants/application_constants';
import {excludePropFromObject} from '../utils/general_utils'; import { getCookie } from '../utils/fetch_api_utils';
import { excludePropFromObject } from '../utils/general_utils';
import { argsToQueryParams } from '../utils/url_utils';
class Requests { class Requests {
_merge(defaults, options) { _merge(defaults, options) {

83
js/utils/url_utils.js Normal file
View File

@ -0,0 +1,83 @@
'use strict'
import camelCase from 'camelcase';
import snakeCase from 'snake-case';
import qs from 'qs';
import { sanitize } from './general_utils';
// TODO: Create Unittests that test all functions
/**
* Takes a key-value dictionary of this form:
*
* {
* 'page': 1,
* 'pageSize': 10
* }
*
* and converts it to a query-parameter, which you can append to your URL.
* The return looks like this:
*
* ?page=1&page_size=10
*
* CamelCase gets converted to snake_case!
*
* @param {object} obj Query params dictionary
* @return {string} Query params string
*/
export function argsToQueryParams(obj) {
const sanitizedObj = sanitize(obj);
const queryParamObj = {};
Object
.keys(sanitizedObj)
.forEach((key) => {
queryParamObj[snakeCase(key)] = sanitizedObj[key];
});
// Use bracket arrayFormat as history.js and react-router use it
return '?' + qs.stringify(queryParamObj, { arrayFormat: 'brackets' });
}
/**
* Get the current url's query params as an key-val dictionary.
* snake_case gets converted to CamelCase!
* @return {object} Query params dictionary
*/
export function getCurrentQueryParams() {
return queryParamsToArgs(window.location.search.substring(1));
}
/**
* Convert the given query param string into a key-val dictionary.
* snake_case gets converted to CamelCase!
* @param {string} queryParamString Query params string
* @return {object} Query params dictionary
*/
export function queryParamsToArgs(queryParamString) {
const qsQueryParamObj = qs.parse(queryParamString);
const camelCaseParamObj = {};
Object
.keys(qsQueryParamObj)
.forEach((key) => {
camelCaseParamObj[camelCase(key)] = qsQueryParamObj[key];
});
return camelCaseParamObj;
}
/**
* Takes a string and a boolean and generates a string query parameter for
* an API call.
*/
export function generateOrderingQueryParams(orderBy, orderAsc) {
let interpolation = '';
if(!orderAsc) {
interpolation += '-';
}
return interpolation + orderBy;
}

View File

@ -46,6 +46,7 @@
"browser-sync": "^2.7.5", "browser-sync": "^2.7.5",
"browserify": "^9.0.8", "browserify": "^9.0.8",
"browserify-shim": "^3.8.10", "browserify-shim": "^3.8.10",
"camelcase": "^1.2.1",
"classnames": "^1.2.2", "classnames": "^1.2.2",
"compression": "^1.4.4", "compression": "^1.4.4",
"envify": "^3.4.0", "envify": "^3.4.0",
@ -73,6 +74,7 @@
"object-assign": "^2.0.0", "object-assign": "^2.0.0",
"opn": "^3.0.2", "opn": "^3.0.2",
"q": "^1.4.1", "q": "^1.4.1",
"qs": "^5.2.0",
"raven-js": "^1.1.19", "raven-js": "^1.1.19",
"react": "0.13.2", "react": "0.13.2",
"react-bootstrap": "0.25.1", "react-bootstrap": "0.25.1",
@ -83,6 +85,7 @@
"react-textarea-autosize": "^2.5.2", "react-textarea-autosize": "^2.5.2",
"reactify": "^1.1.0", "reactify": "^1.1.0",
"shmui": "^0.1.0", "shmui": "^0.1.0",
"snake-case": "^1.1.1",
"spark-md5": "~1.0.0", "spark-md5": "~1.0.0",
"uglifyjs": "^2.4.10", "uglifyjs": "^2.4.10",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",