diff --git a/README.md b/README.md index e07eca0d..478562d3 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,18 @@ For this project, we're using: Branch names ===================== -Since we moved to Github, we cannot create branch names automatically with JIRA anymore. -To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion. + +To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches: ``` +// For issues logged in Github: +AG--brief-and-sane-description-of-the-ticket + +// For issues logged in JIRA: AD--brief-and-sane-description-of-the-ticket ``` -where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title. -This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind. +where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title. Example ------------- diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index 8546e43f..61e0724f 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -8,6 +8,7 @@ import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader'; import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; +import { validationTypes } from '../../constants/uploader_constants'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -15,23 +16,26 @@ import { getLangText } from '../../utils/lang_utils'; let FurtherDetailsFileuploader = React.createClass({ propTypes: { + pieceId: React.PropTypes.number.isRequired, + + areAssetsDownloadable: React.PropTypes.bool, + editable: React.PropTypes.bool, + isReadyForFormSubmission: React.PropTypes.func, label: React.PropTypes.string, - pieceId: React.PropTypes.number, + multiple: React.PropTypes.bool, otherData: React.PropTypes.arrayOf(React.PropTypes.object), + onValidationFailed: React.PropTypes.func, setIsUploadReady: React.PropTypes.func, submitFile: React.PropTypes.func, - onValidationFailed: React.PropTypes.func, - isReadyForFormSubmission: React.PropTypes.func, - editable: React.PropTypes.bool, - multiple: React.PropTypes.bool, - areAssetsDownloadable: React.PropTypes.bool + validation: ReactS3FineUploader.propTypes.validation }, getDefaultProps() { return { areAssetsDownloadable: true, label: getLangText('Additional files'), - multiple: false + multiple: false, + validation: validationTypes.additionalData }; }, @@ -61,7 +65,7 @@ let FurtherDetailsFileuploader = React.createClass({ url: ApiUrls.blob_otherdatas, pieceId: this.props.pieceId }} - validation={AppConstants.fineUploader.validation.additionalData} + validation={this.props.validation} submitFile={this.props.submitFile} onValidationFailed={this.props.onValidationFailed} setIsUploadReady={this.props.setIsUploadReady} diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js index fcd874da..cbdd12c3 100644 --- a/js/components/ascribe_forms/form_create_contract.js +++ b/js/components/ascribe_forms/form_create_contract.js @@ -2,19 +2,20 @@ import React from 'react'; -import Form from '../ascribe_forms/form'; -import Property from '../ascribe_forms/property'; +import ContractListActions from '../../actions/contract_list_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import ContractListActions from '../../actions/contract_list_actions'; - -import AppConstants from '../../constants/application_constants'; -import ApiUrls from '../../constants/api_urls'; - import InputFineUploader from './input_fineuploader'; +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; +import { validationTypes } from '../../constants/uploader_constants'; + import { getLangText } from '../../utils/lang_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; @@ -78,8 +79,8 @@ let CreateContractForm = React.createClass({ url: ApiUrls.blob_contracts }} validation={{ - itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + itemLimit: validationTypes.additionalData.itemLimit, + sizeLimit: validationTypes.additionalData.sizeLimit, allowedExtensions: ['pdf'] }} areAssetsDownloadable={true} diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 10fb54d3..93f0ddf4 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -8,12 +8,16 @@ import UserActions from '../../actions/user_actions'; import Form from './form'; import Property from './property'; import InputFineUploader from './input_fineuploader'; -import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + import FormSubmitButton from '../ascribe_buttons/form_submit_button'; +import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + +import AscribeSpinner from '../ascribe_spinner'; + import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; -import AscribeSpinner from '../ascribe_spinner'; +import { validationParts, validationTypes } from '../../constants/uploader_constants'; import { getLangText } from '../../utils/lang_utils'; import { mergeOptions } from '../../utils/general_utils'; @@ -180,7 +184,7 @@ let RegisterPieceForm = React.createClass({ createBlobRoutine={{ url: ApiUrls.blob_digitalworks }} - validation={AppConstants.fineUploader.validation.registerWork} + validation={validationTypes.registerWork} setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isFineUploaderActive={isFineUploaderActive} @@ -206,9 +210,9 @@ let RegisterPieceForm = React.createClass({ fileClass: 'thumbnail' }} validation={{ - itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit, - allowedExtensions: ['png', 'jpg', 'jpeg', 'gif'] + itemLimit: validationTypes.workThumbnail.itemLimit, + sizeLimit: validationTypes.workThumbnail.sizeLimit, + allowedExtensions: validationParts.allowedExtensions.images }} setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')} fileClassToUpload={{ diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index fa9c72b6..80fa1891 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -28,11 +28,7 @@ const InputFineUploader = React.createClass({ createBlobRoutine: shape({ url: string }), - validation: shape({ - itemLimit: number, - sizeLimit: string, - allowedExtensions: arrayOf(string) - }), + validation: ReactS3FineUploader.propTypes.validation, // isFineUploaderActive is used to lock react fine uploader in case // a user is actually not logged in already to prevent him from droping files diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js index f3e92c69..dc652b9b 100644 --- a/js/components/ascribe_settings/contract_settings_update_button.js +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -2,17 +2,18 @@ import React from 'react'; -import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; -import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; - -import AppConstants from '../../constants/application_constants'; -import ApiUrls from '../../constants/api_urls'; - import ContractListActions from '../../actions/contract_list_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; +import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; +import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; +import { validationTypes } from '../../constants/uploader_constants'; + import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -68,8 +69,8 @@ let ContractSettingsUpdateButton = React.createClass({ url: ApiUrls.blob_contracts }} validation={{ - itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + itemLimit: validationTypes.registerWork.itemLimit, + sizeLimit: validationTypes.additionalData.sizeLimit, allowedExtensions: ['pdf'] }} setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}} diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index feb0479d..447b76ea 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -13,9 +13,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import AppConstants from '../../constants/application_constants'; -import { computeHashOfFile } from '../../utils/file_utils'; import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; +import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -91,7 +91,7 @@ const ReactS3FineUploader = React.createClass({ }), validation: shape({ itemLimit: number, - sizeLimit: string, + sizeLimit: number, allowedExtensions: arrayOf(string) }), messages: shape({ @@ -278,22 +278,6 @@ const ReactS3FineUploader = React.createClass({ this.setState(this.getInitialState()); }, - // Cancel uploads and clear previously selected files on the input element - cancelUploads(id) { - typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); - - // Reset the file input element to clear the previously selected files so that - // the user can reselect them again. - this.clearFileSelection(); - }, - - clearFileSelection() { - const { fileInput } = this.refs; - if (fileInput && typeof fileInput.clearSelection === 'function') { - fileInput.clearSelection(); - } - }, - requestKey(fileId) { let filename = this.state.uploader.getName(fileId); let uuid = this.state.uploader.getUuid(fileId); @@ -384,6 +368,107 @@ const ReactS3FineUploader = React.createClass({ }); }, + // Cancel uploads and clear previously selected files on the input element + cancelUploads(id) { + typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); + + // Reset the file input element to clear the previously selected files so that + // the user can reselect them again. + this.clearFileSelection(); + }, + + clearFileSelection() { + const { fileInput } = this.refs; + if (fileInput && typeof fileInput.clearSelection === 'function') { + fileInput.clearSelection(); + } + }, + + getAllowedExtensions() { + const { validation: { allowedExtensions } = {} } = this.props; + + if (allowedExtensions && allowedExtensions.length) { + return transformAllowedExtensionsToInputAcceptProp(allowedExtensions); + } else { + return null; + } + }, + + getXhrErrorComment(xhr) { + if (xhr) { + return { + response: xhr.response, + url: xhr.responseURL, + status: xhr.status, + statusText: xhr.statusText + }; + } + }, + + isDropzoneInactive() { + const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); + + if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { + return true; + } else { + return false; + } + }, + + isFileValid(file) { + const { validation: { allowedExtensions, sizeLimit = 0 }, onValidationFailed } = this.props; + const fileExt = extractFileExtensionFromString(file.name); + + if (file.size > sizeLimit) { + const fileSizeInMegaBytes = sizeLimit / 1000000; + + const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + if (typeof onValidationFailed === 'function') { + onValidationFailed(file); + } + + return false; + } else if (allowedExtensions && !allowedExtensions.includes(fileExt)) { + const notification = new GlobalNotificationModel(getLangText(`The file you've submitted is of an invalid file format: Valid format(s): ${allowedExtensions.join(', ')}`), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + return false; + } else { + return true; + } + }, + + selectValidFiles(files) { + return Array.from(files).reduce((validFiles, file) => { + if (this.isFileValid(file)) { + validFiles.push(file); + } + return validFiles; + }, []); + }, + + // This method has been made promise-based to immediately afterwards + // call a callback function (instantly after this.setState went through) + // This is e.g. needed when showing/hiding the optional thumbnail upload + // field in the registration form + setStatusOfFile(fileId, status) { + return Q.Promise((resolve) => { + let changeSet = {}; + + if(status === 'deleted' || status === 'canceled') { + changeSet.progress = { $set: 0 }; + } + + changeSet.status = { $set: status }; + + let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); + + this.setState({ filesToUpload }, resolve); + }); + }, + setThumbnailForFileId(fileId, url) { const { filesToUpload } = this.state; @@ -506,34 +591,6 @@ const ReactS3FineUploader = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); }, - getXhrErrorComment(xhr) { - if (xhr) { - return { - response: xhr.response, - url: xhr.responseURL, - status: xhr.status, - statusText: xhr.statusText - }; - } - }, - - isFileValid(file) { - if (file.size > this.props.validation.sizeLimit) { - const fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - - const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); - - if (typeof this.props.onValidationFailed === 'function') { - this.props.onValidationFailed(file); - } - - return false; - } else { - return true; - } - }, - onCancel(id) { // when a upload is canceled, we need to update this components file array this.setStatusOfFile(id, 'canceled') @@ -670,6 +727,13 @@ const ReactS3FineUploader = React.createClass({ this.cancelUploads(fileId); }, + handleCancelHashing() { + // Every progress tick of the hashing function in handleUploadFile there is a + // check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels + // the hashing of all files immediately. + this.setState({ hashingProgress: -1 }); + }, + handlePauseFile(fileId) { if(this.state.uploader.pauseUpload(fileId)) { this.setStatusOfFile(fileId, 'paused'); @@ -698,15 +762,8 @@ const ReactS3FineUploader = React.createClass({ return; } - // validate each submitted file if it fits the file size - let validFiles = []; - for(let i = 0; i < files.length; i++) { - if(this.isFileValid(files[i])) { - validFiles.push(files[i]); - } - } - // override standard files list with only valid files - files = validFiles; + // Select only the submitted files that fit the file size and allowed extensions + files = this.selectValidFiles(files); // if multiple is set to false and user drops multiple files into the dropzone, // take the first one and notify user that only one file can be submitted @@ -817,13 +874,6 @@ const ReactS3FineUploader = React.createClass({ } }, - handleCancelHashing() { - // Every progress tick of the hashing function in handleUploadFile there is a - // check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels - // the hashing of all files immediately. - this.setState({ hashingProgress: -1 }); - }, - // ReactFineUploader is essentially just a react layer around s3 fineuploader. // However, since we need to display the status of a file (progress, uploading) as well as // be able to execute actions on a currently uploading file we need to exactly sync the file list @@ -893,46 +943,6 @@ const ReactS3FineUploader = React.createClass({ }); }, - // This method has been made promise-based to immediately afterwards - // call a callback function (instantly after this.setState went through) - // This is e.g. needed when showing/hiding the optional thumbnail upload - // field in the registration form - setStatusOfFile(fileId, status) { - return Q.Promise((resolve) => { - let changeSet = {}; - - if(status === 'deleted' || status === 'canceled') { - changeSet.progress = { $set: 0 }; - } - - changeSet.status = { $set: status }; - - let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); - - this.setState({ filesToUpload }, resolve); - }); - }, - - isDropzoneInactive() { - const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); - - if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { - return true; - } else { - return false; - } - }, - - getAllowedExtensions() { - let { validation } = this.props; - - if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) { - return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions); - } else { - return null; - } - }, - render() { const { multiple, diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js index 2ef96661..12d62944 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -3,19 +3,21 @@ import React from 'react'; import { History } from 'react-router'; +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; + import Form from '../../../../../ascribe_forms/form'; import Property from '../../../../../ascribe_forms/property'; import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable'; -import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button'; import InputFineuploader from '../../../../../ascribe_forms/input_fineuploader'; +import UploadButton from '../../../../../ascribe_uploader/ascribe_upload_button/upload_button'; + import AscribeSpinner from '../../../../../ascribe_spinner'; -import GlobalNotificationModel from '../../../../../../models/global_notification_model'; -import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; - -import AppConstants from '../../../../../../constants/application_constants'; import ApiUrls from '../../../../../../constants/api_urls'; +import AppConstants from '../../../../../../constants/application_constants'; +import { validationParts, validationTypes } from '../../../../../../constants/uploader_constants'; import requests from '../../../../../../utils/requests'; @@ -193,7 +195,7 @@ const PRRegisterPieceForm = React.createClass({ render() { const { location } = this.props; - const maxThumbnailSize = AppConstants.fineUploader.validation.workThumbnail.sizeLimit / 1000000; + const maxThumbnailSize = validationTypes.workThumbnail.sizeLimit / 1000000; return (
@@ -305,8 +307,8 @@ const PRRegisterPieceForm = React.createClass({ fileClass: 'digitalwork' }} validation={{ - itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit, + itemLimit: validationTypes.registerWork.itemLimit, + sizeLimit: validationTypes.additionalData.sizeLimit, allowedExtensions: ['pdf'] }} location={location} @@ -318,7 +320,7 @@ const PRRegisterPieceForm = React.createClass({ + label={`${getLangText('Featured Cover photo')} (max ${maxThumbnailSize}MB)`}> + submitFile={function () {}} + validation={{ + itemLimit: validationTypes.workThumbnail.itemLimit, + sizeLimit: validationTypes.workThumbnail.sizeLimit, + allowedExtensions: validationParts.allowedExtensions.images + }} /> 1 ? explodedFileName.pop() : ''; -} \ No newline at end of file +} diff --git a/sass/ascribe_global_notification.scss b/sass/ascribe_global_notification.scss index 5286a32c..dda29503 100644 --- a/sass/ascribe_global_notification.scss +++ b/sass/ascribe_global_notification.scss @@ -7,6 +7,8 @@ position: fixed; transition: .2s bottom cubic-bezier(.77, 0, .175, 1); width: 100%; + + z-index: 9999; } .ascribe-global-notification-off { @@ -37,6 +39,8 @@ right: -50em; transition: 1s right ease; + z-index: 9999; + > div { padding: .75em 1.5em; }