diff --git a/gulpfile.js b/gulpfile.js index c7816a3a..3c92945d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -189,17 +189,7 @@ function bundle(watch) { .pipe(gulpif(!argv.production, sourcemaps.write())) // writes .map file .on('error', notify.onError('Error: <%= error.message %>')) .pipe(gulpif(argv.production, uglify({ - mangle: true, - compress: { - sequences: true, - dead_code: true, - conditionals: true, - booleans: true, - unused: true, - if_return: true, - join_vars: true, - drop_console: true - } + mangle: true }))) .on('error', notify.onError('Error: <%= error.message %>')) .pipe(gulp.dest('./build/js')) diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index 307706ef..aaee33c6 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -12,8 +12,8 @@ class ContractListActions { ); } - fetchContractList() { - OwnershipFetcher.fetchContractList() + fetchContractList(isActive) { + OwnershipFetcher.fetchContractList(isActive) .then((contracts) => { this.actions.updateContractList(contracts.results); }) @@ -23,8 +23,8 @@ class ContractListActions { }); } - makeContractPublic(contract){ - contract.public = true; + + changeContract(contract){ return Q.Promise((resolve, reject) => { OwnershipFetcher.makeContractPublic(contract) .then((res) => { @@ -38,14 +38,12 @@ class ContractListActions { } removeContract(contractId){ - return Q.Promise((resolve, reject) => { + return Q.Promise( (resolve, reject) => { OwnershipFetcher.deleteContract(contractId) .then((res) => { - console.log('Contract deleted'); resolve(res); }) .catch( (err) => { - console.log('Error while deleting'); console.logGlobal(err); reject(err); }); diff --git a/js/actions/notification_actions.js b/js/actions/notification_actions.js new file mode 100644 index 00000000..9318c922 --- /dev/null +++ b/js/actions/notification_actions.js @@ -0,0 +1,68 @@ +'use strict'; + +import alt from '../alt'; +import Q from 'q'; + +import NotificationFetcher from '../fetchers/notification_fetcher'; + +class NotificationActions { + constructor() { + this.generateActions( + 'updatePieceListNotifications', + 'updateEditionListNotifications', + 'updateEditionNotifications', + 'updatePieceNotifications', + 'updateContractAgreementListNotifications' + ); + } + + fetchPieceListNotifications() { + NotificationFetcher + .fetchPieceListNotifications() + .then((res) => { + this.actions.updatePieceListNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchPieceNotifications(pieceId) { + NotificationFetcher + .fetchPieceNotifications(pieceId) + .then((res) => { + this.actions.updatePieceNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchEditionListNotifications() { + NotificationFetcher + .fetchEditionListNotifications() + .then((res) => { + this.actions.updateEditionListNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchEditionNotifications(editionId) { + NotificationFetcher + .fetchEditionNotifications(editionId) + .then((res) => { + this.actions.updateEditionNotifications(res); + }) + .catch((err) => console.logGlobal(err)); + } + + fetchContractAgreementListNotifications() { + return Q.Promise((resolve, reject) => { + NotificationFetcher + .fetchContractAgreementListNotifications() + .then((res) => { + this.actions.updateContractAgreementListNotifications(res); + resolve(res); + }) + .catch((err) => console.logGlobal(err)); + }); + } +} + +export default alt.createActions(NotificationActions); diff --git a/js/app.js b/js/app.js index 6dcf58bf..5ad36d28 100644 --- a/js/app.js +++ b/js/app.js @@ -26,6 +26,7 @@ import EventActions from './actions/event_actions'; import GoogleAnalyticsHandler from './third_party/ga'; import RavenHandler from './third_party/raven'; import IntercomHandler from './third_party/intercom'; +import NotificationsHandler from './third_party/notifications'; /* eslint-enable */ initLogging(); @@ -71,9 +72,8 @@ class AppGateway { type = settings.type; subdomain = settings.subdomain; } - EventActions.applicationWillBoot(settings); - Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => { + window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => { React.render( , document.getElementById('main') diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js index d1ab2112..350d61a8 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js @@ -160,7 +160,7 @@ let AccordionListItemTableEditions = React.createClass({ let content = item.acl; return { 'content': content, - 'requestAction': item.request_action + 'notifications': item.notifications }; }, 'acl', getLangText('Actions'), diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index 178a7db4..dde5c43d 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -61,8 +61,7 @@ let AccordionListItemWallet = React.createClass({ }, getGlyphicon(){ - if ((this.props.content.request_action && this.props.content.request_action.length > 0) || - (this.props.content.request_action_editions)){ + if ((this.props.content.notifications && this.props.content.notifications.length > 0)){ return ( - - - ); - } - return ( - - {this.props.text} - - ); - } -}); - -export default ButtonSubmitOrClose; diff --git a/js/components/ascribe_buttons/button_submit_close.js b/js/components/ascribe_buttons/button_submit_close.js deleted file mode 100644 index cdf0ebae..00000000 --- a/js/components/ascribe_buttons/button_submit_close.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -import React from 'react'; - -import AppConstants from '../../constants/application_constants'; -import { getLangText } from '../../utils/lang_utils.js'; - -let ButtonSubmitOrClose = React.createClass({ - propTypes: { - submitted: React.PropTypes.bool.isRequired, - text: React.PropTypes.string.isRequired, - onClose: React.PropTypes.func.isRequired - }, - - render() { - if (this.props.submitted){ - return ( - - - - ); - } - return ( - - {this.props.text} - {getLangText('CLOSE')} - - ); - } -}); - -export default ButtonSubmitOrClose; diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 49175851..47761e39 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -14,7 +14,7 @@ import CoaActions from '../../actions/coa_actions'; import CoaStore from '../../stores/coa_store'; import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; -import EditionListActions from '../../actions/edition_list_actions'; +import EditionListActions from '../../actions/edition_list_actions';; import HistoryIterator from './history_iterator'; @@ -234,13 +234,15 @@ let EditionSummary = React.createClass({ getActions(){ let actions = null; - if (this.props.edition.request_action && this.props.edition.request_action.length > 0){ + if (this.props.edition && + this.props.edition.notifications && + this.props.edition.notifications.length > 0){ actions = ( ); + notifications={this.props.edition.notifications}/>); } else { diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 78b1e477..0f726ae5 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -61,8 +61,7 @@ let EditionContainer = React.createClass({ }, render() { - console.log(this.state); - if('title' in this.state.edition) { + if(this.state.edition && this.state.edition.title) { return ( {return data.id; }).join() : null; + let otherDataIds = this.props.otherData ? this.props.otherData.map((data) => data.id).join() : null; return ( 0) { + this.state.piece.notifications && + this.state.piece.notifications.length > 0) { return ( - ); + notifications={this.state.piece.notifications}/>); } else { return ( @@ -206,7 +205,7 @@ let PieceContainer = React.createClass({ }, render() { - if('title' in this.state.piece) { + if(this.state.piece && this.state.piece.title) { return ( this[this.props.method](), 100); } else { throw new Error('This HTTP method is not supported by form.js (' + this.props.method + ')'); @@ -93,6 +100,20 @@ let Form = React.createClass({ .catch(this.handleError); }, + put() { + requests + .put(this.props.url, { body: this.getFormData() }) + .then(this.handleSuccess) + .catch(this.handleError); + }, + + patch() { + requests + .patch(this.props.url, { body: this.getFormData() }) + .then(this.handleSuccess) + .catch(this.handleError); + }, + delete() { requests .delete(this.props.url, this.getFormData()) @@ -100,13 +121,13 @@ let Form = React.createClass({ .catch(this.handleError); }, - getFormData(){ + getFormData() { let data = {}; - for (let ref in this.refs){ + for(let ref in this.refs){ data[this.refs[ref].props.name] = this.refs[ref].state.value; } - if ('getFormData' in this.props){ + if (this.props.getFormData && typeof this.props.getFormData === 'function'){ data = mergeOptionsWithDuplicates(data, this.props.getFormData()); } @@ -118,11 +139,12 @@ let Form = React.createClass({ }, handleSuccess(response){ - if ('handleSuccess' in this.props){ + if(this.props.handleSuccess && typeof this.props.handleSuccess === 'function') { this.props.handleSuccess(response); } - for (var ref in this.refs){ - if ('handleSuccess' in this.refs[ref]){ + + for(let ref in this.refs) { + if(this.refs[ref] && this.refs[ref].handleSuccess && typeof this.refs[ref].handleSuccess === 'function'){ this.refs[ref].handleSuccess(); } } @@ -134,9 +156,9 @@ let Form = React.createClass({ handleError(err){ if (err.json) { - for (var input in err.json.errors){ + for (let input in err.json.errors){ if (this.refs && this.refs[input] && this.refs[input].state) { - this.refs[input].setErrors( err.json.errors[input]); + this.refs[input].setErrors(err.json.errors[input]); } else { this.setState({errors: this.state.errors.concat(err.json.errors[input])}); } @@ -164,8 +186,8 @@ let Form = React.createClass({ }, clearErrors(){ - for (var ref in this.refs){ - if ('clearErrors' in this.refs[ref]){ + for(let ref in this.refs){ + if (this.refs[ref] && this.refs[ref].clearErrors && typeof this.refs[ref].clearErrors === 'function'){ this.refs[ref].clearErrors(); } } @@ -185,8 +207,16 @@ let Form = React.createClass({ buttons = ( - {this.props.buttonSubmitText} - CANCEL + + {this.props.buttonSubmitText} + + + CANCEL + ); @@ -251,6 +281,7 @@ let Form = React.createClass({ role="form" className={className} onSubmit={this.submit} + onReset={this.reset} autoComplete={this.props.autoComplete}> {this.getFakeAutocompletableInputs()} {this.getErrors()} diff --git a/js/components/ascribe_forms/form_contract_agreement.js b/js/components/ascribe_forms/form_contract_agreement.js index 1d9f7c72..0c20383c 100644 --- a/js/components/ascribe_forms/form_contract_agreement.js +++ b/js/components/ascribe_forms/form_contract_agreement.js @@ -35,7 +35,7 @@ let ContractAgreementForm = React.createClass({ componentDidMount() { ContractListStore.listen(this.onChange); - ContractListActions.fetchContractList(); + ContractListActions.fetchContractList({is_active: 'True'}); }, componentWillUnmount() { @@ -50,9 +50,11 @@ let ContractAgreementForm = React.createClass({ this.setState({selectedContract: event.target.selectedIndex}); }, - handleSubmitSuccess(response) { - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + handleSubmitSuccess() { + let notification = 'Contract agreement send'; + notification = new GlobalNotificationModel(notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); + this.refs.form.reset(); }, getFormData(){ @@ -60,8 +62,8 @@ let ContractAgreementForm = React.createClass({ }, getContracts() { - if (this.state.contractList && this.state.contractList.count > 0) { - let contractList = this.state.contractList.results; + if (this.state.contractList && this.state.contractList.length > 0) { + let contractList = this.state.contractList; return ( + value={ contract.id }> { contract.name } ); @@ -99,7 +101,7 @@ let ContractAgreementForm = React.createClass({ ref='form' url={ApiUrls.ownership_contract_agreements} getFormData={this.getFormData} - handleSuccess={this.props.handleSuccess} + handleSuccess={this.handleSubmitSuccess} buttons={ diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js index 488a2153..9cf97db7 100644 --- a/js/components/ascribe_forms/form_create_contract.js +++ b/js/components/ascribe_forms/form_create_contract.js @@ -16,6 +16,8 @@ import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; import AppConstants from '../../constants/application_constants'; import ApiUrls from '../../constants/api_urls'; + + import { getLangText } from '../../utils/lang_utils'; import { getCookie } from '../../utils/fetch_api_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; @@ -48,17 +50,17 @@ let CreateContractForm = React.createClass({ }, handleCreateSuccess(response) { + ContractListActions.fetchContractList({is_active: 'True'}); let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - - // also refresh contract lists for the rest of the contract settings page - ContractListActions.fetchContractList(); + this.refs.form.reset(); }, render() { return ( diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index 9fee79a0..44a24ad2 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -69,10 +69,11 @@ let LoanForm = React.createClass({ }, handleOnChange(event) { - let potentialEmail = event.target.value; - - if(potentialEmail.match(/.*@.*/)) { - ContractActions.fetchContract(potentialEmail); + // event.target.value is the submitted email of the loanee + if(event && event.target && event.target.value && event.target.value.match(/.*@.*/)) { + ContractActions.fetchContract(event.target.value); + } else { + ContractActions.flushContract(); } }, @@ -143,6 +144,7 @@ let LoanForm = React.createClass({ ref='form' url={this.props.url} getFormData={this.getFormData} + onReset={this.handleOnChange} handleSuccess={this.props.handleSuccess} buttons={this.getButtons()} spinner={ diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index c6a28626..8f2666c0 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -7,16 +7,13 @@ import UserActions from '../../actions/user_actions'; import Form from './form'; import Property from './property'; +import InputFineUploader from './input_fineuploader'; -import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; - -import AppConstants from '../../constants/application_constants'; import ApiUrls from '../../constants/api_urls'; -import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; import { mergeOptions } from '../../utils/general_utils'; -import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { isReadyForFormSubmission } from '../ascribe_uploader/react_s3_fine_uploader_utils'; let RegisterPieceForm = React.createClass({ @@ -45,7 +42,6 @@ let RegisterPieceForm = React.createClass({ getInitialState(){ return mergeOptions( { - digitalWorkKey: null, isUploadReady: false }, UserStore.getState() @@ -65,18 +61,6 @@ let RegisterPieceForm = React.createClass({ this.setState(state); }, - getFormData(){ - return { - digital_work_key: this.state.digitalWorkKey - }; - }, - - submitKey(key){ - this.setState({ - digitalWorkKey: key - }); - }, - setIsUploadReady(isReady) { this.setState({ isUploadReady: isReady @@ -94,14 +78,15 @@ let RegisterPieceForm = React.createClass({ className="ascribe-form-bordered" ref='form' url={ApiUrls.pieces_list} - getFormData={this.getFormData} handleSuccess={this.props.handleSuccess} - buttons={ - {this.props.submitMessage} - } + buttons={ + + {this.props.submitMessage} + + } spinner={ @@ -111,11 +96,11 @@ let RegisterPieceForm = React.createClass({ {this.props.headerMessage} - - ); - } -}); - export default RegisterPieceForm; diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js index 2448720a..b0f3b6c6 100644 --- a/js/components/ascribe_forms/form_request_action.js +++ b/js/components/ascribe_forms/form_request_action.js @@ -6,7 +6,7 @@ import AclButton from './../ascribe_buttons/acl_button'; import ActionPanel from '../ascribe_panel/action_panel'; import Form from './form'; -import PieceListActions from '../../actions/piece_list_actions'; +import NotificationActions from '../../actions/notification_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; @@ -22,8 +22,7 @@ let RequestActionForm = React.createClass({ React.PropTypes.object, React.PropTypes.array ]).isRequired, - requestAction: React.PropTypes.string, - requestUser: React.PropTypes.string, + notifications: React.PropTypes.object, currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func }, @@ -35,19 +34,19 @@ let RequestActionForm = React.createClass({ getUrls() { let urls = {}; - if (this.props.requestAction === 'consign'){ + if (this.props.notifications.action === 'consign'){ urls.accept = ApiUrls.ownership_consigns_confirm; urls.deny = ApiUrls.ownership_consigns_deny; - } else if (this.props.requestAction === 'unconsign'){ + } else if (this.props.notifications.action === 'unconsign'){ urls.accept = ApiUrls.ownership_unconsigns; urls.deny = ApiUrls.ownership_unconsigns_deny; - } else if (this.props.requestAction === 'loan' && !this.isPiece()){ + } else if (this.props.notifications.action === 'loan' && !this.isPiece()){ urls.accept = ApiUrls.ownership_loans_confirm; urls.deny = ApiUrls.ownership_loans_deny; - } else if (this.props.requestAction === 'loan' && this.isPiece()){ + } else if (this.props.notifications.action === 'loan' && this.isPiece()){ urls.accept = ApiUrls.ownership_loans_pieces_confirm; urls.deny = ApiUrls.ownership_loans_pieces_deny; - } else if (this.props.requestAction === 'loan_request' && this.isPiece()){ + } else if (this.props.notifications.action === 'loan_request' && this.isPiece()){ urls.accept = ApiUrls.ownership_loans_pieces_request_confirm; urls.deny = ApiUrls.ownership_loans_pieces_request_deny; } @@ -70,8 +69,8 @@ let RequestActionForm = React.createClass({ return () => { let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; - let notification = new GlobalNotificationModel(message, 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); + let notifications = new GlobalNotificationModel(message, 'success'); + GlobalNotificationActions.appendGlobalNotification(notifications); this.handleSuccess(); @@ -79,27 +78,27 @@ let RequestActionForm = React.createClass({ }, handleSuccess() { - PieceListActions.fetchPieceRequestActions(); + if (this.isPiece()){ + NotificationActions.fetchPieceListNotifications(); + } + else { + NotificationActions.fetchEditionListNotifications(); + } if(this.props.handleSuccess) { this.props.handleSuccess(); } }, getContent() { - let pieceOrEditionStr = this.isPiece() ? getLangText('this work%s', '.') : getLangText('this edition%s', '.'); - let message = this.props.requestUser + ' ' + getLangText('requests you') + ' ' + this.props.requestAction + ' ' + pieceOrEditionStr; - if (this.props.requestAction === 'loan_request'){ - message = this.props.requestUser + ' ' + getLangText('requests you to loan') + ' ' + pieceOrEditionStr; - } return ( - {message} + {this.props.notifications.action_str + ' by ' + this.props.notifications.by} ); }, getAcceptButtonForm(urls) { - if(this.props.requestAction === 'unconsign') { + if(this.props.notifications.action === 'unconsign') { return ( ); - } else if(this.props.requestAction === 'loan_request') { + } else if(this.props.notifications.action === 'loan_request') { return ( @@ -151,7 +150,7 @@ let RequestActionForm = React.createClass({ isInline={true} getFormData={this.getFormData} handleSuccess={ - this.showNotification(getLangText('denied'), this.props.requestAction, this.props.requestUser) + this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by) } className='inline pull-right'> diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js new file mode 100644 index 00000000..52e1d5b5 --- /dev/null +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -0,0 +1,95 @@ +'use strict'; + +import React from 'react'; + +import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; + +import AppConstants from '../../constants/application_constants'; +import ApiUrls from '../../constants/api_urls'; + +import { getCookie } from '../../utils/fetch_api_utils'; + +let InputFileUploader = React.createClass({ + propTypes: { + setIsUploadReady: React.PropTypes.func, + isReadyForFormSubmission: React.PropTypes.func, + onClick: React.PropTypes.func, + + // isFineUploaderActive is used to lock react fine uploader in case + // a user is actually not logged in already to prevent him from droping files + // before login in + isFineUploaderActive: React.PropTypes.bool, + onLoggedOut: React.PropTypes.func, + editable: React.PropTypes.bool, + enableLocalHashing: React.PropTypes.bool, + + // provided by Property + disabled: React.PropTypes.bool + }, + + getInitialState() { + return { + value: null + }; + }, + + submitKey(key){ + this.setState({ + value: key + }); + }, + + reset() { + this.refs.fineuploader.reset(); + }, + + render() { + let editable = this.props.isFineUploaderActive; + + // if disabled is actually set by property, we want to override + // isFineUploaderActive + if(typeof this.props.disabled !== 'undefined') { + editable = !this.props.disabled; + } + + return ( + + ); + } +}); + +export default InputFileUploader; \ No newline at end of file diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index bc70c530..ac3994a7 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -4,6 +4,7 @@ import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; + let InputTextAreaToggable = React.createClass({ propTypes: { editable: React.PropTypes.bool.isRequired, @@ -17,14 +18,17 @@ let InputTextAreaToggable = React.createClass({ value: this.props.defaultValue }; }, + handleChange(event) { this.setState({value: event.target.value}); this.props.onChange(event); }, + render() { let className = 'form-control ascribe-textarea'; let textarea = null; - if (this.props.editable){ + + if(this.props.editable) { className = className + ' ascribe-textarea-editable'; textarea = ( ); - } - else{ + } else { textarea = {this.state.value}; } + return textarea; } }); diff --git a/js/components/ascribe_forms/list_form_request_actions.js b/js/components/ascribe_forms/list_form_request_actions.js index 429a4944..082ae8ef 100644 --- a/js/components/ascribe_forms/list_form_request_actions.js +++ b/js/components/ascribe_forms/list_form_request_actions.js @@ -12,20 +12,19 @@ let ListRequestActions = React.createClass({ ]).isRequired, currentUser: React.PropTypes.object.isRequired, handleSuccess: React.PropTypes.func.isRequired, - requestActions: React.PropTypes.array.isRequired + notifications: React.PropTypes.array.isRequired }, render () { - if (this.props.requestActions && - this.props.requestActions.length > 0) { + if (this.props.notifications && + this.props.notifications.length > 0) { return ( - {this.props.requestActions.map((requestAction) => + {this.props.notifications.map((notification) => )} ); diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index cfc4c4d9..0bc431b0 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -29,8 +29,11 @@ let Property = React.createClass({ handleChange: React.PropTypes.func, ignoreFocus: React.PropTypes.bool, className: React.PropTypes.string, + onClick: React.PropTypes.func, onChange: React.PropTypes.func, + onBlur: React.PropTypes.func, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -90,18 +93,28 @@ let Property = React.createClass({ // maybe do reset by reload instead of front end state? this.setState({value: this.state.initialValue}); - // resets the value of a custom react component input - this.refs.input.state.value = this.state.initialValue; + if (this.refs.input.state && this.refs.input.state.value) { + // resets the value of a custom react component input + this.refs.input.state.value = this.state.initialValue; + } // resets the value of a plain HTML5 input this.refs.input.getDOMNode().value = this.state.initialValue; + // For some inputs, reseting state.value is not enough to visually reset the + // component. + // + // So if the input actually needs a visual reset, it needs to implement + // a dedicated reset method. + if(this.refs.input.reset && typeof this.refs.input.reset === 'function') { + this.refs.input.reset(); + } }, handleChange(event) { this.props.handleChange(event); - if ('onChange' in this.props) { + if (this.props.onChange && typeof this.props.onChange === 'function') { this.props.onChange(event); } @@ -117,7 +130,7 @@ let Property = React.createClass({ // if onClick is defined from the outside, // just call it - if(this.props.onClick) { + if(this.props.onClick && typeof this.props.onClick === 'function') { this.props.onClick(); } @@ -132,7 +145,7 @@ let Property = React.createClass({ isFocused: false }); - if(this.props.onBlur) { + if(this.props.onBlur && typeof this.props.onBlur === 'function') { this.props.onBlur(event); } }, @@ -190,6 +203,7 @@ let Property = React.createClass({ }, render() { + let footer = null; let tooltip = ; let style = this.props.style ? mergeOptions({}, this.props.style) : {}; @@ -199,7 +213,7 @@ let Property = React.createClass({ {this.props.tooltip} ); } - let footer = null; + if(this.props.footer){ footer = ( diff --git a/js/components/ascribe_forms/property_collapsible.js b/js/components/ascribe_forms/property_collapsible.js index 03ec404d..ef9a1329 100644 --- a/js/components/ascribe_forms/property_collapsible.js +++ b/js/components/ascribe_forms/property_collapsible.js @@ -42,6 +42,13 @@ let PropertyCollapsile = React.createClass({ } }, + reset() { + // If the child input is a native HTML element, it will be reset automatically + // by the DOM. + // However, we need to collapse this component again. + this.setState(this.getInitialState()); + }, + render() { let tooltip = ; if (this.props.tooltip){ diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index ad53b61f..e4b69c69 100644 --- a/js/components/ascribe_media/media_player.js +++ b/js/components/ascribe_media/media_player.js @@ -28,12 +28,20 @@ let Other = React.createClass({ }, render() { - let ext = this.props.url.split('.').pop(); + let filename = this.props.url.split('/').pop(); + let tokens = filename.split('.'); + let preview; + + if (tokens.length > 1) { + preview = '.' + tokens.pop(); + } else { + preview = 'file'; + } return ( - .{ext} + {preview} ); diff --git a/js/components/ascribe_prizes_dashboard/prizes_dashboard.js b/js/components/ascribe_prizes_dashboard/prizes_dashboard.js deleted file mode 100644 index b4c695f4..00000000 --- a/js/components/ascribe_prizes_dashboard/prizes_dashboard.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -import React from 'react'; - -import PrizeListActions from '../../actions/prize_list_actions'; -import PrizeListStore from '../../stores/prize_list_store'; - -import Table from '../ascribe_table/table'; -import TableItem from '../ascribe_table/table_item'; -import TableItemText from '../ascribe_table/table_item_text'; - -import { ColumnModel} from '../ascribe_table/models/table_models'; -import { getLangText } from '../../utils/lang_utils'; - -let PrizesDashboard = React.createClass({ - - getInitialState() { - return PrizeListStore.getState(); - }, - - componentDidMount() { - PrizeListStore.listen(this.onChange); - PrizeListActions.fetchPrizeList(); - }, - - componentWillUnmount() { - PrizeListStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - getColumnList() { - return [ - new ColumnModel( - (item) => { - return { - 'content': item.name - }; }, - 'name', - getLangText('Name'), - TableItemText, - 6, - false, - null - ), - new ColumnModel( - (item) => { - return { - 'content': item.domain - }; }, - 'domain', - getLangText('Domain'), - TableItemText, - 1, - false, - null - ) - ]; - }, - - render() { - return ( - - {this.state.prizeList.map((item, i) => { - return ( - - ); - })} - - ); - } -}); - -export default PrizesDashboard; \ No newline at end of file diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index e34772f2..278c4c1b 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -26,7 +26,7 @@ let ContractSettings = React.createClass({ componentDidMount() { ContractListStore.listen(this.onChange); - ContractListActions.fetchContractList(); + ContractListActions.fetchContractList({is_active: 'True'}); }, componentWillUnmount() { @@ -39,32 +39,42 @@ let ContractSettings = React.createClass({ makeContractPublic(contract) { return () => { - ContractListActions.makeContractPublic(contract) - .then(() => ContractListActions.fetchContractList()) - .catch((error) => { - let notification = new GlobalNotificationModel(error, 'success', 10000); + contract.is_public = true; + ContractListActions.changeContract(contract) + .then(() => { + ContractListActions.fetchContractList({is_active: 'True'}); + let notification = getLangText('Contract %s is now public', contract.name); + notification = new GlobalNotificationModel(notification, 'success', 4000); GlobalNotificationActions.appendGlobalNotification(notification); - }); + }) + .catch((err) => { + let notification = new GlobalNotificationModel(err, 'danger', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); }; }, removeContract(contract) { return () => { ContractListActions.removeContract(contract.id) - .then(( ) => ContractListActions.fetchContractList()) - .catch((error) => { - let notification = new GlobalNotificationModel(error, 'danger', 10000); + .then((response) => { + ContractListActions.fetchContractList({is_active: 'True'}); + let notification = new GlobalNotificationModel(response.notification, 'success', 4000); GlobalNotificationActions.appendGlobalNotification(notification); - }); + }) + .catch((err) => { + let notification = new GlobalNotificationModel(err, 'danger', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); }; }, getPublicContracts(){ - return this.state.contractList.filter((contract) => contract.public); + return this.state.contractList.filter((contract) => contract.is_public); }, getPrivateContracts(){ - return this.state.contractList.filter((contract) => !contract.public); + return this.state.contractList.filter((contract) => !contract.is_public); }, render() { @@ -95,8 +105,9 @@ let ContractSettings = React.createClass({ UPDATE - + REMOVE @@ -121,12 +132,14 @@ let ContractSettings = React.createClass({ UPDATE - + REMOVE - + MAKE PUBLIC diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 4ac4d57a..84dff61c 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -4,13 +4,12 @@ import React from 'react'; import Router from 'react-router'; import ReactAddons from 'react/addons'; -import Col from 'react-bootstrap/lib/Col'; - import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs'; let State = Router.State; let Navigation = Router.Navigation; + let SlidesContainer = React.createClass({ propTypes: { children: React.PropTypes.arrayOf(React.PropTypes.element), @@ -30,12 +29,15 @@ let SlidesContainer = React.createClass({ let slideNum = -1; let startFrom = -1; + // We can actually need to check if slide_num is present as a key in queryParams. + // We do not really care about its value though... if(queryParams && 'slide_num' in queryParams) { slideNum = parseInt(queryParams.slide_num, 10); } // if slide_num is not set, this will be done in componentDidMount // the query param 'start_from' removes all slide children before the respective number + // Also, we use the 'in' keyword for the same reason as above in 'slide_num' if(queryParams && 'start_from' in queryParams) { startFrom = parseInt(queryParams.start_from, 10); } @@ -51,6 +53,9 @@ let SlidesContainer = React.createClass({ componentDidMount() { // check if slide_num was defined, and if not then default to 0 let queryParams = this.getQuery(); + + // We use 'in' to check if the key is present in the user's browser url bar, + // we do not really care about its value at this point if(!('slide_num' in queryParams)) { // we're first requiring all the other possible queryParams and then set @@ -241,6 +246,16 @@ let SlidesContainer = React.createClass({ }, render() { + let spacing = this.state.containerWidth * this.state.slideNum; + let translateXValue = 'translateX(' + (-1) * spacing + 'px)'; + + /* + According to the react documentation, + all browser vendor prefixes need to be upper cases in the beginning except for + the Microsoft one *bigfuckingsurprise* + https://facebook.github.io/react/tips/inline-styles.html + */ + return ( {this.renderChildren()} diff --git a/js/components/ascribe_table/table_item_acl_filtered.js b/js/components/ascribe_table/table_item_acl_filtered.js index c850ab59..22a28130 100644 --- a/js/components/ascribe_table/table_item_acl_filtered.js +++ b/js/components/ascribe_table/table_item_acl_filtered.js @@ -6,15 +6,15 @@ import React from 'react'; let TableItemAclFiltered = React.createClass({ propTypes: { content: React.PropTypes.object, - requestAction: React.PropTypes.string + notifications: React.PropTypes.string }, render() { var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; - if (this.props.requestAction && this.props.requestAction.length > 0){ + if (this.props.notifications && this.props.notifications.length > 0){ return ( - {this.props.requestAction[0].action + ' request pending'} + {this.props.notifications[0].action_str} ); } diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index dc17f665..6f0d7a90 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -20,7 +20,6 @@ import AppConstants from '../../constants/application_constants'; import { computeHashOfFile } from '../../utils/file_utils'; var ReactS3FineUploader = React.createClass({ - propTypes: { keyRoutine: React.PropTypes.shape({ url: React.PropTypes.string, @@ -125,6 +124,7 @@ var ReactS3FineUploader = React.createClass({ bucket: 'ascribe0' }, request: { + //endpoint: 'https://www.ascribe.io.global.prod.fastly.net', endpoint: 'https://ascribe0.s3.amazonaws.com', accessKey: 'AKIAIVCZJ33WSCBQ3QDA' }, @@ -235,6 +235,21 @@ var ReactS3FineUploader = React.createClass({ }; }, + // Resets the whole react fineuploader component to its initial state + reset() { + // Cancel all currently ongoing uploads + this.state.uploader.cancelAll(); + + // and reset component in general + this.state.uploader.reset(); + + // proclaim that upload is not ready + this.props.setIsUploadReady(false); + + // reset internal data structures of component + this.setState(this.getInitialState()); + }, + requestKey(fileId) { let filename = this.state.uploader.getName(fileId); let uuid = this.state.uploader.getUuid(fileId); @@ -356,64 +371,59 @@ var ReactS3FineUploader = React.createClass({ onComplete(id, name, res, xhr) { // there has been an issue with the server's connection - if(xhr.status === 0) { - - console.logGlobal(new Error('Complete was called but there wasn\t a success'), false, { + if((xhr && xhr.status === 0) || res.error) { + console.logGlobal(new Error(res.error || 'Complete was called but there wasn\t a success'), false, { files: this.state.filesToUpload, chunks: this.state.chunks }); + } else { + let files = this.state.filesToUpload; - return; - } + // Set the state of the completed file to 'upload successful' in order to + // remove it from the GUI + files[id].status = 'upload successful'; + files[id].key = this.state.uploader.getKey(id); - let files = this.state.filesToUpload; - - // Set the state of the completed file to 'upload successful' in order to - // remove it from the GUI - files[id].status = 'upload successful'; - files[id].key = this.state.uploader.getKey(id); - - let newState = React.addons.update(this.state, { - filesToUpload: { $set: files } - }); - - this.setState(newState); - - // Only after the blob has been created server-side, we can make the form submittable. - this.createBlob(files[id]) - .then(() => { - // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey - // are optional, we'll only trigger them when they're actually defined - if(this.props.submitKey) { - this.props.submitKey(files[id].key); - } else { - console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader'); - } - - // for explanation, check comment of if statement above - if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { - // also, lets check if after the completion of this upload, - // the form is ready for submission or not - if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { - // if so, set uploadstatus to true - this.props.setIsUploadReady(true); - } else { - this.props.setIsUploadReady(false); - } - } else { - console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); - } - }) - .catch((err) => { - console.logGlobal(err, false, { - files: this.state.filesToUpload, - chunks: this.state.chunks - }); - let notification = new GlobalNotificationModel(err.message, 'danger', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); + let newState = React.addons.update(this.state, { + filesToUpload: { $set: files } }); - + this.setState(newState); + + // Only after the blob has been created server-side, we can make the form submittable. + this.createBlob(files[id]) + .then(() => { + // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey + // are optional, we'll only trigger them when they're actually defined + if(this.props.submitKey) { + this.props.submitKey(files[id].key); + } else { + console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader'); + } + + // for explanation, check comment of if statement above + if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { + // also, lets check if after the completion of this upload, + // the form is ready for submission or not + if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { + // if so, set uploadstatus to true + this.props.setIsUploadReady(true); + } else { + this.props.setIsUploadReady(false); + } + } else { + console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); + } + }) + .catch((err) => { + console.logGlobal(err, false, { + files: this.state.filesToUpload, + chunks: this.state.chunks + }); + let notification = new GlobalNotificationModel(err.message, 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); + } }, onError(id, name, errorReason) { @@ -442,7 +452,6 @@ var ReactS3FineUploader = React.createClass({ }, onCancel(id) { - // when a upload is canceled, we need to update this components file array this.setStatusOfFile(id, 'canceled'); @@ -464,7 +473,6 @@ var ReactS3FineUploader = React.createClass({ }, onProgress(id, name, uploadedBytes, totalBytes) { - let newState = React.addons.update(this.state, { filesToUpload: { [id]: { progress: { $set: (uploadedBytes / totalBytes) * 100} } diff --git a/js/components/contract_notification.js b/js/components/contract_notification.js new file mode 100644 index 00000000..cd6ceb53 --- /dev/null +++ b/js/components/contract_notification.js @@ -0,0 +1,36 @@ +'use strict'; + +import React from 'react'; + +import NotificationStore from '../stores/notification_store'; + +import { mergeOptions } from '../utils/general_utils'; + +let ContractNotification = React.createClass({ + getInitialState() { + return mergeOptions( + NotificationStore.getState() + ); + }, + + componentDidMount() { + NotificationStore.listen(this.onChange); + }, + + componentWillUnmount() { + NotificationStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + + return ( + null + ); + } +}); + +export default ContractNotification; \ No newline at end of file diff --git a/js/components/header.js b/js/components/header.js index 0ae18751..0309b748 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -96,6 +96,31 @@ let Header = React.createClass({ } }, + onMenuItemClick(event) { + /* + This is a hack to make the dropdown close after clicking on an item + The function just need to be defined + + from https://github.com/react-bootstrap/react-bootstrap/issues/368: + + @jvillasante - Have you tried to use onSelect with the DropdownButton? + I don't have a working example that is exactly like yours, + but I just noticed that the Dropdown closes when I've attached an event handler to OnSelect: + + + + onSelected: function(e) { + // doesn't need to have functionality (necessarily) ... just wired up + } + Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu. + So, you should be able to call that directly on the DropdownButton instance as well if needed. + + NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases + Hence, we do this manually + */ + this.refs.dropdownbutton.setDropdownState(false); + }, + render() { let account; let signup; @@ -103,9 +128,15 @@ let Header = React.createClass({ if (this.state.currentUser.username){ account = ( - {getLangText('Account Settings')} + + {getLangText('Account Settings')} + {getLangText('Log out')} diff --git a/js/components/header_notification.js b/js/components/header_notification.js index 9222c0c4..67252af8 100644 --- a/js/components/header_notification.js +++ b/js/components/header_notification.js @@ -8,7 +8,8 @@ import MenuItem from 'react-bootstrap/lib/MenuItem'; import Nav from 'react-bootstrap/lib/Nav'; -import PieceListStore from '../stores/piece_list_store'; +import NotificationActions from '../actions/notification_actions'; +import NotificationStore from '../stores/notification_store'; import { mergeOptions } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; @@ -20,23 +21,25 @@ let HeaderNotifications = React.createClass({ getInitialState() { return mergeOptions( - PieceListStore.getState() + NotificationStore.getState() ); }, componentDidMount() { - PieceListStore.listen(this.onChange); + NotificationStore.listen(this.onChange); + NotificationActions.fetchPieceListNotifications(); + NotificationActions.fetchEditionListNotifications(); }, componentWillUnmount() { - PieceListStore.unlisten(this.onChange); + NotificationStore.unlisten(this.onChange); }, onChange(state) { this.setState(state); }, - onSelected(event) { + onMenuItemClick(event) { /* This is a hack to make the dropdown close after clicking on an item The function just need to be defined @@ -54,32 +57,87 @@ let HeaderNotifications = React.createClass({ } Internally, a call to DropdownButton.setDropDownState(false) is made which will hide the dropdown menu. So, you should be able to call that directly on the DropdownButton instance as well if needed. + + NOW, THAT DIDN'T WORK - the onSelect routine isnt triggered in all cases + Hence, we do this manually */ + this.refs.dropdownbutton.setDropdownState(false); + }, + + getPieceNotifications(){ + if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { + return ( + + + Artworks ({this.state.pieceListNotifications.length}) + + {this.state.pieceListNotifications.map((pieceNotification, i) => { + return ( + + + + ); + } + )} + + ); + } + return null; + }, + + getEditionNotifications(){ + if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { + return ( + + + Editions ({this.state.editionListNotifications.length}) + + {this.state.editionListNotifications.map((editionNotification, i) => { + return ( + + + + ); + } + )} + + ); + } + return null; }, render() { - if (this.state.requestActions && this.state.requestActions.length > 0) { + if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) || + (this.state.editionListNotifications && this.state.editionListNotifications.length > 0)){ + let numNotifications = 0; + if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { + numNotifications += this.state.pieceListNotifications.length; + } + if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { + numNotifications += this.state.editionListNotifications.length; + } return ( - ({this.state.requestActions.length}) + ({numNotifications}) } - className="notification-menu" - onSelect={this.onSelected}> - {this.state.requestActions.map((pieceOrEdition, i) => { - return ( - - - ); - } - )} + className="notification-menu"> + {this.getPieceNotifications()} + {this.getEditionNotifications()} ); @@ -90,33 +148,55 @@ let HeaderNotifications = React.createClass({ let NotificationListItem = React.createClass({ propTypes: { - pieceOrEdition: React.PropTypes.object + notification: React.PropTypes.array, + pieceOrEdition: React.PropTypes.object, + onClick: React.PropTypes.func + }, + + isPiece() { + return !(this.props.pieceOrEdition && this.props.pieceOrEdition.parent); }, getLinkData() { - if(this.props.pieceOrEdition && this.props.pieceOrEdition.parent) { - return { - to: 'edition', - params: { - editionId: this.props.pieceOrEdition.bitcoin_id - } - }; - } else { + if (this.isPiece()) { return { to: 'piece', params: { pieceId: this.props.pieceOrEdition.id } }; + } else { + return { + to: 'edition', + params: { + editionId: this.props.pieceOrEdition.bitcoin_id + } + }; } }, + onClick(event){ + this.props.onClick(event); + }, + + getNotificationText(){ + let numNotifications = null; + if (this.props.notification.length > 1){ + numNotifications = + {this.props.notification.length - 1} more...; + } + return ( + + {this.props.notification[0].action_str} + {numNotifications} + ); + }, + render() { if (this.props.pieceOrEdition) { return ( - + @@ -126,13 +206,7 @@ let NotificationListItem = React.createClass({ {this.props.pieceOrEdition.title} by {this.props.pieceOrEdition.artist_name} - - { - this.props.pieceOrEdition.request_action.map((requestAction) => { - return 'Pending ' + requestAction.action + ' request'; - }) - } - + {this.getNotificationText()} ); diff --git a/js/components/piece_list.js b/js/components/piece_list.js index d841f7af..71304a63 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -65,8 +65,7 @@ let PieceList = React.createClass({ let orderBy = this.props.orderBy ? this.props.orderBy : this.state.orderBy; if (this.state.pieceList.length === 0 || this.state.page !== page){ PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, - orderBy, this.state.orderAsc, this.state.filterBy) - .then(() => PieceListActions.fetchPieceRequestActions()); + orderBy, this.state.orderAsc, this.state.filterBy); } }, diff --git a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js index 9e927ac4..406b5827 100644 --- a/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/components/ascribe_detail/prize_piece_container.js @@ -88,15 +88,37 @@ let PieceContainer = React.createClass({ PieceActions.fetchOne(this.props.params.pieceId); }, + getActions() { + if (this.state.piece && + this.state.piece.notifications && + this.state.piece.notifications.length > 0) { + return ( + ); + } + }, + render() { - if('title' in this.state.piece) { + if(this.state.piece && this.state.piece.title) { + /* + + This really needs a refactor! + + - Tim + + */ // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted let artistName = ((this.state.currentUser.is_jury && !this.state.currentUser.is_judge) || (this.state.currentUser.is_judge && !this.state.piece.selected )) ? : this.state.piece.artist_name; + // Only show the artist email if you are a judge and the piece is shortlisted let artistEmail = (this.state.currentUser.is_judge && this.state.piece.selected ) ? : null; + return ( {artistEmail} - + {this.getActions()} } diff --git a/js/components/whitelabel/wallet/components/cyland/ascribe_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/ascribe_detail/cyland_piece_container.js index 75ccab10..395cd86e 100644 --- a/js/components/whitelabel/wallet/components/cyland/ascribe_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/ascribe_detail/cyland_piece_container.js @@ -68,7 +68,7 @@ let CylandPieceContainer = React.createClass({ }, render() { - if('title' in this.state.piece) { + if(this.state.piece && this.state.piece.title) { return ( {' ' + getLangText('I agree to the Terms of Service of IkonoTV Archive') + ' '} - ( + ( {getLangText('read')} ) diff --git a/js/components/whitelabel/wallet/components/ikonotv/ascribe_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ascribe_detail/ikonotv_piece_container.js index 7f094a26..3f84ded7 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ascribe_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ascribe_detail/ikonotv_piece_container.js @@ -89,15 +89,14 @@ let IkonotvPieceContainer = React.createClass({ getActions(){ if (this.state.piece && - this.state.piece.request_action && - this.state.piece.request_action.length > 0) { + this.state.piece.notifications && + this.state.piece.notifications.length > 0) { return ( - ); + notifications={this.state.piece.notifications}/>); } else { @@ -133,7 +132,7 @@ let IkonotvPieceContainer = React.createClass({ }, render() { - if('title' in this.state.piece) { + if(this.state.piece && this.state.piece.title) { return ( + + + + + + Download PDF version + + + + + ); + } + return ( + + + + + Download contract + + + + ); + }, + + getAppendix() { + let notifications = this.state.contractAgreementListNotifications[0]; + let appendix = notifications.contract_agreement.appendix; + if (appendix) { + return ( + {getLangText('Appendix')} + + {appendix.default} + + + ); + } + return null; + }, + + handleConfirmSuccess() { + let notification = new GlobalNotificationModel(getLangText('You have accepted the conditions'), 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + this.transitionTo('pieces'); + }, + + handleDeny() { + let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; + requests.put(apiUrls.ownership_contract_agreements_deny, {contract_agreement_id: contractAgreement.id}).then( + () => this.handleDenySuccess() + ); + }, + + handleDenySuccess() { + let notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + this.transitionTo('pieces'); + }, + + render() { + + if (this.state.contractAgreementListNotifications && + this.state.contractAgreementListNotifications.length > 0) { + let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; + return ( + + + + + + {getLangText('Production Contract')} + + + {this.getContract()} + + + {this.getAppendix} + {getLangText('Are you a member of any copyright societies?')} + + ARS, DACS, Bildkunst, Pictoright, SODRAC, Copyright Agency/Viscopy, SAVA, Bildrecht GmbH, + SABAM, AUTVIS, CREAIMAGEN, SONECA, Copydan, EAU, Kuvasto, GCA, HUNGART, IVARO, SIAE, JASPAR-SPDA, + AKKA/LAA, LATGA-A, SOMAAP, ARTEGESTION, CARIER, BONO, APSAV, SPA, GESTOR, VISaRTA, RAO, LITA, + DALRO, VeGaP, BUS, ProLitteris, AGADU, AUTORARTE, BUBEDRA, BBDA, BCDA, BURIDA, ADAVIS, BSDA + + + {getLangText('I agree with the conditions')} + + {getLangText('I disagree')} + + + }> + + + + {' ' + getLangText('Yes') } + + + + + + + + + ); + } + return null; + } +}); + +export default IkonotvContractNotifications; \ No newline at end of file diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index f6723a55..f333e6bb 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -22,6 +22,7 @@ import CylandPieceList from './components/cyland/cyland_piece_list'; import IkonotvPieceList from './components/ikonotv/ikonotv_piece_list'; import IkonotvRequestLoan from './components/ikonotv/ikonotv_request_loan'; import IkonotvPieceContainer from './components/ikonotv/ascribe_detail/ikonotv_piece_container'; +import IkonotvContractNotifications from './components/ikonotv/ikonotv_contract_notifications'; import CCRegisterPiece from './components/cc/cc_register_piece'; @@ -77,6 +78,7 @@ let ROUTES = { + ) }; diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index 5af07ba6..b8d6713d 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -27,7 +27,14 @@ let ApiUrls = { 'note_private_piece': AppConstants.apiEndpoint + 'note/private/pieces/', 'note_public_edition': AppConstants.apiEndpoint + 'note/public/editions/', 'note_public_piece': AppConstants.apiEndpoint + 'note/public/pieces/', + 'notification_piecelist': AppConstants.apiEndpoint + 'notifications/pieces/', + 'notification_piece': AppConstants.apiEndpoint + 'notifications/pieces/${piece_id}/', + 'notification_editionlist': AppConstants.apiEndpoint + 'notifications/editions/', + 'notification_edition': AppConstants.apiEndpoint + 'notifications/editions/${edition_id}/', + 'notification_contractagreementlist': AppConstants.apiEndpoint + 'notifications/contract_agreements/', 'ownership_contract_agreements': AppConstants.apiEndpoint + 'ownership/contract_agreements/', + 'ownership_contract_agreements_confirm': AppConstants.apiEndpoint + 'ownership/contract_agreements/${contract_agreement_id}/accept/', + 'ownership_contract_agreements_deny': AppConstants.apiEndpoint + 'ownership/contract_agreements/${contract_agreement_id}/reject/', 'ownership_consigns': AppConstants.apiEndpoint + 'ownership/consigns/', 'ownership_consigns_confirm': AppConstants.apiEndpoint + 'ownership/consigns/confirm/', 'ownership_consigns_deny': AppConstants.apiEndpoint + 'ownership/consigns/deny/', @@ -53,7 +60,6 @@ let ApiUrls = { 'piece_extradata': AppConstants.apiEndpoint + 'pieces/${piece_id}/extradata/', 'piece_first_edition_id': AppConstants.apiEndpoint + 'pieces/${piece_id}/edition_index/', 'pieces_list': AppConstants.apiEndpoint + 'pieces/', - 'pieces_list_request_actions': AppConstants.apiEndpoint + 'pieces/request_actions/', 'piece_remove_from_collection': AppConstants.apiEndpoint + 'ownership/shares/pieces/${piece_id}/', 'user': AppConstants.apiEndpoint + 'users/', 'users_login': AppConstants.apiEndpoint + 'users/login/', diff --git a/js/fetchers/notification_fetcher.js b/js/fetchers/notification_fetcher.js new file mode 100644 index 00000000..48606b70 --- /dev/null +++ b/js/fetchers/notification_fetcher.js @@ -0,0 +1,29 @@ +'use strict'; + +import requests from '../utils/requests'; + + +let NotificationFetcher = { + + fetchPieceListNotifications() { + return requests.get('notification_piecelist'); + }, + + fetchPieceNotifications(pieceId) { + return requests.get('notification_piece', {'piece_id': pieceId}); + }, + + fetchEditionListNotifications() { + return requests.get('notification_editionlist'); + }, + + fetchEditionNotifications(editionId) { + return requests.get('notification_edition', {'edition_id': editionId}); + }, + + fetchContractAgreementListNotifications() { + return requests.get('notification_contractagreementlist'); + } +}; + +export default NotificationFetcher; diff --git a/js/fetchers/ownership_fetcher.js b/js/fetchers/ownership_fetcher.js index 07b37bf5..c0d32d71 100644 --- a/js/fetchers/ownership_fetcher.js +++ b/js/fetchers/ownership_fetcher.js @@ -15,8 +15,8 @@ let OwnershipFetcher = { /** * Fetch the contracts of the logged-in user from the API. */ - fetchContractList(){ - return requests.get(ApiUrls.ownership_contract_list); + fetchContractList(isActive){ + return requests.get(ApiUrls.ownership_contract_list, isActive); }, fetchLoanPieceRequestList(){ @@ -24,11 +24,11 @@ let OwnershipFetcher = { }, makeContractPublic(contractObj){ - return requests.put('ownership_csontract', { body: contractObj, contract_id: contractObj.id }); + return requests.put(ApiUrls.ownership_contract, { body: contractObj, contract_id: contractObj.id }); }, deleteContract(contractObjId){ - return requests.delete('ownership_contract', {contract_id: contractObjId}); + return requests.delete(ApiUrls.ownership_contract, {contract_id: contractObjId}); } }; diff --git a/js/routes.js b/js/routes.js index 2762052b..1d521cab 100644 --- a/js/routes.js +++ b/js/routes.js @@ -21,8 +21,7 @@ import SettingsContainer from './components/ascribe_settings/settings_container' import CoaVerifyContainer from './components/coa_verify_container'; import RegisterPiece from './components/register_piece'; - -import PrizesDashboard from './components/ascribe_prizes_dashboard/prizes_dashboard'; +import ContractNotification from './components/contract_notification'; import AppConstants from './constants/application_constants'; @@ -45,7 +44,7 @@ const COMMON_ROUTES = ( - + ); diff --git a/js/stores/notification_store.js b/js/stores/notification_store.js new file mode 100644 index 00000000..9f6bdecf --- /dev/null +++ b/js/stores/notification_store.js @@ -0,0 +1,41 @@ +'use strict'; + +import React from 'react'; +import alt from '../alt'; + +import NotificationActions from '../actions/notification_actions'; + + +class NotificationStore { + constructor() { + this.pieceListNotifications = {}; + this.editionListNotifications = {}; + this.contractAgreementListNotifications = null; + this.editionNotifications = null; + this.pieceNotifications = null; + this.bindActions(NotificationActions); + } + + onUpdatePieceListNotifications(res) { + this.pieceListNotifications = res.notifications; + } + + onUpdatePieceNotifications(res) { + this.pieceNotifications = res.notification; + } + + onUpdateEditionListNotifications(res) { + this.editionListNotifications = res.notifications; + } + + onUpdateEditionNotifications(res) { + this.editionNotifications = res.notification; + } + + onUpdateContractAgreementListNotifications(res) { + this.contractAgreementListNotifications = res.notifications; + } + +} + +export default alt.createStore(NotificationStore, 'NotificationStore'); diff --git a/js/third_party/notifications.js b/js/third_party/notifications.js new file mode 100644 index 00000000..8887bbe2 --- /dev/null +++ b/js/third_party/notifications.js @@ -0,0 +1,36 @@ +'use strict'; + +import alt from '../alt'; +import EventActions from '../actions/event_actions'; + +import NotificationActions from '../actions/notification_actions'; + + +class NotificationsHandler { + + constructor() { + this.bindActions(EventActions); + this.loaded = false; + } + + onProfileDidLoad(profile) { + if (this.loaded) { + return; + } + let subdomain = window.location.host.split('.')[0]; + if (subdomain === 'ikonotv') { + NotificationActions.fetchContractAgreementListNotifications().then( + (res) => { + if (res.notifications && res.notifications.length > 0) { + this.loaded = true; + console.log('Contractagreement notifications loaded'); + setTimeout(() => window.appRouter.transitionTo('contract_notifications'), 0); + } + } + ); + } + this.loaded = true; + } +} + +export default alt.createStore(NotificationsHandler, 'NotificationsHandler'); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index 673a5509..7717a96a 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -52,19 +52,6 @@ export function sumNumList(l) { return sum; } -export function excludePropFromObject(obj, propList){ - let clonedObj = mergeOptions({},obj); - for (let item in propList){ - console.log(item); - if (clonedObj[propList[item]]){ - console.log('deleting... '); - delete clonedObj[propList[item]]; - } - } - console.log(clonedObj); - return clonedObj; -} - /* Taken from http://stackoverflow.com/a/4795914/1263876 Behaves like C's format string function @@ -207,4 +194,14 @@ function _mergeOptions(obj1, obj2) { */ export function escapeHTML(s) { return document.createElement('div').appendChild(document.createTextNode(s)).parentNode.innerHTML; +} + +export function excludePropFromObject(obj, propList){ + let clonedObj = mergeOptions({}, obj); + for (let item in propList){ + if (clonedObj[propList[item]]){ + delete clonedObj[propList[item]]; + } + } + return clonedObj; } \ No newline at end of file diff --git a/js/utils/requests.js b/js/utils/requests.js index 8e846d7c..fd676896 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -33,7 +33,7 @@ class Requests { // If this is the case, we can not try to parse it as JSON. if(responseText !== 'None') { let body = JSON.parse(responseText); - + if(body && body.errors) { let error = new Error('Form Error'); error.json = body; @@ -116,7 +116,6 @@ class Requests { merged.headers['X-CSRFToken'] = csrftoken; } merged.method = verb; - return fetch(url, merged) .then(this.unpackResponse) .catch(this.handleError); @@ -139,8 +138,7 @@ class Requests { _putOrPost(url, paramsAndBody, method){ let paramsCopy = this._merge(paramsAndBody); - let params = excludePropFromObject(paramsAndBody,['body']); - + let params = excludePropFromObject(paramsAndBody, ['body']); let newUrl = this.prepareUrl(url, params); let body = null; if (paramsCopy && paramsCopy.body) { @@ -157,6 +155,10 @@ class Requests { return this._putOrPost(url, params, 'put'); } + patch(url, params){ + return this._putOrPost(url, params, 'patch'); + } + defaults(options) { this.httpOptions = options.http || {}; this.urlMap = options.urlMap || {}; diff --git a/sass/ascribe_media_player.scss b/sass/ascribe_media_player.scss index 6a70ad4d..213fbafa 100644 --- a/sass/ascribe_media_player.scss +++ b/sass/ascribe_media_player.scss @@ -3,35 +3,40 @@ video, img { - display: block; - height: auto; - margin: 0 auto; - max-height: 640px; max-width: 100%; + max-height: 640px; width: auto; + height: auto; + display: block; + margin: 0 auto; } .media-other { color: #cccccc; font-size: 500%; + p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .audiojs { - background-image: none; margin: 50px auto; - * { - box-sizing: content-box; - } + background-image: none; + } - .loaded { - background-color: $ascribe-color-green; - background-image: none; - } + .audiojs * { + box-sizing: content-box; + } - .progress { - background-color: rgba(255, 255, 255, .8); - background-image: none; - } + .audiojs .loaded { + background-color: $ascribe-color-green; + background-image: none; + } + .audiojs .progress { + background-color: rgba(255,255,255,0.8); + background-image: none; } .video-js, @@ -44,13 +49,12 @@ } .vjs-fullscreen { - padding-top: 0; - - video { - max-height: 100%; - } + padding-top: 0px; } + .vjs-fullscreen video { + max-height: 100%; + } .vjs-default-skin .vjs-play-progress, .vjs-default-skin .vjs-volume-level { @@ -58,35 +62,41 @@ } .vjs-default-skin .vjs-big-play-button { - background-color: rgba(0, 0, 0, .8); - border: 0; - -moz-border-radius: 6px; - -o-border-radius: 6px; - -webkit-border-radius: 6px; border-radius: 6px; - -moz-box-shadow: none; - -o-box-shadow: none; - -webkit-box-shadow: none; + -o-border-radius: 6px; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + box-shadow: none; + -o-box-shadow: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + + width: 100px; height: 60px; + top: 50%; left: 50%; margin: -30px -50px; - top: 50%; - width: 100px; + + border: none; + + background-color: rgba(0,0,0,.8); } .vjs-default-skin:hover .vjs-big-play-button, .vjs-default-skin .vjs-big-play-button:focus { - background-color: rgba(0, 0, 0, .9); border-color: #fff; - -moz-box-shadow: none; - -o-box-shadow: none; - -webkit-box-shadow: none; + background-color: rgba(0,0,0,.9); + box-shadow: none; - -moz-transition: all 0s; - -o-transition: all 0s; - -webkit-transition: all 0s; + -o-box-shadow: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + transition: all 0s; + -o-transition: all 0s; + -moz-transition: all 0s; + -webkit-transition: all 0s; } .vjs-default-skin .vjs-big-play-button:before { @@ -103,7 +113,7 @@ } .vjs-default-skin .vjs-control-bar { - background-color: rgba(0, 0, 0, .7); + background-color: rgba(0,0,0,.7); } } diff --git a/sass/ascribe_notification_list.scss b/sass/ascribe_notification_list.scss index bd3c0b20..1b4b6a0c 100644 --- a/sass/ascribe_notification_list.scss +++ b/sass/ascribe_notification_list.scss @@ -2,13 +2,27 @@ $break-small: 764px; $break-medium: 991px; $break-medium: 1200px; -.notification-wrapper { +.notification-header,.notification-wrapper { width: 350px; - height:8em; - padding: 0.3em; - border-bottom: 1px solid #cccccc; - margin: -3px -20px; +} +.notification-header { + border-bottom: 1px solid #cccccc; + border-top: 1px solid #cccccc; + padding: 0.3em 1em; + background-color: #eeeeee; +} + +.notification-wrapper { + height:8.4em; + border-bottom: 1px solid #eeeeee; + margin: -3px 0; + padding: 0.5em; + color: black; + + &:hover{ + background-color: rgba(2, 182, 163, .05); + } // ToDo: Include media queries for thumbnail .thumbnail-wrapper { width: 7.4em; @@ -31,9 +45,15 @@ $break-medium: 1200px; margin-top: 0.3em; margin-bottom: 0.15em; font-size: 1.8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .sub-header{ - margin-bottom: 1em; + margin-bottom: 0.6em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .notification-action{ color: $ascribe-color-green; @@ -46,6 +66,10 @@ $break-medium: 1200px; li a { padding-top: 0; } + border-top: 0; + overflow-y: auto; + overflow-x: hidden; + max-height: 70vh; } } diff --git a/sass/ascribe_notification_page.scss b/sass/ascribe_notification_page.scss new file mode 100644 index 00000000..ad277aed --- /dev/null +++ b/sass/ascribe_notification_page.scss @@ -0,0 +1,62 @@ + +.notification-contract-download { + +} + +.notification-contract-wrapper{ + text-align: center; +} + +.notification-contract-logo { + + img { + margin-bottom: 1em; + } + .notification-contract-header { + font-size: 2em; + text-transform: uppercase; + margin-bottom: 0.8em; + } +} + +.notification-contract-pdf, .notification-contract-footer { + width: 100%; + max-width: 750px; + margin: 0 auto; +} +.notification-contract-pdf { + embed { + border: 1px solid #cccccc; + width: 100%; + height: 60vh; + margin-bottom: 0.4em; + } + .notification-contract-pdf-download { + text-align: left; + margin-left: 1em; + } +} + +.notification-contract-footer { + text-align: left; + padding: 1em; + > h1 { + margin-top: 0.4em; + font-size: 1.4em; + } + > p { + font-size: 0.9em; + color: #444444; + } + > pre { + color: #444; + cursor: default; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: rgba(0, 0, 0, 0); + border: 0; + box-shadow: none; + margin-bottom: 1em; + padding-left: 0; + width: 100%; + } +} \ No newline at end of file diff --git a/sass/lib/buttons.scss b/sass/lib/buttons.scss new file mode 100644 index 00000000..e69de29b diff --git a/sass/main.scss b/sass/main.scss index fb71ff2a..27890be9 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -25,6 +25,7 @@ $BASE_URL: '<%= BASE_URL %>'; @import 'ascribe_global_action'; @import 'ascribe_global_notification'; @import 'ascribe_notification_list'; +@import 'ascribe_notification_page'; @import 'ascribe_piece_register'; @import 'offset_right'; @import 'ascribe_settings'; @@ -208,36 +209,20 @@ hr { border: 1px solid $ascribe-brand-danger; } } - -.btn-ascribe, -.btn-ascribe-inv { +.btn-ascribe { border: 1px solid #444; border-radius: 0 !important; font-family: sans-serif !important; line-height: 2em; margin-left: 0 !important; - margin-right: 1px; -} - -.btn-ascribe, -.btn-ascribe-inv:active, -.btn-ascribe-inv:hover { - background-color: #fff; + font-family: sans-serif !important; + border-radius: 0 !important; color: #222 !important; } -.btn-ascribe:active, -.btn-ascribe:hover, -.btn-ascribe-inv { +.btn-ascribe:active, .btn-ascribe:hover { + color: #FFF !important; background-color: #444; - color: #fff !important; -} - -.btn-ascribe-inv:disabled, -.btn-ascribe-inv:focus { - background-color: #BBB !important; - border: 1px solid #444 !important; - color: #444 !important; } .btn-ascribe-sm {
- {this.props.buttonSubmitText} - CANCEL + + {this.props.buttonSubmitText} + + + CANCEL +
{this.state.value}
- .{ext} + {preview}
+ {appendix.default} +
+ ARS, DACS, Bildkunst, Pictoright, SODRAC, Copyright Agency/Viscopy, SAVA, Bildrecht GmbH, + SABAM, AUTVIS, CREAIMAGEN, SONECA, Copydan, EAU, Kuvasto, GCA, HUNGART, IVARO, SIAE, JASPAR-SPDA, + AKKA/LAA, LATGA-A, SOMAAP, ARTEGESTION, CARIER, BONO, APSAV, SPA, GESTOR, VISaRTA, RAO, LITA, + DALRO, VeGaP, BUS, ProLitteris, AGADU, AUTORARTE, BUBEDRA, BBDA, BCDA, BURIDA, ADAVIS, BSDA +