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",
|
"parser": "babel-eslint",
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true
|
"es6": true,
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"new-cap": [2, {newIsCap: true, capIsNew: false}],
|
"new-cap": [2, {newIsCap: true, capIsNew: false}],
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ gemini-report/*
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
|
|
||||||
.DS_Store
|
.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
|
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.
|
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:
|
Some interesting links:
|
||||||
@ -66,15 +67,17 @@ Some interesting links:
|
|||||||
Branch names
|
Branch names
|
||||||
============
|
============
|
||||||
|
|
||||||
Since we moved to Github, we cannot create branch names automatically with JIRA anymore.
|
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:
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
// 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
|
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.
|
where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title.
|
||||||
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
|
|
||||||
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
|
@ -39,7 +39,7 @@ class EditionListActions {
|
|||||||
|
|
||||||
return Q.Promise((resolve, reject) => {
|
return Q.Promise((resolve, reject) => {
|
||||||
EditionListFetcher
|
EditionListFetcher
|
||||||
.fetch({ pieceId, page, itemsToFetch, orderBy, orderAsc, filterBy })
|
.fetch({ pieceId, page, orderBy, orderAsc, filterBy, pageSize: itemsToFetch })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res && !res.editions) {
|
if (res && !res.editions) {
|
||||||
throw new Error('Piece has no editions to fetch.');
|
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 ApiUrls from '../../constants/api_urls';
|
||||||
import AppConstants from '../../constants/application_constants';
|
import AppConstants from '../../constants/application_constants';
|
||||||
|
import { validationTypes } from '../../constants/uploader_constants';
|
||||||
|
|
||||||
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';
|
||||||
@ -15,23 +16,26 @@ import { getLangText } from '../../utils/lang_utils';
|
|||||||
|
|
||||||
let FurtherDetailsFileuploader = React.createClass({
|
let FurtherDetailsFileuploader = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
pieceId: React.PropTypes.number.isRequired,
|
||||||
|
|
||||||
|
areAssetsDownloadable: React.PropTypes.bool,
|
||||||
|
editable: React.PropTypes.bool,
|
||||||
|
isReadyForFormSubmission: React.PropTypes.func,
|
||||||
label: React.PropTypes.string,
|
label: React.PropTypes.string,
|
||||||
pieceId: React.PropTypes.number,
|
multiple: React.PropTypes.bool,
|
||||||
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
|
onValidationFailed: React.PropTypes.func,
|
||||||
setIsUploadReady: React.PropTypes.func,
|
setIsUploadReady: React.PropTypes.func,
|
||||||
submitFile: React.PropTypes.func,
|
submitFile: React.PropTypes.func,
|
||||||
onValidationFailed: React.PropTypes.func,
|
validation: ReactS3FineUploader.propTypes.validation
|
||||||
isReadyForFormSubmission: React.PropTypes.func,
|
|
||||||
editable: React.PropTypes.bool,
|
|
||||||
multiple: React.PropTypes.bool,
|
|
||||||
areAssetsDownloadable: React.PropTypes.bool
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return {
|
return {
|
||||||
areAssetsDownloadable: true,
|
areAssetsDownloadable: true,
|
||||||
label: getLangText('Additional files'),
|
label: getLangText('Additional files'),
|
||||||
multiple: false
|
multiple: false,
|
||||||
|
validation: validationTypes.additionalData
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
|||||||
url: ApiUrls.blob_otherdatas,
|
url: ApiUrls.blob_otherdatas,
|
||||||
pieceId: this.props.pieceId
|
pieceId: this.props.pieceId
|
||||||
}}
|
}}
|
||||||
validation={AppConstants.fineUploader.validation.additionalData}
|
validation={this.props.validation}
|
||||||
submitFile={this.props.submitFile}
|
submitFile={this.props.submitFile}
|
||||||
onValidationFailed={this.props.onValidationFailed}
|
onValidationFailed={this.props.onValidationFailed}
|
||||||
setIsUploadReady={this.props.setIsUploadReady}
|
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
|
// We also force uniqueness of usernames, so this check is safe to dtermine if the
|
||||||
// content was registered by the current user.
|
// content was registered by the current user.
|
||||||
const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered);
|
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'] ?
|
let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
|
||||||
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
|
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
|
||||||
@ -123,7 +132,11 @@ let MediaContainer = React.createClass({
|
|||||||
className="ascribe-margin-1px"
|
className="ascribe-margin-1px"
|
||||||
href={content.digital_work.url}
|
href={content.digital_work.url}
|
||||||
target="_blank">
|
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>
|
</Button>
|
||||||
</AclProxy>
|
</AclProxy>
|
||||||
{embed}
|
{embed}
|
||||||
|
@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Form from '../ascribe_forms/form';
|
import ContractListActions from '../../actions/contract_list_actions';
|
||||||
import Property from '../ascribe_forms/property';
|
|
||||||
|
|
||||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
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 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 { getLangText } from '../../utils/lang_utils';
|
||||||
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||||
|
|
||||||
@ -78,8 +79,8 @@ let CreateContractForm = React.createClass({
|
|||||||
url: ApiUrls.blob_contracts
|
url: ApiUrls.blob_contracts
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit,
|
itemLimit: validationTypes.additionalData.itemLimit,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||||
allowedExtensions: ['pdf']
|
allowedExtensions: ['pdf']
|
||||||
}}
|
}}
|
||||||
areAssetsDownloadable={true}
|
areAssetsDownloadable={true}
|
||||||
|
@ -8,12 +8,16 @@ import UserActions from '../../actions/user_actions';
|
|||||||
import Form from './form';
|
import Form from './form';
|
||||||
import Property from './property';
|
import Property from './property';
|
||||||
import InputFineUploader from './input_fineuploader';
|
import InputFineUploader from './input_fineuploader';
|
||||||
import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button';
|
|
||||||
import FormSubmitButton from '../ascribe_buttons/form_submit_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 ApiUrls from '../../constants/api_urls';
|
||||||
import AppConstants from '../../constants/application_constants';
|
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 { getLangText } from '../../utils/lang_utils';
|
||||||
import { mergeOptions } from '../../utils/general_utils';
|
import { mergeOptions } from '../../utils/general_utils';
|
||||||
@ -180,7 +184,7 @@ let RegisterPieceForm = React.createClass({
|
|||||||
createBlobRoutine={{
|
createBlobRoutine={{
|
||||||
url: ApiUrls.blob_digitalworks
|
url: ApiUrls.blob_digitalworks
|
||||||
}}
|
}}
|
||||||
validation={AppConstants.fineUploader.validation.registerWork}
|
validation={validationTypes.registerWork}
|
||||||
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
|
setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')}
|
||||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||||
isFineUploaderActive={isFineUploaderActive}
|
isFineUploaderActive={isFineUploaderActive}
|
||||||
@ -206,9 +210,9 @@ let RegisterPieceForm = React.createClass({
|
|||||||
fileClass: 'thumbnail'
|
fileClass: 'thumbnail'
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit,
|
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit,
|
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
allowedExtensions: validationParts.allowedExtensions.images
|
||||||
}}
|
}}
|
||||||
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
|
setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')}
|
||||||
fileClassToUpload={{
|
fileClassToUpload={{
|
||||||
|
@ -28,11 +28,7 @@ const InputFineUploader = React.createClass({
|
|||||||
createBlobRoutine: shape({
|
createBlobRoutine: shape({
|
||||||
url: string
|
url: string
|
||||||
}),
|
}),
|
||||||
validation: shape({
|
validation: ReactS3FineUploader.propTypes.validation,
|
||||||
itemLimit: number,
|
|
||||||
sizeLimit: string,
|
|
||||||
allowedExtensions: arrayOf(string)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// isFineUploaderActive is used to lock react fine uploader in case
|
// isFineUploaderActive is used to lock react fine uploader in case
|
||||||
// a user is actually not logged in already to prevent him from droping files
|
// a user is actually not logged in already to prevent him from droping files
|
||||||
|
@ -2,17 +2,18 @@
|
|||||||
|
|
||||||
import React from 'react';
|
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 ContractListActions from '../../actions/contract_list_actions';
|
||||||
|
|
||||||
import GlobalNotificationModel from '../../models/global_notification_model';
|
import GlobalNotificationModel from '../../models/global_notification_model';
|
||||||
import GlobalNotificationActions from '../../actions/global_notification_actions';
|
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 { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||||
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';
|
||||||
@ -68,8 +69,8 @@ let ContractSettingsUpdateButton = React.createClass({
|
|||||||
url: ApiUrls.blob_contracts
|
url: ApiUrls.blob_contracts
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
itemLimit: validationTypes.registerWork.itemLimit,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||||
allowedExtensions: ['pdf']
|
allowedExtensions: ['pdf']
|
||||||
}}
|
}}
|
||||||
setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}}
|
setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}}
|
||||||
|
@ -23,6 +23,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
s3Url: string,
|
s3Url: string,
|
||||||
s3UrlSafe: string
|
s3UrlSafe: string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|
||||||
handleDeleteFile: func,
|
handleDeleteFile: func,
|
||||||
handleCancelFile: func,
|
handleCancelFile: func,
|
||||||
handlePauseFile: func,
|
handlePauseFile: func,
|
||||||
@ -33,9 +34,9 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
toggleUploadProcess() {
|
toggleUploadProcess() {
|
||||||
if(this.props.file.status === 'uploading') {
|
if (this.props.file.status === 'uploading') {
|
||||||
this.props.handlePauseFile(this.props.file.id);
|
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);
|
this.props.handleResumeFile(this.props.file.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -54,13 +55,13 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
(file.status === 'upload successful' || file.status === 'online') &&
|
(file.status === 'upload successful' || file.status === 'online') &&
|
||||||
file.s3UrlSafe) {
|
file.s3UrlSafe) {
|
||||||
handleDeleteFile(file.id);
|
handleDeleteFile(file.id);
|
||||||
} else if(handleCancelFile) {
|
} else if (handleCancelFile) {
|
||||||
handleCancelFile(file.id);
|
handleCancelFile(file.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDownloadFile() {
|
handleDownloadFile() {
|
||||||
if(this.props.file.s3Url) {
|
if (this.props.file.s3Url) {
|
||||||
// This simply opens a new browser tab with the url provided
|
// This simply opens a new browser tab with the url provided
|
||||||
open(this.props.file.s3Url);
|
open(this.props.file.s3Url);
|
||||||
}
|
}
|
||||||
@ -69,7 +70,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
getFileName() {
|
getFileName() {
|
||||||
const { numberOfDisplayedFiles, file } = this.props;
|
const { numberOfDisplayedFiles, file } = this.props;
|
||||||
|
|
||||||
if(numberOfDisplayedFiles === 1) {
|
if (numberOfDisplayedFiles === 1) {
|
||||||
return (
|
return (
|
||||||
<span className="file-name">
|
<span className="file-name">
|
||||||
{truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))}
|
{truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))}
|
||||||
@ -81,7 +82,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getRemoveButton() {
|
getRemoveButton() {
|
||||||
if(this.props.areAssetsEditable) {
|
if (this.props.areAssetsEditable) {
|
||||||
return (
|
return (
|
||||||
<div className="delete-file">
|
<div className="delete-file">
|
||||||
<span
|
<span
|
||||||
@ -107,7 +108,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
|
|
||||||
// Decide whether an image or a placeholder picture should be displayed
|
// 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 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 = (
|
previewElement = (
|
||||||
<FileDragAndDropPreviewImage
|
<FileDragAndDropPreviewImage
|
||||||
onClick={this.handleDeleteFile}
|
onClick={this.handleDeleteFile}
|
||||||
@ -123,7 +124,7 @@ const FileDragAndDropPreview = React.createClass({
|
|||||||
<FileDragAndDropPreviewOther
|
<FileDragAndDropPreviewOther
|
||||||
onClick={this.handleDeleteFile}
|
onClick={this.handleDeleteFile}
|
||||||
progress={file.progress}
|
progress={file.progress}
|
||||||
type={file.type.split('/')[1]}
|
type={extractFileExtensionFromString(file.name)}
|
||||||
toggleUploadProcess={this.toggleUploadProcess}
|
toggleUploadProcess={this.toggleUploadProcess}
|
||||||
areAssetsDownloadable={areAssetsDownloadable}
|
areAssetsDownloadable={areAssetsDownloadable}
|
||||||
downloadUrl={file.s3UrlSafe}
|
downloadUrl={file.s3UrlSafe}
|
||||||
|
@ -56,9 +56,9 @@ const FileDragAndDropPreviewOther = React.createClass({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="glyphicon glyphicon-download action-file"
|
className="glyphicon glyphicon-download action-file"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
title={getLangText('Download file')}/>
|
title={getLangText('Download file')} />
|
||||||
);
|
);
|
||||||
} else if(progress >= 0 && progress < 100) {
|
} else if (progress >= 0 && progress < 100) {
|
||||||
actionSymbol = (
|
actionSymbol = (
|
||||||
<div className="spinner-file">
|
<div className="spinner-file">
|
||||||
<AscribeSpinner color='dark-blue' size='md' />
|
<AscribeSpinner color='dark-blue' size='md' />
|
||||||
@ -66,22 +66,19 @@ const FileDragAndDropPreviewOther = React.createClass({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
actionSymbol = (
|
actionSymbol = (
|
||||||
<span className='ascribe-icon icon-ascribe-ok action-file'/>
|
<span className='ascribe-icon icon-ascribe-ok action-file' />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="file-drag-and-drop-preview">
|
||||||
className="file-drag-and-drop-preview">
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
now={Math.ceil(progress)}
|
now={Math.ceil(progress)}
|
||||||
style={style}
|
style={style}
|
||||||
className="ascribe-progress-bar ascribe-progress-bar-xs"/>
|
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">
|
||||||
<div className="file-drag-and-drop-preview-other">
|
{actionSymbol}
|
||||||
{actionSymbol}
|
<p style={style}>{'.' + (type ? type : 'file')}</p>
|
||||||
<p style={style}>{'.' + type}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -13,9 +13,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
|
|||||||
|
|
||||||
import AppConstants from '../../constants/application_constants';
|
import AppConstants from '../../constants/application_constants';
|
||||||
|
|
||||||
import { computeHashOfFile } from '../../utils/file_utils';
|
|
||||||
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
|
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
|
||||||
import { getCookie } from '../../utils/fetch_api_utils';
|
import { getCookie } from '../../utils/fetch_api_utils';
|
||||||
|
import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils';
|
||||||
import { getLangText } from '../../utils/lang_utils';
|
import { getLangText } from '../../utils/lang_utils';
|
||||||
|
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
}),
|
}),
|
||||||
validation: shape({
|
validation: shape({
|
||||||
itemLimit: number,
|
itemLimit: number,
|
||||||
sizeLimit: string,
|
sizeLimit: number,
|
||||||
allowedExtensions: arrayOf(string)
|
allowedExtensions: arrayOf(string)
|
||||||
}),
|
}),
|
||||||
messages: shape({
|
messages: shape({
|
||||||
@ -278,22 +278,6 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
this.setState(this.getInitialState());
|
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) {
|
requestKey(fileId) {
|
||||||
let filename = this.state.uploader.getName(fileId);
|
let filename = this.state.uploader.getName(fileId);
|
||||||
let uuid = this.state.uploader.getUuid(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) {
|
setThumbnailForFileId(fileId, url) {
|
||||||
const { filesToUpload } = this.state;
|
const { filesToUpload } = this.state;
|
||||||
|
|
||||||
@ -506,34 +591,6 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
GlobalNotificationActions.appendGlobalNotification(notification);
|
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) {
|
onCancel(id) {
|
||||||
// when a upload is canceled, we need to update this components file array
|
// when a upload is canceled, we need to update this components file array
|
||||||
this.setStatusOfFile(id, 'canceled')
|
this.setStatusOfFile(id, 'canceled')
|
||||||
@ -670,6 +727,13 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
this.cancelUploads(fileId);
|
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) {
|
handlePauseFile(fileId) {
|
||||||
if(this.state.uploader.pauseUpload(fileId)) {
|
if(this.state.uploader.pauseUpload(fileId)) {
|
||||||
this.setStatusOfFile(fileId, 'paused');
|
this.setStatusOfFile(fileId, 'paused');
|
||||||
@ -698,15 +762,8 @@ const ReactS3FineUploader = React.createClass({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate each submitted file if it fits the file size
|
// Select only the submitted files that fit the file size and allowed extensions
|
||||||
let validFiles = [];
|
files = this.selectValidFiles(files);
|
||||||
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;
|
|
||||||
|
|
||||||
// if multiple is set to false and user drops multiple files into the dropzone,
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
multiple,
|
multiple,
|
||||||
|
@ -18,6 +18,8 @@ import AclProxy from './acl_proxy';
|
|||||||
|
|
||||||
import EventActions from '../actions/event_actions';
|
import EventActions from '../actions/event_actions';
|
||||||
|
|
||||||
|
import PieceListStore from '../stores/piece_list_store';
|
||||||
|
|
||||||
import UserActions from '../actions/user_actions';
|
import UserActions from '../actions/user_actions';
|
||||||
import UserStore from '../stores/user_store';
|
import UserStore from '../stores/user_store';
|
||||||
|
|
||||||
@ -43,12 +45,17 @@ let Header = React.createClass({
|
|||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return mergeOptions(
|
return mergeOptions(
|
||||||
|
PieceListStore.getState(),
|
||||||
WhitelabelStore.getState(),
|
WhitelabelStore.getState(),
|
||||||
UserStore.getState()
|
UserStore.getState()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
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);
|
UserStore.listen(this.onChange);
|
||||||
UserActions.fetchCurrentUser.defer();
|
UserActions.fetchCurrentUser.defer();
|
||||||
|
|
||||||
@ -75,11 +82,16 @@ let Header = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
PieceListStore.unlisten(this.onChange);
|
||||||
UserStore.unlisten(this.onChange);
|
UserStore.unlisten(this.onChange);
|
||||||
WhitelabelStore.unlisten(this.onChange);
|
WhitelabelStore.unlisten(this.onChange);
|
||||||
//history.unlisten(this.onRouteChange);
|
//history.unlisten(this.onRouteChange);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onChange(state) {
|
||||||
|
this.setState(state);
|
||||||
|
},
|
||||||
|
|
||||||
getLogo() {
|
getLogo() {
|
||||||
let { whitelabel } = this.state;
|
let { whitelabel } = this.state;
|
||||||
|
|
||||||
@ -93,16 +105,16 @@ let Header = React.createClass({
|
|||||||
<img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/>
|
<img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/>
|
||||||
</Link>
|
</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 (
|
return (
|
||||||
<AclProxy
|
<AclProxy
|
||||||
aclObject={this.state.whitelabel}
|
aclObject={this.state.whitelabel}
|
||||||
@ -117,10 +129,6 @@ let Header = React.createClass({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange(state) {
|
|
||||||
this.setState(state);
|
|
||||||
},
|
|
||||||
|
|
||||||
onMenuItemClick() {
|
onMenuItemClick() {
|
||||||
/*
|
/*
|
||||||
This is a hack to make the dropdown close after clicking on an item
|
This is a hack to make the dropdown close after clicking on an item
|
||||||
@ -156,16 +164,18 @@ let Header = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { currentUser, unfilteredPieceListCount } = this.state;
|
||||||
let account;
|
let account;
|
||||||
let signup;
|
let signup;
|
||||||
let navRoutesLinks;
|
let navRoutesLinks;
|
||||||
if (this.state.currentUser.username){
|
|
||||||
|
if (currentUser.username) {
|
||||||
account = (
|
account = (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
ref='dropdownbutton'
|
ref='dropdownbutton'
|
||||||
id="nav-route-user-dropdown"
|
id="nav-route-user-dropdown"
|
||||||
eventKey="1"
|
eventKey="1"
|
||||||
title={this.state.currentUser.username}>
|
title={currentUser.username}>
|
||||||
<LinkContainer
|
<LinkContainer
|
||||||
to="/settings"
|
to="/settings"
|
||||||
onClick={this.onMenuItemClick}>
|
onClick={this.onMenuItemClick}>
|
||||||
@ -175,7 +185,7 @@ let Header = React.createClass({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
<AclProxy
|
<AclProxy
|
||||||
aclObject={this.state.currentUser.acl}
|
aclObject={currentUser.acl}
|
||||||
aclName="acl_view_settings_contract">
|
aclName="acl_view_settings_contract">
|
||||||
<LinkContainer
|
<LinkContainer
|
||||||
to="/contract_settings"
|
to="/contract_settings"
|
||||||
@ -196,9 +206,21 @@ let Header = React.createClass({
|
|||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
navRoutesLinks = <NavRoutesLinks routes={this.props.routes} userAcl={this.state.currentUser.acl} navbar right/>;
|
|
||||||
}
|
// Let's assume that if the piece list hasn't loaded yet (ie. when unfilteredPieceListCount === -1)
|
||||||
else {
|
// 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 = (
|
account = (
|
||||||
<LinkContainer
|
<LinkContainer
|
||||||
to="/login">
|
to="/login">
|
||||||
|
@ -11,14 +11,33 @@ import AclProxy from './acl_proxy';
|
|||||||
import { sanitizeList } from '../utils/general_utils';
|
import { sanitizeList } from '../utils/general_utils';
|
||||||
|
|
||||||
|
|
||||||
|
const DISABLE_ENUM = ['hasPieces', 'noPieces'];
|
||||||
|
|
||||||
let NavRoutesLinks = React.createClass({
|
let NavRoutesLinks = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
hasPieces: React.PropTypes.bool,
|
||||||
routes: React.PropTypes.arrayOf(React.PropTypes.object),
|
routes: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
userAcl: 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
|
* 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
|
* 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
|
* @return {Array} Array of ReactElements that can be displayed to the user
|
||||||
*/
|
*/
|
||||||
extractLinksFromRoutes(node, userAcl, i) {
|
extractLinksFromRoutes(node, userAcl, i) {
|
||||||
if(!node) {
|
if (!node) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let links = node.childRoutes.map((child, j) => {
|
const links = node.childRoutes.map((child, j) => {
|
||||||
let childrenFn = null;
|
const { aclName, disableOn, headerTitle, path, childRoutes } = child;
|
||||||
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++);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We validate if the user has set the title correctly,
|
// We validate if the user has set the title correctly,
|
||||||
// otherwise we're not going to render his route
|
// 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,
|
// if there is an aclName present on the route definition,
|
||||||
// we evaluate it against the user's acl
|
// we evaluate it against the user's acl
|
||||||
if(aclName && typeof aclName !== 'undefined') {
|
if (aclName && typeof aclName !== 'undefined') {
|
||||||
return (
|
return (
|
||||||
<AclProxy
|
<AclProxy
|
||||||
key={j}
|
key={j}
|
||||||
aclName={aclName}
|
aclName={aclName}
|
||||||
aclObject={this.props.userAcl}>
|
aclObject={this.props.userAcl}>
|
||||||
<NavRoutesLinksLink
|
<NavRoutesLinksLink {...navLinkProps} />
|
||||||
headerTitle={headerTitle}
|
|
||||||
routePath={'/' + path}
|
|
||||||
depth={i}
|
|
||||||
children={childrenFn}/>
|
|
||||||
</AclProxy>
|
</AclProxy>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<NavRoutesLinksLink
|
<NavRoutesLinksLink
|
||||||
key={j}
|
key={j}
|
||||||
headerTitle={headerTitle}
|
{...navLinkProps} />
|
||||||
routePath={'/' + path}
|
|
||||||
depth={i}
|
|
||||||
children={childrenFn}/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -84,7 +105,7 @@ let NavRoutesLinks = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let {routes, userAcl} = this.props;
|
const {routes, userAcl} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nav {...this.props}>
|
<Nav {...this.props}>
|
||||||
@ -94,4 +115,4 @@ let NavRoutesLinks = React.createClass({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NavRoutesLinks;
|
export default NavRoutesLinks;
|
||||||
|
@ -11,42 +11,46 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer';
|
|||||||
|
|
||||||
let NavRoutesLinksLink = React.createClass({
|
let NavRoutesLinksLink = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
headerTitle: React.PropTypes.string,
|
|
||||||
routePath: React.PropTypes.string,
|
|
||||||
|
|
||||||
children: React.PropTypes.oneOfType([
|
children: React.PropTypes.oneOfType([
|
||||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||||
React.PropTypes.element
|
React.PropTypes.element
|
||||||
]),
|
]),
|
||||||
|
disabled: React.PropTypes.bool,
|
||||||
depth: React.PropTypes.number
|
depth: React.PropTypes.number,
|
||||||
|
headerTitle: React.PropTypes.string,
|
||||||
|
routePath: React.PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
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
|
// if the route has children, we're returning a DropdownButton that will get filled
|
||||||
// with MenuItems
|
// with MenuItems
|
||||||
if(children) {
|
if (children) {
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
disabled={disabled}
|
||||||
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
|
id={`nav-route-${headerTitle.toLowerCase()}-dropdown`}
|
||||||
title={headerTitle}>
|
title={headerTitle}>
|
||||||
{children}
|
{children}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
} else {
|
} 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
|
// if the node's child is actually a node of level one (a child of a node), we're
|
||||||
// returning a DropdownButton matching MenuItem
|
// returning a DropdownButton matching MenuItem
|
||||||
return (
|
return (
|
||||||
<LinkContainer to={routePath}>
|
<LinkContainer
|
||||||
|
disabled={disabled}
|
||||||
|
to={routePath}>
|
||||||
<MenuItem>{headerTitle}</MenuItem>
|
<MenuItem>{headerTitle}</MenuItem>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
);
|
);
|
||||||
} else if(depth === 0) {
|
} else if (depth === 0) {
|
||||||
return (
|
return (
|
||||||
<LinkContainer to={routePath}>
|
<LinkContainer
|
||||||
|
disabled={disabled}
|
||||||
|
to={routePath}>
|
||||||
<NavItem>{headerTitle}</NavItem>
|
<NavItem>{headerTitle}</NavItem>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,6 @@ let PieceList = React.createClass({
|
|||||||
accordionListItemType: AccordionListItemWallet,
|
accordionListItemType: AccordionListItemWallet,
|
||||||
bulkModalButtonListType: AclButtonList,
|
bulkModalButtonListType: AclButtonList,
|
||||||
canLoadPieceList: true,
|
canLoadPieceList: true,
|
||||||
orderParams: ['artist_name', 'title'],
|
|
||||||
filterParams: [{
|
filterParams: [{
|
||||||
label: getLangText('Show works I can'),
|
label: getLangText('Show works I can'),
|
||||||
items: [
|
items: [
|
||||||
@ -61,7 +60,10 @@ let PieceList = React.createClass({
|
|||||||
'acl_consign',
|
'acl_consign',
|
||||||
'acl_create_editions'
|
'acl_create_editions'
|
||||||
]
|
]
|
||||||
}]
|
}],
|
||||||
|
orderParams: ['artist_name', 'title'],
|
||||||
|
redirectTo: '/register_piece',
|
||||||
|
shouldRedirect: () => true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3,19 +3,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { History } from 'react-router';
|
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 Form from '../../../../../ascribe_forms/form';
|
||||||
import Property from '../../../../../ascribe_forms/property';
|
import Property from '../../../../../ascribe_forms/property';
|
||||||
import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable';
|
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 InputFineuploader from '../../../../../ascribe_forms/input_fineuploader';
|
||||||
|
import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button';
|
||||||
|
|
||||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
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 ApiUrls from '../../../../../../constants/api_urls';
|
||||||
|
import AppConstants from '../../../../../../constants/application_constants';
|
||||||
|
import { validationParts, validationTypes } from '../../../../../../constants/uploader_constants';
|
||||||
|
|
||||||
import requests from '../../../../../../utils/requests';
|
import requests from '../../../../../../utils/requests';
|
||||||
|
|
||||||
@ -193,7 +195,7 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const maxThumbnailSize = AppConstants.fineUploader.validation.workThumbnail.sizeLimit / 1000000;
|
const maxThumbnailSize = validationTypes.workThumbnail.sizeLimit / 1000000;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="register-piece--form">
|
<div className="register-piece--form">
|
||||||
@ -305,8 +307,8 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
fileClass: 'digitalwork'
|
fileClass: 'digitalwork'
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
itemLimit: validationTypes.registerWork.itemLimit,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||||
allowedExtensions: ['pdf']
|
allowedExtensions: ['pdf']
|
||||||
}}
|
}}
|
||||||
location={location}
|
location={location}
|
||||||
@ -318,7 +320,7 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
</Property>
|
</Property>
|
||||||
<Property
|
<Property
|
||||||
name="thumbnailKey"
|
name="thumbnailKey"
|
||||||
label={`${getLangText('Featured Cover photo')} max ${maxThumbnailSize}MB`}>
|
label={`${getLangText('Featured Cover photo')} (max ${maxThumbnailSize}MB)`}>
|
||||||
<InputFineuploader
|
<InputFineuploader
|
||||||
fileInputElement={UploadButton()}
|
fileInputElement={UploadButton()}
|
||||||
createBlobRoutine={{
|
createBlobRoutine={{
|
||||||
@ -331,9 +333,9 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
fileClass: 'thumbnail'
|
fileClass: 'thumbnail'
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit,
|
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit,
|
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
allowedExtensions: validationParts.allowedExtensions.images
|
||||||
}}
|
}}
|
||||||
location={location}
|
location={location}
|
||||||
fileClassToUpload={{
|
fileClassToUpload={{
|
||||||
@ -356,8 +358,8 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
fileClass: 'otherdata'
|
fileClass: 'otherdata'
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
itemLimit: validationParts.itemLimit.single,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit
|
sizeLimit: validationTypes.additionalData.sizeLimit
|
||||||
}}
|
}}
|
||||||
location={location}
|
location={location}
|
||||||
fileClassToUpload={{
|
fileClassToUpload={{
|
||||||
@ -378,9 +380,9 @@ const PRRegisterPieceForm = React.createClass({
|
|||||||
fileClass: 'otherdata'
|
fileClass: 'otherdata'
|
||||||
}}
|
}}
|
||||||
validation={{
|
validation={{
|
||||||
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
|
itemLimit: validationParts.itemLimit.single,
|
||||||
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
|
sizeLimit: validationTypes.additionalData.sizeLimit,
|
||||||
allowedExtensions: ['png', 'jpg', 'jpeg', 'gif']
|
allowedExtensions: validationParts.allowedExtensions.images
|
||||||
}}
|
}}
|
||||||
location={location}
|
location={location}
|
||||||
fileClassToUpload={{
|
fileClassToUpload={{
|
||||||
|
@ -75,7 +75,6 @@ let PrizePieceList = React.createClass({
|
|||||||
<div>
|
<div>
|
||||||
<PieceList
|
<PieceList
|
||||||
ref="list"
|
ref="list"
|
||||||
redirectTo="/register_piece"
|
|
||||||
accordionListItemType={AccordionListItemPrize}
|
accordionListItemType={AccordionListItemPrize}
|
||||||
orderParams={orderParams}
|
orderParams={orderParams}
|
||||||
orderBy={this.state.currentUser.is_jury ? 'rating' : null}
|
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 Form from '../../../../../ascribe_forms/form';
|
||||||
import Property from '../../../../../ascribe_forms/property';
|
import Property from '../../../../../ascribe_forms/property';
|
||||||
|
|
||||||
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
|
|
||||||
|
|
||||||
import AscribeSpinner from '../../../../../ascribe_spinner';
|
import AscribeSpinner from '../../../../../ascribe_spinner';
|
||||||
|
|
||||||
import ApiUrls from '../../../../../../constants/api_urls';
|
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 requests from '../../../../../../utils/requests';
|
||||||
|
|
||||||
|
import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils';
|
||||||
import { mergeOptions } from '../../../../../../utils/general_utils';
|
import { mergeOptions } from '../../../../../../utils/general_utils';
|
||||||
import { getLangText } from '../../../../../../utils/lang_utils';
|
import { getLangText } from '../../../../../../utils/lang_utils';
|
||||||
|
|
||||||
@ -170,7 +170,12 @@ let MarketAdditionalDataForm = React.createClass({
|
|||||||
otherData={otherData}
|
otherData={otherData}
|
||||||
pieceId={pieceId}
|
pieceId={pieceId}
|
||||||
setIsUploadReady={this.setIsUploadReady}
|
setIsUploadReady={this.setIsUploadReady}
|
||||||
submitFile={function () {}} />
|
submitFile={function () {}}
|
||||||
|
validation={{
|
||||||
|
itemLimit: validationTypes.workThumbnail.itemLimit,
|
||||||
|
sizeLimit: validationTypes.workThumbnail.sizeLimit,
|
||||||
|
allowedExtensions: validationParts.allowedExtensions.images
|
||||||
|
}} />
|
||||||
<Property
|
<Property
|
||||||
name='artist_bio'
|
name='artist_bio'
|
||||||
label={getLangText('Artist Bio')}
|
label={getLangText('Artist Bio')}
|
||||||
|
@ -75,7 +75,8 @@ let ROUTES = {
|
|||||||
<Route
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(CylandPieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(CylandPieceList)}
|
||||||
headerTitle='COLLECTION' />
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route path='editions/:editionId' component={EditionContainer} />
|
<Route path='editions/:editionId' component={EditionContainer} />
|
||||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||||
<Route path='pieces/:pieceId' component={CylandPieceContainer} />
|
<Route path='pieces/:pieceId' component={CylandPieceContainer} />
|
||||||
@ -109,7 +110,8 @@ let ROUTES = {
|
|||||||
<Route
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
||||||
headerTitle='COLLECTION' />
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route path='pieces/:pieceId' component={PieceContainer} />
|
<Route path='pieces/:pieceId' component={PieceContainer} />
|
||||||
<Route path='editions/:editionId' component={EditionContainer} />
|
<Route path='editions/:editionId' component={EditionContainer} />
|
||||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||||
@ -150,7 +152,8 @@ let ROUTES = {
|
|||||||
<Route
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvPieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvPieceList)}
|
||||||
headerTitle='COLLECTION' />
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route
|
<Route
|
||||||
path='contract_notifications'
|
path='contract_notifications'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} />
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} />
|
||||||
@ -189,7 +192,8 @@ let ROUTES = {
|
|||||||
<Route
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(MarketPieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(MarketPieceList)}
|
||||||
headerTitle='COLLECTION' />
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
||||||
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
||||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||||
@ -225,7 +229,8 @@ let ROUTES = {
|
|||||||
<Route
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(Vivi23PieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(Vivi23PieceList)}
|
||||||
headerTitle='COLLECTION' />
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
<Route path='pieces/:pieceId' component={MarketPieceContainer} />
|
||||||
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
<Route path='editions/:editionId' component={MarketEditionContainer} />
|
||||||
<Route path='coa_verify' component={CoaVerifyContainer} />
|
<Route path='coa_verify' component={CoaVerifyContainer} />
|
||||||
|
@ -68,23 +68,6 @@ const constants = {
|
|||||||
// Source: http://www.w3schools.com/tags/att_input_type.asp
|
// 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'],
|
'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',
|
'copyrightAssociations': ['ARS', 'DACS', 'Bildkunst', 'Pictoright', 'SODRAC', 'Copyright Agency/Viscopy', 'SAVA',
|
||||||
'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART',
|
'Bildrecht GmbH', 'SABAM', 'AUTVIS', 'CREAIMAGEN', 'SONECA', 'Copydan', 'EAU', 'Kuvasto', 'GCA', 'HUNGART',
|
||||||
'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV',
|
'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
|
<Route
|
||||||
path='collection'
|
path='collection'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PieceList)}
|
||||||
headerTitle='COLLECTION'/>
|
headerTitle='COLLECTION'
|
||||||
|
disableOn='noPieces' />
|
||||||
<Route
|
<Route
|
||||||
path='signup'
|
path='signup'
|
||||||
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} />
|
component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} />
|
||||||
|
@ -78,7 +78,7 @@ export function computeHashOfFile(file) {
|
|||||||
progress: start / file.size,
|
progress: start / file.size,
|
||||||
reject
|
reject
|
||||||
});
|
});
|
||||||
|
|
||||||
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
|
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,4 +101,4 @@ export function extractFileExtensionFromString(s) {
|
|||||||
const explodedFileName = s.split('.');
|
const explodedFileName = s.split('.');
|
||||||
return explodedFileName.length > 1 ? explodedFileName.pop()
|
return explodedFileName.length > 1 ? explodedFileName.pop()
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
16
package.json
16
package.json
@ -8,10 +8,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint ./js",
|
"lint": "eslint ./js",
|
||||||
|
"preinstall": "export SAUCE_CONNECT_DOWNLOAD_ON_INSTALL=true",
|
||||||
"postinstall": "npm run build",
|
"postinstall": "npm run build",
|
||||||
"build": "gulp build --production",
|
"build": "gulp build --production",
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
|
|
||||||
|
"test": "mocha",
|
||||||
|
"tunnel": "node test/tunnel.js"
|
||||||
|
|
||||||
"vi-clean": "rm -rf gemini-report",
|
"vi-clean": "rm -rf gemini-report",
|
||||||
"vi-phantom": "phantomjs --webdriver=4444",
|
"vi-phantom": "phantomjs --webdriver=4444",
|
||||||
"vi-update": "gemini update",
|
"vi-update": "gemini update",
|
||||||
@ -48,10 +52,16 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^3.1.11",
|
"babel-eslint": "^3.1.11",
|
||||||
"babel-jest": "^5.2.0",
|
"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",
|
"gemini": "^2.1.0",
|
||||||
"gulp-sass": "^2.1.1",
|
|
||||||
"jest-cli": "^0.4.0",
|
"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": {
|
"dependencies": {
|
||||||
"alt": "^0.16.5",
|
"alt": "^0.16.5",
|
||||||
@ -77,7 +87,7 @@
|
|||||||
"gulp-if": "^1.2.5",
|
"gulp-if": "^1.2.5",
|
||||||
"gulp-minify-css": "^1.1.6",
|
"gulp-minify-css": "^1.1.6",
|
||||||
"gulp-notify": "^2.2.0",
|
"gulp-notify": "^2.2.0",
|
||||||
"gulp-sass": "^2.0.1",
|
"gulp-sass": "^2.1.1",
|
||||||
"gulp-sourcemaps": "^1.5.2",
|
"gulp-sourcemaps": "^1.5.2",
|
||||||
"gulp-template": "~3.0.0",
|
"gulp-template": "~3.0.0",
|
||||||
"gulp-uglify": "^1.2.0",
|
"gulp-uglify": "^1.2.0",
|
||||||
|
@ -7,23 +7,25 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
transition: .2s bottom cubic-bezier(.77, 0, .175, 1);
|
transition: .2s bottom cubic-bezier(.77, 0, .175, 1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.ascribe-global-notification-off {
|
z-index: 9999;
|
||||||
bottom: -3.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ascribe-global-notification-on {
|
> div {
|
||||||
bottom: 0;
|
padding: .5em 1em;
|
||||||
}
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.ascribe-global-notification > div,
|
&.ascribe-global-notification-off {
|
||||||
.ascribe-global-notification-bubble > div {
|
bottom: -5em;
|
||||||
display: table-cell;
|
}
|
||||||
font-size: 1.25em;
|
|
||||||
padding-right: 3em;
|
&.ascribe-global-notification-on {
|
||||||
text-align: right;
|
bottom: 0;
|
||||||
vertical-align: middle;
|
|
||||||
|
// 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 {
|
.ascribe-global-notification-bubble {
|
||||||
@ -33,21 +35,35 @@
|
|||||||
color: white;
|
color: white;
|
||||||
display: table;
|
display: table;
|
||||||
height: 3.5em;
|
height: 3.5em;
|
||||||
|
max-width: 75%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: -50em;
|
transition: 0.5s right ease;
|
||||||
transition: 1s right ease;
|
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
padding: .75em 1.5em;
|
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 {
|
.ascribe-global-notification > div,
|
||||||
right: -100em;
|
.ascribe-global-notification-bubble > div {
|
||||||
}
|
display: table-cell;
|
||||||
|
font-size: 1.25em;
|
||||||
.ascribe-global-notification-bubble-on {
|
vertical-align: middle;
|
||||||
right: 3.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ascribe-global-notification-danger {
|
.ascribe-global-notification-danger {
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding-top: 1.5em;
|
padding-top: 1.5em;
|
||||||
|
|
||||||
|
@media screen and (max-width: 625px) {
|
||||||
|
.file-name {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive-dropzone {
|
.inactive-dropzone {
|
||||||
@ -78,12 +84,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-preview-table-wrapper {
|
|
||||||
display: table;
|
|
||||||
height: 94px;
|
|
||||||
width: 104px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-drag-and-drop-preview {
|
.file-drag-and-drop-preview {
|
||||||
background-color: #eeeeee;
|
background-color: #eeeeee;
|
||||||
border: 1px solid #616161;
|
border: 1px solid #616161;
|
||||||
@ -131,7 +131,7 @@
|
|||||||
width: 104px;
|
width: 104px;
|
||||||
|
|
||||||
// REFACTOR TO USE TABLE CELL
|
// REFACTOR TO USE TABLE CELL
|
||||||
.action-file, .spinner-file, .icon-ascribe-ok {
|
.action-file, .spinner-file {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@ -142,23 +142,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-drag-and-drop-preview-other {
|
.file-drag-and-drop-preview-other {
|
||||||
display: table-cell;
|
height: 94px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
width: 104px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-file:not(.icon-ascribe-ok), .spinner-file {
|
.action-file:not(.icon-ascribe-ok), .spinner-file {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: .3em;
|
top: 0.8em;
|
||||||
|
|
||||||
span:not(:first-child) {
|
|
||||||
display: block;
|
|
||||||
margin-top: .5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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