mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 09:23:13 +01:00
Merge with master
This commit is contained in:
commit
841cc05525
3
.env-template
Normal file
3
.env-template
Normal file
@ -0,0 +1,3 @@
|
||||
SAUCE_USERNAME=ascribe
|
||||
SAUCE_ACCESS_KEY=
|
||||
SAUCE_DEFAULT_URL=
|
@ -2,7 +2,7 @@
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
"es6": true,
|
||||
},
|
||||
"rules": {
|
||||
"new-cap": [2, {newIsCap: true, capIsNew: false}],
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ gemini-report/*
|
||||
node_modules/*
|
||||
|
||||
.DS_Store
|
||||
.env
|
||||
|
11
README.md
11
README.md
@ -57,6 +57,7 @@ We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](
|
||||
|
||||
SCSS Code Conventions
|
||||
=====================
|
||||
|
||||
Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor.
|
||||
|
||||
Some interesting links:
|
||||
@ -66,15 +67,17 @@ Some interesting links:
|
||||
Branch names
|
||||
============
|
||||
|
||||
Since we moved to Github, we cannot create branch names automatically with JIRA anymore.
|
||||
To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion.
|
||||
To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches:
|
||||
|
||||
```
|
||||
// For issues logged in Github:
|
||||
AG-<Github-issue-id>-brief-and-sane-description-of-the-ticket
|
||||
|
||||
// For issues logged in JIRA:
|
||||
AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
|
||||
```
|
||||
|
||||
where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title.
|
||||
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
|
||||
where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title.
|
||||
|
||||
|
||||
Example
|
||||
|
@ -39,7 +39,7 @@ class EditionListActions {
|
||||
|
||||
return Q.Promise((resolve, reject) => {
|
||||
EditionListFetcher
|
||||
.fetch({ pieceId, page, itemsToFetch, orderBy, orderAsc, filterBy })
|
||||
.fetch({ pieceId, page, orderBy, orderAsc, filterBy, pageSize: itemsToFetch })
|
||||
.then((res) => {
|
||||
if (res && !res.editions) {
|
||||
throw new Error('Piece has no editions to fetch.');
|
||||
|
@ -8,6 +8,7 @@ import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import { validationTypes } from '../../constants/uploader_constants';
|
||||
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
@ -15,23 +16,26 @@ import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
let FurtherDetailsFileuploader = React.createClass({
|
||||
propTypes: {
|
||||
pieceId: React.PropTypes.number.isRequired,
|
||||
|
||||
areAssetsDownloadable: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
label: React.PropTypes.string,
|
||||
pieceId: React.PropTypes.number,
|
||||
multiple: React.PropTypes.bool,
|
||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
onValidationFailed: React.PropTypes.func,
|
||||
setIsUploadReady: React.PropTypes.func,
|
||||
submitFile: React.PropTypes.func,
|
||||
onValidationFailed: React.PropTypes.func,
|
||||
isReadyForFormSubmission: React.PropTypes.func,
|
||||
editable: React.PropTypes.bool,
|
||||
multiple: React.PropTypes.bool,
|
||||
areAssetsDownloadable: React.PropTypes.bool
|
||||
validation: ReactS3FineUploader.propTypes.validation
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
areAssetsDownloadable: true,
|
||||
label: getLangText('Additional files'),
|
||||
multiple: false
|
||||
multiple: false,
|
||||
validation: validationTypes.additionalData
|
||||
};
|
||||
},
|
||||
|
||||
@ -61,7 +65,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
||||
url: ApiUrls.blob_otherdatas,
|
||||
pieceId: this.props.pieceId
|
||||
}}
|
||||
validation={AppConstants.fineUploader.validation.additionalData}
|
||||
validation={this.props.validation}
|
||||
submitFile={this.props.submitFile}
|
||||
onValidationFailed={this.props.onValidationFailed}
|
||||
setIsUploadReady={this.props.setIsUploadReady}
|
||||
|
@ -65,7 +65,16 @@ let MediaContainer = React.createClass({
|
||||
// We also force uniqueness of usernames, so this check is safe to dtermine if the
|
||||
// content was registered by the current user.
|
||||
const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered);
|
||||
const fileExtension = extractFileExtensionFromString(content.digital_work.url);
|
||||
|
||||
// We want to show the file's extension as a label of the download button.
|
||||
// We can however not only use `extractFileExtensionFromString` on the url for that
|
||||
// as files might be saved on S3 without a file extension which leads
|
||||
// `extractFileExtensionFromString` to extract everything starting from the top level
|
||||
// domain: e.g. '.net/live/<hash>'.
|
||||
// Therefore, we extract the file's name (last part of url, separated with a slash)
|
||||
// and try to extract the file extension from there.
|
||||
const fileName = content.digital_work.url.split('/').pop();
|
||||
const fileExtension = extractFileExtensionFromString(fileName);
|
||||
|
||||
let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
|
||||
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
|
||||
@ -123,7 +132,11 @@ let MediaContainer = React.createClass({
|
||||
className="ascribe-margin-1px"
|
||||
href={content.digital_work.url}
|
||||
target="_blank">
|
||||
{getLangText('Download')} .{fileExtension} <Glyphicon glyph="cloud-download"/>
|
||||
{/*
|
||||
If it turns out that `fileExtension` is an empty string, we're just
|
||||
using the label 'file'.
|
||||
*/}
|
||||
{getLangText('Download')} .{fileExtension || 'file'} <Glyphicon glyph="cloud-download"/>
|
||||
</Button>
|
||||
</AclProxy>
|
||||
{embed}
|
||||
|
@ -2,19 +2,20 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Form from '../ascribe_forms/form';
|
||||
import Property from '../ascribe_forms/property';
|
||||
import ContractListActions from '../../actions/contract_list_actions';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
import ContractListActions from '../../actions/contract_list_actions';
|
||||
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
|
||||
import InputFineUploader from './input_fineuploader';
|
||||
|
||||
import Form from '../ascribe_forms/form';
|
||||
import Property from '../ascribe_forms/property';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import { validationTypes } from '../../constants/uploader_constants';
|
||||
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||
|
||||
@ -78,8 +79,8 @@ let CreateContractForm = React.createClass({
|
||||
url: ApiUrls.blob_contracts
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
itemLimit: validationTypes.additionalData.itemLimit,
|
||||
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||
allowedExtensions: ['pdf']
|
||||
}}
|
||||
areAssetsDownloadable={true}
|
||||
|
@ -8,12 +8,16 @@ import UserActions from '../../actions/user_actions';
|
||||
import Form from './form';
|
||||
import Property from './property';
|
||||
import InputFineUploader from './input_fineuploader';
|
||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
|
||||
import FormSubmitButton from '../ascribe_buttons/form_submit_button';
|
||||
|
||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
|
||||
import AscribeSpinner from '../ascribe_spinner';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import AscribeSpinner from '../ascribe_spinner';
|
||||
import { validationParts, validationTypes } from '../../constants/uploader_constants';
|
||||
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
import { mergeOptions } from '../../utils/general_utils';
|
||||
@ -180,7 +184,7 @@ let RegisterPieceForm = React.createClass({
|
||||
createBlobRoutine={{
|
||||
url: ApiUrls.blob_digitalworks
|
||||
}}
|
||||
validation={AppConstants.fineUploader.validation.registerWork}
|
||||
validation={validationTypes.registerWork}
|
||||
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
isFineUploaderActive={isFineUploaderActive}
|
||||
@ -206,9 +210,9 @@ let RegisterPieceForm = React.createClass({
|
||||
fileClass: 'thumbnail'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit,
|
||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
||||
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||
allowedExtensions: validationParts.allowedExtensions.images
|
||||
}}
|
||||
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
|
||||
fileClassToUpload={{
|
||||
|
@ -28,11 +28,7 @@ const InputFineUploader = React.createClass({
|
||||
createBlobRoutine: shape({
|
||||
url: string
|
||||
}),
|
||||
validation: shape({
|
||||
itemLimit: number,
|
||||
sizeLimit: string,
|
||||
allowedExtensions: arrayOf(string)
|
||||
}),
|
||||
validation: ReactS3FineUploader.propTypes.validation,
|
||||
|
||||
// isFineUploaderActive is used to lock react fine uploader in case
|
||||
// a user is actually not logged in already to prevent him from droping files
|
||||
|
@ -2,17 +2,18 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
|
||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
|
||||
import ContractListActions from '../../actions/contract_list_actions';
|
||||
|
||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
||||
|
||||
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
|
||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
|
||||
import ApiUrls from '../../constants/api_urls';
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
import { validationTypes } from '../../constants/uploader_constants';
|
||||
|
||||
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
@ -68,8 +69,8 @@ let ContractSettingsUpdateButton = React.createClass({
|
||||
url: ApiUrls.blob_contracts
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
itemLimit: validationTypes.registerWork.itemLimit,
|
||||
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||
allowedExtensions: ['pdf']
|
||||
}}
|
||||
setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}}
|
||||
|
@ -23,6 +23,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
s3Url: string,
|
||||
s3UrlSafe: string
|
||||
}).isRequired,
|
||||
|
||||
handleDeleteFile: func,
|
||||
handleCancelFile: func,
|
||||
handlePauseFile: func,
|
||||
@ -33,9 +34,9 @@ const FileDragAndDropPreview = React.createClass({
|
||||
},
|
||||
|
||||
toggleUploadProcess() {
|
||||
if(this.props.file.status === 'uploading') {
|
||||
if (this.props.file.status === 'uploading') {
|
||||
this.props.handlePauseFile(this.props.file.id);
|
||||
} else if(this.props.file.status === 'paused') {
|
||||
} else if (this.props.file.status === 'paused') {
|
||||
this.props.handleResumeFile(this.props.file.id);
|
||||
}
|
||||
},
|
||||
@ -54,13 +55,13 @@ const FileDragAndDropPreview = React.createClass({
|
||||
(file.status === 'upload successful' || file.status === 'online') &&
|
||||
file.s3UrlSafe) {
|
||||
handleDeleteFile(file.id);
|
||||
} else if(handleCancelFile) {
|
||||
} else if (handleCancelFile) {
|
||||
handleCancelFile(file.id);
|
||||
}
|
||||
},
|
||||
|
||||
handleDownloadFile() {
|
||||
if(this.props.file.s3Url) {
|
||||
if (this.props.file.s3Url) {
|
||||
// This simply opens a new browser tab with the url provided
|
||||
open(this.props.file.s3Url);
|
||||
}
|
||||
@ -69,7 +70,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
getFileName() {
|
||||
const { numberOfDisplayedFiles, file } = this.props;
|
||||
|
||||
if(numberOfDisplayedFiles === 1) {
|
||||
if (numberOfDisplayedFiles === 1) {
|
||||
return (
|
||||
<span className="file-name">
|
||||
{truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))}
|
||||
@ -81,7 +82,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
},
|
||||
|
||||
getRemoveButton() {
|
||||
if(this.props.areAssetsEditable) {
|
||||
if (this.props.areAssetsEditable) {
|
||||
return (
|
||||
<div className="delete-file">
|
||||
<span
|
||||
@ -107,7 +108,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
|
||||
// Decide whether an image or a placeholder picture should be displayed
|
||||
// If a file has its `thumbnailUrl` defined, then we display it also as an image
|
||||
if(file.type.split('/')[0] === 'image' || file.thumbnailUrl) {
|
||||
if (file.type.split('/')[0] === 'image' || file.thumbnailUrl) {
|
||||
previewElement = (
|
||||
<FileDragAndDropPreviewImage
|
||||
onClick={this.handleDeleteFile}
|
||||
@ -123,7 +124,7 @@ const FileDragAndDropPreview = React.createClass({
|
||||
<FileDragAndDropPreviewOther
|
||||
onClick={this.handleDeleteFile}
|
||||
progress={file.progress}
|
||||
type={file.type.split('/')[1]}
|
||||
type={extractFileExtensionFromString(file.name)}
|
||||
toggleUploadProcess={this.toggleUploadProcess}
|
||||
areAssetsDownloadable={areAssetsDownloadable}
|
||||
downloadUrl={file.s3UrlSafe}
|
||||
|
@ -56,9 +56,9 @@ const FileDragAndDropPreviewOther = React.createClass({
|
||||
target="_blank"
|
||||
className="glyphicon glyphicon-download action-file"
|
||||
aria-hidden="true"
|
||||
title={getLangText('Download file')}/>
|
||||
title={getLangText('Download file')} />
|
||||
);
|
||||
} else if(progress >= 0 && progress < 100) {
|
||||
} else if (progress >= 0 && progress < 100) {
|
||||
actionSymbol = (
|
||||
<div className="spinner-file">
|
||||
<AscribeSpinner color='dark-blue' size='md' />
|
||||
@ -66,22 +66,19 @@ const FileDragAndDropPreviewOther = React.createClass({
|
||||
);
|
||||
} else {
|
||||
actionSymbol = (
|
||||
<span className='ascribe-icon icon-ascribe-ok action-file'/>
|
||||
<span className='ascribe-icon icon-ascribe-ok action-file' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="file-drag-and-drop-preview">
|
||||
<div className="file-drag-and-drop-preview">
|
||||
<ProgressBar
|
||||
now={Math.ceil(progress)}
|
||||
style={style}
|
||||
className="ascribe-progress-bar ascribe-progress-bar-xs"/>
|
||||
<div className="file-drag-and-drop-preview-table-wrapper">
|
||||
<div className="file-drag-and-drop-preview-other">
|
||||
{actionSymbol}
|
||||
<p style={style}>{'.' + type}</p>
|
||||
</div>
|
||||
className="ascribe-progress-bar ascribe-progress-bar-xs" />
|
||||
<div className="file-drag-and-drop-preview-other">
|
||||
{actionSymbol}
|
||||
<p style={style}>{'.' + (type ? type : 'file')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -13,9 +13,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
|
||||
|
||||
import AppConstants from '../../constants/application_constants';
|
||||
|
||||
import { computeHashOfFile } from '../../utils/file_utils';
|
||||
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
|
||||
import { getCookie } from '../../utils/fetch_api_utils';
|
||||
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
|
||||
import { getLangText } from '../../utils/lang_utils';
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ const ReactS3FineUploader = React.createClass({
|
||||
}),
|
||||
validation: shape({
|
||||
itemLimit: number,
|
||||
sizeLimit: string,
|
||||
sizeLimit: number,
|
||||
allowedExtensions: arrayOf(string)
|
||||
}),
|
||||
messages: shape({
|
||||
@ -278,22 +278,6 @@ const ReactS3FineUploader = React.createClass({
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
// Cancel uploads and clear previously selected files on the input element
|
||||
cancelUploads(id) {
|
||||
typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
|
||||
|
||||
// Reset the file input element to clear the previously selected files so that
|
||||
// the user can reselect them again.
|
||||
this.clearFileSelection();
|
||||
},
|
||||
|
||||
clearFileSelection() {
|
||||
const { fileInput } = this.refs;
|
||||
if (fileInput && typeof fileInput.clearSelection === 'function') {
|
||||
fileInput.clearSelection();
|
||||
}
|
||||
},
|
||||
|
||||
requestKey(fileId) {
|
||||
let filename = this.state.uploader.getName(fileId);
|
||||
let uuid = this.state.uploader.getUuid(fileId);
|
||||
@ -384,6 +368,107 @@ const ReactS3FineUploader = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
// Cancel uploads and clear previously selected files on the input element
|
||||
cancelUploads(id) {
|
||||
typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
|
||||
|
||||
// Reset the file input element to clear the previously selected files so that
|
||||
// the user can reselect them again.
|
||||
this.clearFileSelection();
|
||||
},
|
||||
|
||||
clearFileSelection() {
|
||||
const { fileInput } = this.refs;
|
||||
if (fileInput && typeof fileInput.clearSelection === 'function') {
|
||||
fileInput.clearSelection();
|
||||
}
|
||||
},
|
||||
|
||||
getAllowedExtensions() {
|
||||
const { validation: { allowedExtensions } = {} } = this.props;
|
||||
|
||||
if (allowedExtensions && allowedExtensions.length) {
|
||||
return transformAllowedExtensionsToInputAcceptProp(allowedExtensions);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getXhrErrorComment(xhr) {
|
||||
if (xhr) {
|
||||
return {
|
||||
response: xhr.response,
|
||||
url: xhr.responseURL,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
isDropzoneInactive() {
|
||||
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
|
||||
|
||||
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
isFileValid(file) {
|
||||
const { validation: { allowedExtensions, sizeLimit = 0 }, onValidationFailed } = this.props;
|
||||
const fileExt = extractFileExtensionFromString(file.name);
|
||||
|
||||
if (file.size > sizeLimit) {
|
||||
const fileSizeInMegaBytes = sizeLimit / 1000000;
|
||||
|
||||
const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
|
||||
if (typeof onValidationFailed === 'function') {
|
||||
onValidationFailed(file);
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (allowedExtensions && !allowedExtensions.includes(fileExt)) {
|
||||
const notification = new GlobalNotificationModel(getLangText(`The file you've submitted is of an invalid file format: Valid format(s): ${allowedExtensions.join(', ')}`), 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
selectValidFiles(files) {
|
||||
return Array.from(files).reduce((validFiles, file) => {
|
||||
if (this.isFileValid(file)) {
|
||||
validFiles.push(file);
|
||||
}
|
||||
return validFiles;
|
||||
}, []);
|
||||
},
|
||||
|
||||
// This method has been made promise-based to immediately afterwards
|
||||
// call a callback function (instantly after this.setState went through)
|
||||
// This is e.g. needed when showing/hiding the optional thumbnail upload
|
||||
// field in the registration form
|
||||
setStatusOfFile(fileId, status) {
|
||||
return Q.Promise((resolve) => {
|
||||
let changeSet = {};
|
||||
|
||||
if(status === 'deleted' || status === 'canceled') {
|
||||
changeSet.progress = { $set: 0 };
|
||||
}
|
||||
|
||||
changeSet.status = { $set: status };
|
||||
|
||||
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
|
||||
|
||||
this.setState({ filesToUpload }, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
setThumbnailForFileId(fileId, url) {
|
||||
const { filesToUpload } = this.state;
|
||||
|
||||
@ -506,34 +591,6 @@ const ReactS3FineUploader = React.createClass({
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
},
|
||||
|
||||
getXhrErrorComment(xhr) {
|
||||
if (xhr) {
|
||||
return {
|
||||
response: xhr.response,
|
||||
url: xhr.responseURL,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
isFileValid(file) {
|
||||
if (file.size > this.props.validation.sizeLimit) {
|
||||
const fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000;
|
||||
|
||||
const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
|
||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
||||
|
||||
if (typeof this.props.onValidationFailed === 'function') {
|
||||
this.props.onValidationFailed(file);
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
onCancel(id) {
|
||||
// when a upload is canceled, we need to update this components file array
|
||||
this.setStatusOfFile(id, 'canceled')
|
||||
@ -670,6 +727,13 @@ const ReactS3FineUploader = React.createClass({
|
||||
this.cancelUploads(fileId);
|
||||
},
|
||||
|
||||
handleCancelHashing() {
|
||||
// Every progress tick of the hashing function in handleUploadFile there is a
|
||||
// check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels
|
||||
// the hashing of all files immediately.
|
||||
this.setState({ hashingProgress: -1 });
|
||||
},
|
||||
|
||||
handlePauseFile(fileId) {
|
||||
if(this.state.uploader.pauseUpload(fileId)) {
|
||||
this.setStatusOfFile(fileId, 'paused');
|
||||
@ -698,15 +762,8 @@ const ReactS3FineUploader = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
// validate each submitted file if it fits the file size
|
||||
let validFiles = [];
|
||||
for(let i = 0; i < files.length; i++) {
|
||||
if(this.isFileValid(files[i])) {
|
||||
validFiles.push(files[i]);
|
||||
}
|
||||
}
|
||||
// override standard files list with only valid files
|
||||
files = validFiles;
|
||||
// Select only the submitted files that fit the file size and allowed extensions
|
||||
files = this.selectValidFiles(files);
|
||||
|
||||
// if multiple is set to false and user drops multiple files into the dropzone,
|
||||
// take the first one and notify user that only one file can be submitted
|
||||
@ -817,13 +874,6 @@ const ReactS3FineUploader = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
handleCancelHashing() {
|
||||
// Every progress tick of the hashing function in handleUploadFile there is a
|
||||
// check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels
|
||||
// the hashing of all files immediately.
|
||||
this.setState({ hashingProgress: -1 });
|
||||
},
|
||||
|
||||
// ReactFineUploader is essentially just a react layer around s3 fineuploader.
|
||||
// However, since we need to display the status of a file (progress, uploading) as well as
|
||||
// be able to execute actions on a currently uploading file we need to exactly sync the file list
|
||||
@ -893,46 +943,6 @@ const ReactS3FineUploader = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
// This method has been made promise-based to immediately afterwards
|
||||
// call a callback function (instantly after this.setState went through)
|
||||
// This is e.g. needed when showing/hiding the optional thumbnail upload
|
||||
// field in the registration form
|
||||
setStatusOfFile(fileId, status) {
|
||||
return Q.Promise((resolve) => {
|
||||
let changeSet = {};
|
||||
|
||||
if(status === 'deleted' || status === 'canceled') {
|
||||
changeSet.progress = { $set: 0 };
|
||||
}
|
||||
|
||||
changeSet.status = { $set: status };
|
||||
|
||||
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
|
||||
|
||||
this.setState({ filesToUpload }, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
isDropzoneInactive() {
|
||||
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
|
||||
|
||||
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getAllowedExtensions() {
|
||||
let { validation } = this.props;
|
||||
|
||||
if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) {
|
||||
return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
multiple,
|
||||
|
@ -18,6 +18,8 @@ import AclProxy from './acl_proxy';
|
||||
|
||||
import EventActions from '../actions/event_actions';
|
||||
|
||||
import PieceListStore from '../stores/piece_list_store';
|
||||
|
||||
import UserActions from '../actions/user_actions';
|
||||
import UserStore from '../stores/user_store';
|
||||
|
||||
@ -43,12 +45,17 @@ let Header = React.createClass({
|
||||
|
||||
getInitialState() {
|
||||
return mergeOptions(
|
||||
PieceListStore.getState(),
|
||||
WhitelabelStore.getState(),
|
||||
UserStore.getState()
|
||||
);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
// Listen to the piece list store, but don't fetch immediately to avoid
|
||||
// conflicts with routes that may need to wait to load the piece list
|
||||
PieceListStore.listen(this.onChange);
|
||||
|
||||
UserStore.listen(this.onChange);
|
||||
UserActions.fetchCurrentUser.defer();
|
||||
|
||||
@ -75,11 +82,16 @@ let Header = React.createClass({
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
PieceListStore.unlisten(this.onChange);
|
||||
UserStore.unlisten(this.onChange);
|
||||
WhitelabelStore.unlisten(this.onChange);
|
||||
//history.unlisten(this.onRouteChange);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
getLogo() {
|
||||
let { whitelabel } = this.state;
|
||||
|
||||
@ -93,16 +105,16 @@ let Header = React.createClass({
|
||||
<img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span>
|
||||
<Link className="icon-ascribe-logo" to="/collection"/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Link className="icon-ascribe-logo" to="/collection"/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
getPoweredBy(){
|
||||
getPoweredBy() {
|
||||
return (
|
||||
<AclProxy
|
||||
aclObject={this.state.whitelabel}
|
||||
@ -117,10 +129,6 @@ let Header = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
onChange(state) {
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
onMenuItemClick() {
|
||||
/*
|
||||
This is a hack to make the dropdown close after clicking on an item
|
||||
@ -156,16 +164,18 @@ let Header = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
const { currentUser, unfilteredPieceListCount } = this.state;
|
||||
let account;
|
||||
let signup;
|
||||
let navRoutesLinks;
|
||||
if (this.state.currentUser.username){
|
||||
|
||||
if (currentUser.username) {
|
||||
account = (
|
||||
<DropdownButton
|
||||
ref='dropdownbutton'
|
||||
id="nav-route-user-dropdown"
|
||||
eventKey="1"
|
||||
title={this.state.currentUser.username}>
|
||||
title={currentUser.username}>
|
||||
<LinkContainer
|
||||
to="/settings"
|
||||
onClick={this.onMenuItemClick}>
|
||||
@ -175,7 +185,7 @@ let Header = React.createClass({
|
||||
</MenuItem>
|
||||
</LinkContainer>
|
||||
<AclProxy
|
||||
aclObject={this.state.currentUser.acl}
|
||||
aclObject={currentUser.acl}
|
||||
aclName="acl_view_settings_contract">
|
||||
<LinkContainer
|
||||
to="/contract_settings"
|
||||
@ -196,9 +206,21 @@ let Header = React.createClass({
|
||||
</LinkContainer>
|
||||
</DropdownButton>
|
||||
);
|
||||
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
|
||||
}
|
||||
else {
|
||||
|
||||
// Let's assume that if the piece list hasn't loaded yet (ie. when unfilteredPieceListCount === -1)
|
||||
// then the user has pieces
|
||||
// FIXME: this doesn't work that well as the user may not load their piece list
|
||||
// until much later, so we would show the 'Collection' header as available until
|
||||
// they actually click on it and get redirected to piece registration.
|
||||
navRoutesLinks = (
|
||||
<NavRoutesLinks
|
||||
navbar
|
||||
right
|
||||
hasPieces={!!unfilteredPieceListCount}
|
||||
routes={this.props.routes}
|
||||
userAcl={currentUser.acl} />
|
||||
);
|
||||
} else {
|
||||
account = (
|
||||
<LinkContainer
|
||||
to="/login">
|
||||
|
@ -11,14 +11,33 @@ import AclProxy from './acl_proxy';
|
||||
import { sanitizeList } from '../utils/general_utils';
|
||||
|
||||
|
||||
const DISABLE_ENUM = ['hasPieces', 'noPieces'];
|
||||
|
||||
let NavRoutesLinks = React.createClass({
|
||||
propTypes: {
|
||||
hasPieces: React.PropTypes.bool,
|
||||
routes: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
userAcl: React.PropTypes.object
|
||||
},
|
||||
|
||||
isRouteDisabled(disableOn) {
|
||||
const { hasPieces } = this.props;
|
||||
|
||||
if (disableOn) {
|
||||
if (!DISABLE_ENUM.includes(disableOn)) {
|
||||
throw new Error(`"disableOn" must be one of: [${DISABLE_ENUM.join(', ')}] got "${disableOn}" instead`);
|
||||
}
|
||||
|
||||
if (disableOn === 'hasPieces') {
|
||||
return hasPieces;
|
||||
} else if (disableOn === 'noPieces') {
|
||||
return !hasPieces;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method generales a bunch of react-bootstrap specific links
|
||||
* This method generates a bunch of react-bootstrap specific links
|
||||
* from the routes we defined in one of the specific routes.js file
|
||||
*
|
||||
* We can define a headerTitle as well as a aclName and according to that the
|
||||
@ -29,48 +48,50 @@ let NavRoutesLinks = React.createClass({
|
||||
* @return {Array} Array of ReactElements that can be displayed to the user
|
||||
*/
|
||||
extractLinksFromRoutes(node, userAcl, i) {
|
||||
if(!node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
let links = node.childRoutes.map((child, j) => {
|
||||
let childrenFn = null;
|
||||
let { aclName, headerTitle, path, childRoutes } = child;
|
||||
|
||||
// If the node has children that could be rendered, then we want
|
||||
// to execute this function again with the child as the root
|
||||
//
|
||||
// Otherwise we'll just pass childrenFn as false
|
||||
if(child.childRoutes && child.childRoutes.length > 0) {
|
||||
childrenFn = this.extractLinksFromRoutes(child, userAcl, i++);
|
||||
}
|
||||
const links = node.childRoutes.map((child, j) => {
|
||||
const { aclName, disableOn, headerTitle, path, childRoutes } = child;
|
||||
|
||||
// We validate if the user has set the title correctly,
|
||||
// otherwise we're not going to render his route
|
||||
if(headerTitle && typeof headerTitle === 'string') {
|
||||
if (headerTitle && typeof headerTitle === 'string') {
|
||||
let nestedChildren = null;
|
||||
|
||||
// If the node has children that could be rendered, then we want
|
||||
// to execute this function again with the child as the root
|
||||
//
|
||||
// Otherwise we'll just pass nestedChildren as false
|
||||
if (child.childRoutes && child.childRoutes.length) {
|
||||
nestedChildren = this.extractLinksFromRoutes(child, userAcl, i++);
|
||||
}
|
||||
|
||||
const navLinkProps = {
|
||||
headerTitle,
|
||||
children: nestedChildren,
|
||||
depth: i,
|
||||
disabled: this.isRouteDisabled(disableOn),
|
||||
routePath: `/${path}`
|
||||
};
|
||||
|
||||
// if there is an aclName present on the route definition,
|
||||
// we evaluate it against the user's acl
|
||||
if(aclName && typeof aclName !== 'undefined') {
|
||||
if (aclName && typeof aclName !== 'undefined') {
|
||||
return (
|
||||
<AclProxy
|
||||
key={j}
|
||||
aclName={aclName}
|
||||
aclObject={this.props.userAcl}>
|
||||
<NavRoutesLinksLink
|
||||
headerTitle={headerTitle}
|
||||
routePath={'/' + path}
|
||||
depth={i}
|
||||
children={childrenFn}/>
|
||||
<NavRoutesLinksLink {...navLinkProps} />
|
||||
</AclProxy>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NavRoutesLinksLink
|
||||
key={j}
|
||||
headerTitle={headerTitle}
|
||||
routePath={'/' + path}
|
||||
depth={i}
|
||||
children={childrenFn}/>
|
||||
{...navLinkProps} />
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -84,7 +105,7 @@ let NavRoutesLinks = React.createClass({
|
||||
},
|
||||
|
||||
render() {
|
||||
let {routes, userAcl} = this.props;
|
||||
const {routes, userAcl} = this.props;
|
||||
|
||||
return (
|
||||
<Nav {...this.props}>
|
||||
|
@ -11,42 +11,46 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
|
||||
|
||||
let NavRoutesLinksLink = React.createClass({
|
||||
propTypes: {
|
||||
headerTitle: React.PropTypes.string,
|
||||
routePath: React.PropTypes.string,
|
||||
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
]),
|
||||
|
||||
depth: React.PropTypes.number
|
||||
disabled: React.PropTypes.bool,
|
||||
depth: React.PropTypes.number,
|
||||
headerTitle: React.PropTypes.string,
|
||||
routePath: React.PropTypes.string
|
||||
},
|
||||
|
||||
render() {
|
||||
let { children, headerTitle, depth, routePath } = this.props;
|
||||
const { children, headerTitle, depth, disabled, routePath } = this.props;
|
||||
|
||||
// if the route has children, we're returning a DropdownButton that will get filled
|
||||
// with MenuItems
|
||||
if(children) {
|
||||
if (children) {
|
||||
return (
|
||||
<DropdownButton
|
||||
disabled={disabled}
|
||||
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
|
||||
title={headerTitle}>
|
||||
{children}
|
||||
</DropdownButton>
|
||||
);
|
||||
} else {
|
||||
if(depth === 1) {
|
||||
if (depth === 1) {
|
||||
// if the node's child is actually a node of level one (a child of a node), we're
|
||||
// returning a DropdownButton matching MenuItem
|
||||
return (
|
||||
<LinkContainer to={routePath}>
|
||||
<LinkContainer
|
||||
disabled={disabled}
|
||||
to={routePath}>
|
||||
<MenuItem>{headerTitle}</MenuItem>
|
||||
</LinkContainer>
|
||||
);
|
||||
} else if(depth === 0) {
|
||||
} else if (depth === 0) {
|
||||
return (
|
||||
<LinkContainer to={routePath}>
|
||||
<LinkContainer
|
||||
disabled={disabled}
|
||||
to={routePath}>
|
||||
<NavItem>{headerTitle}</NavItem>
|
||||
</LinkContainer>
|
||||
);
|
||||
|
@ -53,7 +53,6 @@ let PieceList = React.createClass({
|
||||
accordionListItemType: AccordionListItemWallet,
|
||||
bulkModalButtonListType: AclButtonList,
|
||||
canLoadPieceList: true,
|
||||
orderParams: ['artist_name', 'title'],
|
||||
filterParams: [{
|
||||
label: getLangText('Show works I can'),
|
||||
items: [
|
||||
@ -61,7 +60,10 @@ let PieceList = React.createClass({
|
||||
'acl_consign',
|
||||
'acl_create_editions'
|
||||
]
|
||||
}]
|
||||
}],
|
||||
orderParams: ['artist_name', 'title'],
|
||||
redirectTo: '/register_piece',
|
||||
shouldRedirect: () => true
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -3,19 +3,21 @@
|
||||
import React from 'react';
|
||||
import { History } from 'react-router';
|
||||
|
||||
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
|
||||
|
||||
import Form from '../../../../../ascribe_forms/form';
|
||||
import Property from '../../../../../ascribe_forms/property';
|
||||
import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable';
|
||||
|
||||
import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
import InputFineuploader from '../../../../../ascribe_forms/input_fineuploader';
|
||||
import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||
|
||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
||||
|
||||
import GlobalNotificationModel from '../../../../../../models/global_notification_model';
|
||||
import GlobalNotificationActions from '../../../../../../actions/global_notification_actions';
|
||||
|
||||
import AppConstants from '../../../../../../constants/application_constants';
|
||||
import ApiUrls from '../../../../../../constants/api_urls';
|
||||
import AppConstants from '../../../../../../constants/application_constants';
|
||||
import { validationParts, validationTypes } from '../../../../../../constants/uploader_constants';
|
||||
|
||||
import requests from '../../../../../../utils/requests';
|
||||
|
||||
@ -193,7 +195,7 @@ const PRRegisterPieceForm = React.createClass({
|
||||
|
||||
render() {
|
||||
const { location } = this.props;
|
||||
const maxThumbnailSize = AppConstants.fineUploader.validation.workThumbnail.sizeLimit / 1000000;
|
||||
const maxThumbnailSize = validationTypes.workThumbnail.sizeLimit / 1000000;
|
||||
|
||||
return (
|
||||
<div className="register-piece--form">
|
||||
@ -305,8 +307,8 @@ const PRRegisterPieceForm = React.createClass({
|
||||
fileClass: 'digitalwork'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
itemLimit: validationTypes.registerWork.itemLimit,
|
||||
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||
allowedExtensions: ['pdf']
|
||||
}}
|
||||
location={location}
|
||||
@ -318,7 +320,7 @@ const PRRegisterPieceForm = React.createClass({
|
||||
</Property>
|
||||
<Property
|
||||
name="thumbnailKey"
|
||||
label={`${getLangText('Featured Cover photo')} max ${maxThumbnailSize}MB`}>
|
||||
label={`${getLangText('Featured Cover photo')} (max ${maxThumbnailSize}MB)`}>
|
||||
<InputFineuploader
|
||||
fileInputElement={UploadButton()}
|
||||
createBlobRoutine={{
|
||||
@ -331,9 +333,9 @@ const PRRegisterPieceForm = React.createClass({
|
||||
fileClass: 'thumbnail'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit,
|
||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
||||
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||
allowedExtensions: validationParts.allowedExtensions.images
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
@ -356,8 +358,8 @@ const PRRegisterPieceForm = React.createClass({
|
||||
fileClass: 'otherdata'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit
|
||||
itemLimit: validationParts.itemLimit.single,
|
||||
sizeLimit: validationTypes.additionalData.sizeLimit
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
@ -378,9 +380,9 @@ const PRRegisterPieceForm = React.createClass({
|
||||
fileClass: 'otherdata'
|
||||
}}
|
||||
validation={{
|
||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
||||
itemLimit: validationParts.itemLimit.single,
|
||||
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||
allowedExtensions: validationParts.allowedExtensions.images
|
||||
}}
|
||||
location={location}
|
||||
fileClassToUpload={{
|
||||
|
@ -75,7 +75,6 @@ let PrizePieceList = React.createClass({
|
||||
<div>
|
||||
<PieceList
|
||||
ref="list"
|
||||
redirectTo="/register_piece"
|
||||
accordionListItemType={AccordionListItemPrize}
|
||||
orderParams={orderParams}
|
||||
orderBy={this.state.currentUser.is_jury ? 'rating' : null}
|
||||
|
@ -11,14 +11,14 @@ import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_t
|
||||
import Form from '../../../../../ascribe_forms/form';
|
||||
import Property from '../../../../../ascribe_forms/property';
|
||||
|
||||
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||
|
||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
||||
|
||||
import ApiUrls from '../../../../../../constants/api_urls';
|
||||
import AppConstants from '../../../../../../constants/application_constants';
|
||||
import { validationParts, validationTypes } from '../../../../../../constants/uploader_constants';
|
||||
|
||||
import requests from '../../../../../../utils/requests';
|
||||
|
||||
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||
import { mergeOptions } from '../../../../../../utils/general_utils';
|
||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||
|
||||
@ -170,7 +170,12 @@ let MarketAdditionalDataForm = React.createClass({
|
||||
otherData={otherData}
|
||||
pieceId={pieceId}
|
||||
setIsUploadReady={this.setIsUploadReady}
|
||||
submitFile={function () {}} />
|
||||
submitFile={function () {}}
|
||||
validation={{
|
||||
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||
allowedExtensions: validationParts.allowedExtensions.images
|
||||
}} />
|
||||
<Property
|
||||
name='artist_bio'
|
||||
label={getLangText('Artist Bio')}
|
||||
|
@ -75,7 +75,8 @@ let ROUTES = {
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(CylandPieceList)}
|
||||
headerTitle='COLLECTION' />
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route path='editions/:editionId' component={EditionContainer} />
|
||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||
<Route path='pieces/:pieceId' component={CylandPieceContainer} />
|
||||
@ -109,7 +110,8 @@ let ROUTES = {
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
||||
headerTitle='COLLECTION' />
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route path='pieces/:pieceId' component={PieceContainer} />
|
||||
<Route path='editions/:editionId' component={EditionContainer} />
|
||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||
@ -150,7 +152,8 @@ let ROUTES = {
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvPieceList)}
|
||||
headerTitle='COLLECTION' />
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route
|
||||
path='contract_notifications'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} />
|
||||
@ -189,7 +192,8 @@ let ROUTES = {
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(MarketPieceList)}
|
||||
headerTitle='COLLECTION' />
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
||||
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||
@ -225,7 +229,8 @@ let ROUTES = {
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(Vivi23PieceList)}
|
||||
headerTitle='COLLECTION' />
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
||||
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||
|
@ -68,23 +68,6 @@ const constants = {
|
||||
// Source: http://www.w3schools.com/tags/att_input_type.asp
|
||||
'possibleInputTypes': ['button', 'checkbox', 'color', 'date', 'datetime', 'datetime-local', 'email', 'file', 'hidden', 'image', 'month', 'number', 'password', 'radio', 'range', 'reset', 'search', 'submit', 'tel', 'text', 'time', 'url', 'week'],
|
||||
|
||||
'fineUploader': {
|
||||
'validation': {
|
||||
'additionalData': {
|
||||
'itemLimit': 100,
|
||||
'sizeLimit': '25000000000'
|
||||
},
|
||||
'registerWork': {
|
||||
'itemLimit': 1,
|
||||
'sizeLimit': '25000000000'
|
||||
},
|
||||
'workThumbnail': {
|
||||
'itemLimit': 1,
|
||||
'sizeLimit': '5000000'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA',
|
||||
'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART',
|
||||
'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV',
|
||||
|
32
js/constants/uploader_constants.js
Normal file
32
js/constants/uploader_constants.js
Normal file
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
export const validationParts = {
|
||||
allowedExtensions: {
|
||||
images: ['png', 'jpg', 'jpeg', 'gif']
|
||||
},
|
||||
itemLimit: {
|
||||
single: 1,
|
||||
multiple: 100
|
||||
},
|
||||
sizeLimit: {
|
||||
default: 50000000000,
|
||||
thumbnail: 5000000
|
||||
}
|
||||
};
|
||||
|
||||
const { allowedExtensions, itemLimit, sizeLimit } = validationParts;
|
||||
|
||||
export const validationTypes = {
|
||||
additionalData: {
|
||||
itemLimit: itemLimit.multiple,
|
||||
sizeLimit: sizeLimit.default
|
||||
},
|
||||
registerWork: {
|
||||
itemLimit: itemLimit.single,
|
||||
sizeLimit: sizeLimit.default
|
||||
},
|
||||
workThumbnail: {
|
||||
itemLimit: itemLimit.single,
|
||||
sizeLimit: sizeLimit.thumbnail
|
||||
}
|
||||
};
|
@ -40,7 +40,8 @@ const COMMON_ROUTES = (
|
||||
<Route
|
||||
path='collection'
|
||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
||||
headerTitle='COLLECTION'/>
|
||||
headerTitle='COLLECTION'
|
||||
disableOn='noPieces' />
|
||||
<Route
|
||||
path='signup'
|
||||
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} />
|
||||
|
16
package.json
16
package.json
@ -8,10 +8,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint ./js",
|
||||
"preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true",
|
||||
"postinstall": "npm run build",
|
||||
"build": "gulp build --production",
|
||||
"start": "node server.js",
|
||||
|
||||
"test": "mocha",
|
||||
"tunnel": "node test/tunnel.js"
|
||||
|
||||
"vi-clean": "rm -rf gemini-report",
|
||||
"vi-phantom": "phantomjs --webdriver=4444",
|
||||
"vi-update": "gemini update",
|
||||
@ -48,10 +52,16 @@
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^3.1.11",
|
||||
"babel-jest": "^5.2.0",
|
||||
"chai": "^3.4.1",
|
||||
"chai-as-promised": "^5.1.0",
|
||||
"colors": "^1.1.2",
|
||||
"dotenv": "^1.2.0",
|
||||
"gemini": "^2.1.0",
|
||||
"gulp-sass": "^2.1.1",
|
||||
"jest-cli": "^0.4.0",
|
||||
"phantomjs2": "^2.0.2"
|
||||
"mocha": "^2.3.4",
|
||||
"phantomjs2": "^2.0.2",
|
||||
"sauce-connect-launcher": "^0.13.0",
|
||||
"wd": "^0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"alt": "^0.16.5",
|
||||
@ -77,7 +87,7 @@
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-minify-css": "^1.1.6",
|
||||
"gulp-notify": "^2.2.0",
|
||||
"gulp-sass": "^2.0.1",
|
||||
"gulp-sass": "^2.1.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-template": "~3.0.0",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
|
@ -7,23 +7,25 @@
|
||||
position: fixed;
|
||||
transition: .2s bottom cubic-bezier(.77, 0, .175, 1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ascribe-global-notification-off {
|
||||
bottom: -3.5em;
|
||||
}
|
||||
z-index: 9999;
|
||||
|
||||
.ascribe-global-notification-on {
|
||||
bottom: 0;
|
||||
}
|
||||
> div {
|
||||
padding: .5em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ascribe-global-notification > div,
|
||||
.ascribe-global-notification-bubble > div {
|
||||
display: table-cell;
|
||||
font-size: 1.25em;
|
||||
padding-right: 3em;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
&.ascribe-global-notification-off {
|
||||
bottom: -5em;
|
||||
}
|
||||
|
||||
&.ascribe-global-notification-on {
|
||||
bottom: 0;
|
||||
|
||||
// React can sometimes take some time to load the notification message,
|
||||
// so add a small delay before showing the notification
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.ascribe-global-notification-bubble {
|
||||
@ -33,21 +35,35 @@
|
||||
color: white;
|
||||
display: table;
|
||||
height: 3.5em;
|
||||
max-width: 75%;
|
||||
position: fixed;
|
||||
right: -50em;
|
||||
transition: 1s right ease;
|
||||
transition: 0.5s right ease;
|
||||
|
||||
z-index: 9999;
|
||||
|
||||
> div {
|
||||
padding: .75em 1.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.ascribe-global-notification-bubble-off {
|
||||
right: -100em;
|
||||
}
|
||||
|
||||
&.ascribe-global-notification-bubble-on {
|
||||
right: 3.5em;
|
||||
|
||||
// React can sometimes take some time to load the notification message,
|
||||
// so add a small delay before showing the notification
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.ascribe-global-notification-bubble-off {
|
||||
right: -100em;
|
||||
}
|
||||
|
||||
.ascribe-global-notification-bubble-on {
|
||||
right: 3.5em;
|
||||
.ascribe-global-notification > div,
|
||||
.ascribe-global-notification-bubble > div {
|
||||
display: table-cell;
|
||||
font-size: 1.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ascribe-global-notification-danger {
|
||||
|
@ -9,6 +9,12 @@
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding-top: 1.5em;
|
||||
|
||||
@media screen and (max-width: 625px) {
|
||||
.file-name {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inactive-dropzone {
|
||||
@ -78,12 +84,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-drag-and-drop-preview-table-wrapper {
|
||||
display: table;
|
||||
height: 94px;
|
||||
width: 104px;
|
||||
}
|
||||
|
||||
.file-drag-and-drop-preview {
|
||||
background-color: #eeeeee;
|
||||
border: 1px solid #616161;
|
||||
@ -131,7 +131,7 @@
|
||||
width: 104px;
|
||||
|
||||
// REFACTOR TO USE TABLE CELL
|
||||
.action-file, .spinner-file, .icon-ascribe-ok {
|
||||
.action-file, .spinner-file {
|
||||
margin-top: 1em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@ -142,23 +142,21 @@
|
||||
}
|
||||
|
||||
.file-drag-and-drop-preview-other {
|
||||
display: table-cell;
|
||||
height: 94px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
width: 104px;
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-file:not(.icon-ascribe-ok), .spinner-file {
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
top: .3em;
|
||||
|
||||
span:not(:first-child) {
|
||||
display: block;
|
||||
margin-top: .5em;
|
||||
}
|
||||
top: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
|
36
test/.eslintrc
Normal file
36
test/.eslintrc
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"new-cap": [2, {newIsCap: true, capIsNew: false}],
|
||||
"quotes": [2, "single"],
|
||||
"eol-last": [0],
|
||||
"no-mixed-requires": [0],
|
||||
"no-underscore-dangle": [0],
|
||||
"global-strict": [2, "always"],
|
||||
"no-trailing-spaces": [2, { skipBlankLines: true }],
|
||||
"no-console": 0,
|
||||
"camelcase": [2, {"properties": "never"}],
|
||||
},
|
||||
"globals": {},
|
||||
"plugins": [],
|
||||
"ecmaFeatures": {
|
||||
"modules": 1,
|
||||
"arrowFunctions",
|
||||
"classes": 1,
|
||||
"blockBindings": 1,
|
||||
"defaultParams": 1,
|
||||
"destructuring": 1,
|
||||
"objectLiteralComputedProperties": 1,
|
||||
"objectLiteralDuplicateProperties": 0,
|
||||
"objectLiteralShorthandMethods": 1,
|
||||
"objectLiteralShorthandProperties": 1,
|
||||
"restParams": 1,
|
||||
"spread": 1,
|
||||
"superInFunctions": 1,
|
||||
"templateStrings": 1
|
||||
}
|
||||
}
|
256
test/README.md
Normal file
256
test/README.md
Normal file
@ -0,0 +1,256 @@
|
||||
# TL;DR
|
||||
Copy the contents of `.env-template` to `.env` and [fill up the missing keys with
|
||||
information from your SauceLabs account](#how-to-set-up-your-env-config-file).
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
$ npm run tunnel
|
||||
$ npm test && git commit
|
||||
```
|
||||
|
||||
|
||||
# TODO
|
||||
* Use gulp to parallelize mocha invocations with different browsers
|
||||
* Figure out good system for changing subdomain through test scripts
|
||||
|
||||
|
||||
# Welcome to our test suite, let me be your guide
|
||||
|
||||
Dear reader, first of all thanks for taking your time reading this document.
|
||||
The purpose of this document is to give you an overview on what we want to test
|
||||
and how we are doing it.
|
||||
|
||||
|
||||
# How it works (bird's-eye view)
|
||||
|
||||
You will notice that the setup is a bit convoluted. This section will explain
|
||||
you why. Testing single functions in JavaScript is not that hard (if you don't
|
||||
need to interact with the DOM), and can be easily achieved using frameworks
|
||||
like [Mocha](https://mochajs.org/). Integration and cross browser testing is,
|
||||
on the other side, a huge PITA. Moreover, "browser testing" includes also
|
||||
"mobile browser testing". On the top of that the same browser (type and
|
||||
version) can behave in a different way on different operating systems.
|
||||
|
||||
To achieve that you can have your own cluster of machines with different
|
||||
operating systems and browsers or, if you don't want to spend the rest of your
|
||||
life configuring an average of 100 browsers for each different operating
|
||||
system, you can pay someone else to do that. Check out [this
|
||||
article](https://saucelabs.com/selenium/selenium-grid) if you want to know why
|
||||
using Selenium Grid is better than a DIY approach.
|
||||
|
||||
We decided to use [saucelabs](https://saucelabs.com/) cloud (they support [over
|
||||
700 combinations](https://saucelabs.com/platforms/) of operating systems and
|
||||
browsers) to run our tests.
|
||||
|
||||
|
||||
## Components and tools
|
||||
|
||||
Right now we are just running the test locally, so no Continuous Integration™.
|
||||
|
||||
The components involved are:
|
||||
- **[Selenium WebDriver](https://www.npmjs.com/package/wd)**: it's a library
|
||||
that can control a browser. You can use the **WebDriver** to load new URLs,
|
||||
click around, fill out forms, submit forms etc. It's basically a way to
|
||||
control remotely a browser. The protocol (language agnostic) is called
|
||||
[JsonWire](https://code.google.com/p/selenium/wiki/JsonWireProtocol), `wd`
|
||||
wraps it and gives you a nice
|
||||
[API](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md)
|
||||
you can use in JavaScript. There are other implementations in Python, PHP,
|
||||
Java, etc. Also, a **WebDriver** can be initialized with a list of [desired
|
||||
capabilities](https://code.google.com/p/selenium/wiki/DesiredCapabilities)
|
||||
describing which features (like the platform, browser name and version) you
|
||||
want to use to run your tests.
|
||||
|
||||
- **[Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2)**: it's
|
||||
the controller for the cluster of machines/devices that can run browsers.
|
||||
Selenium Grid is able to scale by distributing tests on several machines,
|
||||
manage multiple environments from a central point, making it easy to run the
|
||||
tests against a vast combination of browsers / OS, minimize the maintenance
|
||||
time for the grid by allowing you to implement custom hooks to leverage
|
||||
virtual infrastructure for instance.
|
||||
|
||||
- **[Saucelabs](https://saucelabs.com/)**: a private company providing a
|
||||
cluster to run your tests on over 700 combinations of browsers/operating
|
||||
systems. (They do other things, check out their websites).
|
||||
|
||||
- **[SauceConnect](https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect)**:
|
||||
is a Java software by Saucelabs to connect to your `localhost` to test the
|
||||
application. There is also a Node.js wrapper
|
||||
[sauce-connect-launcher](https://www.npmjs.com/package/sauce-connect-launcher),
|
||||
so you can use it programmatically within your code for tests. Please note
|
||||
that this module is just a wrapper around the actual software. Running `npm
|
||||
install` should install the additional Java software as well.
|
||||
|
||||
|
||||
On the JavaScript side, we use:
|
||||
- [Mocha](https://mochajs.org/): a test framework running on Node.js.
|
||||
|
||||
- [chai](http://chaijs.com/): a BDD/TDD assertion library for node that can be
|
||||
paired with any javascript testing framework.
|
||||
|
||||
- [chaiAsPromised](https://github.com/domenic/chai-as-promised/): an extension
|
||||
for Chai with a fluent language for asserting facts about promises. The
|
||||
extension is actually quite cool, we can do assertions on promises without
|
||||
writing callbacks but just chaining operators. Check out their `README` on
|
||||
GitHub to see an example.
|
||||
|
||||
- [dotenv](https://github.com/motdotla/dotenv): a super nice package to load
|
||||
environment variables from `.env` into `process.env`.
|
||||
|
||||
|
||||
## How to set up your `.env` config file
|
||||
In the root of this repository there is a file called `.env-template`. Create a
|
||||
copy and call it `.env`. This file will store some values we need to connect to
|
||||
Saucelabs.
|
||||
|
||||
There are two values to be set:
|
||||
- `SAUCE_ACCESS_KEY`
|
||||
- `SAUCE_USERNAME`
|
||||
|
||||
The two keys are the [default
|
||||
ones](https://github.com/admc/wd#environment-variables-for-saucelabs) used by
|
||||
many products related to Saucelabs. This allow us to keep the configuration
|
||||
fairly straightforward and simple.
|
||||
|
||||
After logging in to https://saucelabs.com/, you can find your **api key** under
|
||||
the **My Account**. Copy paste the value in your `.env` file.
|
||||
|
||||
|
||||
## Anatomy of a test
|
||||
|
||||
First, you need to learn how [Mocha](https://mochajs.org/) works. Brew a coffee
|
||||
(or tea, if coffee is not your cup of tea), sit down and read the docs.
|
||||
|
||||
Done? Great, let's move on and analyze how a test is written.
|
||||
|
||||
From a very high level, the flow of a test is the following:
|
||||
1. load a page with a specific URL
|
||||
2. do something on the page (click a button, submit a form, etc.)
|
||||
3. maybe wait some seconds, or wait if something has changed
|
||||
4. check if the new page contains some text you expect to be there
|
||||
|
||||
This is not set in stone, so go crazy if you want. But keep in mind that we
|
||||
have a one page application, there might be some gotchas on how to wait for
|
||||
stuff to happen. I suggest you to read the section [Wait for
|
||||
something](https://github.com/admc/wd#waiting-for-something) to understand
|
||||
better which tools you have to solve this problem.
|
||||
Again, take a look to the [`wd` implementation of the JsonWire
|
||||
protocol](https://github.com/admc/wd/blob/master/doc/jsonwire-full-mapping.md)
|
||||
to know all the methods you can use to control the browser.
|
||||
|
||||
|
||||
Import the libraries we need.
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
require('dotenv').load();
|
||||
|
||||
const wd = require('wd');
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
```
|
||||
|
||||
|
||||
Set up `chai` to use `chaiAsPromised`.
|
||||
|
||||
```javascript
|
||||
chai.use(chaiAsPromised);
|
||||
chai.should();
|
||||
```
|
||||
|
||||
`browser` is the main object to interact with Saucelab "real" browsers. We will
|
||||
use this object a lot. It allows us to load pages, click around, check if a
|
||||
specific text is present, etc.
|
||||
|
||||
```javascript
|
||||
describe('Login logs users in', function() {
|
||||
let browser;
|
||||
```
|
||||
|
||||
Create the driver to control the browser. `before` will be executed once at the
|
||||
start of the test before any `it` functions. Use `beforeEach` instead if you'd
|
||||
like to run some code before each `it` function.
|
||||
|
||||
```javascript
|
||||
before(function() {
|
||||
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
|
||||
|
||||
// Start the browser, go to /login, and wait for the react app to render
|
||||
return browser
|
||||
.init({ browserName, version, platform })
|
||||
.get(config.APP_URL + '/login')
|
||||
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000);
|
||||
});
|
||||
```
|
||||
|
||||
Close the browser after finishing all tests. `after` will be executed at the end
|
||||
of all `it` functions. Use `afterEach` instead if you'd like to run some code
|
||||
after each `it` function.
|
||||
|
||||
```javascript
|
||||
after(function() {
|
||||
// Destroys the browser session
|
||||
return browser.quit();
|
||||
});
|
||||
```
|
||||
|
||||
The actual test. We query the `browser` object to get the title of the page.
|
||||
Note that `.title()` returns a `promise` **but**, since we are using
|
||||
`chaiAsPromised`, we have some syntactic sugar to handle the promise in line,
|
||||
without writing new functions.
|
||||
|
||||
```javascript
|
||||
it('should contain "Log in" in the title', function() {
|
||||
return browser.title().should.become('Log in');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
All together:
|
||||
|
||||
```javascript
|
||||
function testSuite(browserName, version, platform) {
|
||||
describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() {
|
||||
// Set timeout to zero so Mocha won't time out.
|
||||
this.timeout(0);
|
||||
let browser;
|
||||
|
||||
before(function() {
|
||||
// No need to inject `username` or `access_key`, by default the constructor
|
||||
// looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY`
|
||||
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
|
||||
|
||||
// Start the browser, go to /login, and wait for the react app to render
|
||||
return browser
|
||||
.init({ browserName, version, platform })
|
||||
.get(config.APP_URL + '/login')
|
||||
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
return browser.quit();
|
||||
});
|
||||
|
||||
it('should contain "Log in" in the title', function() {
|
||||
return browser.title().should.become('Log in');
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## How to run the test suite
|
||||
To run the tests, type:
|
||||
```bash
|
||||
$ mocha
|
||||
```
|
||||
|
||||
By default the test suite runs on `http://www.localhost.com:3000/`, if you
|
||||
want to change the URL, change the `APP_URL` env variable.
|
||||
|
||||
|
||||
# How to have fun
|
||||
Try this!
|
||||
```bash
|
||||
$ mocha -R nyan
|
||||
```
|
18
test/config.js
Normal file
18
test/config.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
require('dotenv').load();
|
||||
|
||||
|
||||
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
|
||||
const BROWSERS = [
|
||||
'chrome,47,WINDOWS',
|
||||
'chrome,46,WINDOWS',
|
||||
'firefox,43,MAC',
|
||||
'internet explorer,10,VISTA'
|
||||
];
|
||||
|
||||
|
||||
module.exports = {
|
||||
BROWSERS: BROWSERS.map(x => x.split(',')),
|
||||
APP_URL: process.env.SAUCE_DEFAULT_URL || 'http://www.localhost.com:3000'
|
||||
};
|
50
test/setup.js
Normal file
50
test/setup.js
Normal file
@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const colors = require('colors');
|
||||
const sauceConnectLauncher = require('sauce-connect-launcher');
|
||||
|
||||
|
||||
let globalSauceProcess;
|
||||
|
||||
if (!process.env.SAUCE_USERNAME) {
|
||||
console.log(colors.red('SAUCE_USERNAME is missing. Please check the README.md file.'));
|
||||
process.exit(1); //eslint-disable-line no-process-exit
|
||||
}
|
||||
|
||||
if (!process.env.SAUCE_ACCESS_KEY) {
|
||||
console.log(colors.red('SAUCE_ACCESS_KEY is missing. Please check the README.md file.'));
|
||||
process.exit(1); //eslint-disable-line no-process-exit
|
||||
}
|
||||
|
||||
|
||||
if (process.env.SAUCE_AUTO_CONNECT) {
|
||||
before(function(done) {
|
||||
console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.'));
|
||||
// Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts.
|
||||
this.timeout(0);
|
||||
|
||||
sauceConnectLauncher(function (err, sauceConnectProcess) {
|
||||
if (err) {
|
||||
console.error(err.message);
|
||||
return;
|
||||
}
|
||||
globalSauceProcess = sauceConnectProcess;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
after(function (done) {
|
||||
// Creating the tunnel takes a bit of time. For this case we can safely disable it.
|
||||
this.timeout(0);
|
||||
|
||||
if (globalSauceProcess) {
|
||||
globalSauceProcess.close(done);
|
||||
}
|
||||
});
|
||||
} else if (config.APP_URL.match(/localhost/)) {
|
||||
console.log(colors.yellow(`You are running tests on ${config.APP_URL}, make sure you already have a tunnel running.`));
|
||||
console.log(colors.yellow('To create the tunnel, run:'));
|
||||
console.log(colors.yellow(' $ node test/tunnel.js'));
|
||||
}
|
50
test/test-login.js
Normal file
50
test/test-login.js
Normal file
@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const Q = require('q');
|
||||
const wd = require('wd');
|
||||
const asserters = wd.asserters; // Commonly used asserters for async waits in the browser
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
const config = require('./config.js');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
chai.should();
|
||||
|
||||
|
||||
function testSuite(browserName, version, platform) {
|
||||
describe(`[${browserName} ${version} ${platform}] Login logs users in`, function() {
|
||||
// Set timeout to zero so Mocha won't time out.
|
||||
this.timeout(0);
|
||||
let browser;
|
||||
|
||||
before(function() {
|
||||
// No need to inject `username` or `access_key`, by default the constructor
|
||||
// looks up the values in `process.env.SAUCE_USERNAME` and `process.env.SAUCE_ACCESS_KEY`
|
||||
browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80);
|
||||
|
||||
// Start the browser, go to /login, and wait for the react app to render
|
||||
return browser
|
||||
.configureHttp({ baseUrl: config.APP_URL })
|
||||
.init({ browserName, version, platform })
|
||||
.get('/login')
|
||||
.waitForElementByCss('.ascribe-default-app', asserters.isDisplayed, 10000)
|
||||
.catch(function (err) {
|
||||
console.log('Failure -- unable to load app.');
|
||||
console.log('Skipping tests for this browser...');
|
||||
return Q.reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
after(function() {
|
||||
return browser.quit();
|
||||
});
|
||||
|
||||
it('should contain "Log in" in the title', function() {
|
||||
return browser.
|
||||
waitForElementByCss('.ascribe-login-wrapper', asserters.isDisplayed, 2000)
|
||||
title().should.become('Log in');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
config.BROWSERS.map(x => testSuite(...x));
|
23
test/tunnel.js
Normal file
23
test/tunnel.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('./config'); //eslint-disable-line no-unused-vars
|
||||
const colors = require('colors');
|
||||
const sauceConnectLauncher = require('sauce-connect-launcher');
|
||||
|
||||
|
||||
function connect() {
|
||||
console.log(colors.yellow('Setting up tunnel from Saucelabs to your lovely computer, will take a while.'));
|
||||
// Creating the tunnel takes a bit of time. For this case we can safely disable Mocha timeouts.
|
||||
|
||||
sauceConnectLauncher(function (err) {
|
||||
if (err) {
|
||||
console.error(err.message);
|
||||
return;
|
||||
}
|
||||
console.log(colors.green('Connected! Keep this process running and execute your tests.'));
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
connect();
|
||||
}
|
Loading…
Reference in New Issue
Block a user