diff --git a/.gitignore b/.gitignore index ee54b7e5..30c9eae9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ lib-cov *.pid *.gz *.sublime-project +.idea +spool-project.sublime-project +*.sublime-workspace *.sublime-workspace webapp-dependencies.txt diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 00000000..61b1c624 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,224 @@ +linters: + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BemDepth: + enabled: false + max_elements: 1 + + BorderZero: + enabled: true + convention: zero # or `none` + + ColorKeyword: + enabled: true + + ColorVariable: + enabled: true + + Comment: + enabled: true + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DisableLinterReason: + enabled: false + + DuplicateProperty: + enabled: true + + ElsePlacement: + enabled: true + style: same_line # or 'new_line' + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: true + + EmptyRule: + enabled: true + + ExtendDirective: + enabled: false + + FinalNewline: + enabled: false + present: true + + HexLength: + enabled: true + style: short # or 'long' + + HexNotation: + enabled: true + style: lowercase # or 'uppercase' + + HexValidation: + enabled: true + + IdSelector: + enabled: true + + ImportantRule: + enabled: true + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + Indentation: + enabled: true + allow_non_nested_indentation: false + character: space # or 'tab' + width: 4 + + LeadingZero: + enabled: true + style: exclude_zero # or 'include_zero' + + MergeableSelector: + enabled: true + force_nesting: true + + NameFormat: + enabled: true + allow_leading_underscore: true + convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern + + NestingDepth: + enabled: true + max_depth: 3 + ignore_parent_selectors: false + + PlaceholderInExtend: + enabled: true + + PropertyCount: + enabled: false + include_nested: false + max_properties: 10 + + PropertySortOrder: + enabled: false + ignore_unspecified: false + min_properties: 2 + separate_groups: false + + PropertySpelling: + enabled: true + extra_properties: [] + + PropertyUnits: + enabled: true + global: [ + 'ch', 'em', 'ex', 'rem', # Font-relative lengths + 'cm', 'in', 'mm', 'pc', 'pt', 'px', 'q', # Absolute lengths + 'vh', 'vw', 'vmin', 'vmax', # Viewport-percentage lengths + 'deg', 'grad', 'rad', 'turn', # Angle + 'ms', 's', # Duration + 'Hz', 'kHz', # Frequency + 'dpi', 'dpcm', 'dppx', # Resolution + '%'] # Other + properties: {} + + QualifyingElement: + enabled: true + allow_element_with_attribute: false + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: true + max_depth: 3 + + SelectorFormat: + enabled: true + convention: hyphenated_lowercase # or 'strict_BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern + + Shorthand: + enabled: true + allowed_shorthands: [1, 2, 3] + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: true + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + + SpaceAfterPropertyColon: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' + + SpaceAfterPropertyName: + enabled: true + + SpaceAfterVariableName: + enabled: true + + SpaceAroundOperator: + enabled: true + style: one_space # or 'no_space' + + SpaceBeforeBrace: + enabled: true + style: space # or 'new_line' + allow_single_line_padding: false + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: single_quotes # or double_quotes + + TrailingSemicolon: + enabled: true + + TrailingWhitespace: + enabled: true + + TrailingZero: + enabled: false + + TransitionAll: + enabled: false + + UnnecessaryMantissa: + enabled: true + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: true + + UrlQuotes: + enabled: true + + VariableForProperty: + enabled: false + properties: [] + + VendorPrefix: + enabled: false + identifier_list: base + additional_identifiers: [] + excluded_identifiers: [] + + ZeroUnit: + enabled: true + + Compass::*: + enabled: false diff --git a/README.md b/README.md index 6fae0011..a3258576 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,16 @@ gulp serve Additionally, to work on the white labeling functionality, you need to edit your `/etc/hosts` file and add: ``` -127.0.0.1 localhost.com -127.0.0.1 cc.localhost.com +127.0.0.1 localhost.com +127.0.0.1 cc.localhost.com +127.0.0.1 cyland.localhost.com +127.0.0.1 ikonotv.localhost.com +127.0.0.1 sluice.localhost.com ``` -Code Conventions -================ +JavaScript Code Conventions +=========================== For this project, we're using: * 4 Spaces @@ -39,6 +42,15 @@ For this project, we're using: * We don't use camel case for file naming but in everything Javascript related * We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) + +SCSS Code Conventions +===================== +Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. + +Some interesting links: +* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) + + Testing =============== We're using Facebook's jest to do testing as it integrates nicely with react.js as well. @@ -124,4 +136,4 @@ Moar stuff - [24ways.org: JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/) - [Babel: Learn ES6](https://babeljs.io/docs/learn-es6/) - [egghead's awesome reactjs and flux tutorials](https://egghead.io/) -- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742) \ No newline at end of file +- [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742) diff --git a/docs/feature_list.md b/docs/feature_list.md new file mode 100644 index 00000000..568f7146 --- /dev/null +++ b/docs/feature_list.md @@ -0,0 +1,63 @@ +# Feature list + +This list specifies all features in Onion that ought to be tested before actually pushing live: + +- sign up & email activation +- login +- log out +- form input + + reset + + save + + disabled state +- form checkbox + + reset + + save + + disabled state +- create app + + refresh token +- loan contract + + upload + + download + + delete +- register work + + with edition + + without edition + + correct encoding of video upload +- fineuploader + + upload file + + upload multiple files + + delete file + + cancel upload of file +- create editions + + in piece list + + in piece detail +- all notes in edition/piece detail +- transfer & consign & loan & share & delete + + bulk + + single + + withdraw +- piece list + + filter (also check for correct filtering of opened edition tables) + + order + + search + + pagination + + expandable edition list for piece +- download coa + +## sluice +- hero landing page +- activation email +- submission (also check extra form fields) + + of existing pieces + + newly registered pieces +- rating + + in piece list + + in piece detail +- short listing (not yet implemented) +- piece list + + order by rating + +## Cyland +- hero landing page +- activation email +- submission (check states of submission (1,2,3)) diff --git a/docs/refactor-todo.md b/docs/refactor-todo.md index 8554f001..f7a5917b 100644 --- a/docs/refactor-todo.md +++ b/docs/refactor-todo.md @@ -2,16 +2,18 @@ *This should be a living document. So if you have any ideas for refactoring stuff, then feel free to add them to this document* -- Get rid of all Mixins. (making good progress there :)) - Make all standalone components independent from things like global utilities (GeneralUtils is maybe used in table for example) -- Check if all polyfills are appropriately initialized and available: Compare to this - Extract all standalone components to their own folder structure and write application independent tests (+ figure out how to do that in a productive way) (fetch lib especially) -- Refactor forms to generic-declarative form component - Check for mobile compatibility: Is site responsive anywhere? queryParams of the piece_list_store should all be reflected in the url and not a single component each should manipulate the URL bar (refactor pagination, use actions and state) - Refactor string-templating for api_urls - Use classNames plugin instead of if-conditional-classes +# Refactor DONE +- Refactor forms to generic-declarative form component ✓ +- Get rid of all Mixins (inject head is fine) ✓ +- Check if all polyfills are appropriately initialized and available: Compare to this ✓ + ## React-S3-Fineuploader - implementation should enable to define all important methods outside - and: maybe create a utility class for all methods to avoid code duplication diff --git a/gulpfile.js b/gulpfile.js index 22252fe8..3c92945d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -23,7 +23,7 @@ var argv = require('yargs').argv; var server = require('./server.js').app; var minifyCss = require('gulp-minify-css'); var uglify = require('gulp-uglify'); - +var opn = require('opn'); var config = { @@ -48,17 +48,16 @@ var config = { }, filesToWatch: [ 'build/css/*.css', - 'build/js/*.js', - 'node_modules/react-s3-fine_uploader/*.js' + 'build/js/*.js' ] }; -var SERVER_URL = process.env.ONION_SERVER_URL || 'http://staging.ascribe.io/'; +var SERVER_URL = process.env.ONION_SERVER_URL || 'https://staging.ascribe.io/'; var constants = { BASE_URL: (function () { var baseUrl = process.env.ONION_BASE_URL || '/'; return baseUrl + (baseUrl.match(/\/$/) ? '' : '/'); })(), SERVER_URL: SERVER_URL, - API_ENDPOINT: SERVER_URL + 'api/' || 'http://staging.ascribe.io/api/', + API_ENDPOINT: SERVER_URL + 'api/' || 'https://staging.ascribe.io/api/', DEBUG: !argv.production, CREDENTIALS: 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw' // dimi@mailinator:0000000000 }; @@ -73,6 +72,9 @@ gulp.task('js:build', function() { gulp.task('serve', ['browser-sync', 'run-server', 'sass:build', 'sass:watch', 'copy'], function() { bundle(true); + + // opens the browser window with the correct url, which is localhost.com + opn('http://www.localhost.com:3000'); }); gulp.task('jest', function(done) { @@ -93,7 +95,9 @@ gulp.task('browser-sync', function() { browserSync({ files: config.filesToWatch, proxy: 'http://localhost:4000', - port: 3000 + port: 3000, + open: false, // does not open the browser-window anymore (handled manually) + ghostMode: false }); }); @@ -185,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_agreement_list_actions.js b/js/actions/contract_agreement_list_actions.js new file mode 100644 index 00000000..589c1f51 --- /dev/null +++ b/js/actions/contract_agreement_list_actions.js @@ -0,0 +1,113 @@ +'use strict'; + +import alt from '../alt'; +import Q from 'q'; + +import OwnershipFetcher from '../fetchers/ownership_fetcher'; +import ContractListActions from './contract_list_actions'; + +class ContractAgreementListActions { + constructor() { + this.generateActions( + 'updateContractAgreementList', + 'flushContractAgreementList' + ); + } + + fetchContractAgreementList(issuer, accepted, pending) { + this.actions.updateContractAgreementList(null); + return Q.Promise((resolve, reject) => { + OwnershipFetcher.fetchContractAgreementList(issuer, accepted, pending) + .then((contractAgreementList) => { + if (contractAgreementList.count > 0) { + this.actions.updateContractAgreementList(contractAgreementList.results); + resolve(contractAgreementList.results); + } + else{ + resolve(null); + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + ); + } + + fetchAvailableContractAgreementList(issuer, createContractAgreement) { + return Q.Promise((resolve, reject) => { + OwnershipFetcher.fetchContractAgreementList(issuer, true, null) + .then((acceptedContractAgreementList) => { + // if there is at least an accepted contract agreement, we're going to + // use it + if(acceptedContractAgreementList.count > 0) { + this.actions.updateContractAgreementList(acceptedContractAgreementList.results); + } else { + // otherwise, we're looking for contract agreements that are still pending + // + // Normally nesting promises, but for this conditional one, it makes sense to not + // overcomplicate the method + OwnershipFetcher.fetchContractAgreementList(issuer, null, true) + .then((pendingContractAgreementList) => { + if(pendingContractAgreementList.count > 0) { + this.actions.updateContractAgreementList(pendingContractAgreementList.results); + } else { + // if there was neither a pending nor an active contractAgreement + // found and createContractAgreement is set to true, we create a + // new contract agreement + if(createContractAgreement) { + this.actions.createContractAgreementFromPublicContract(issuer); + } + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + }) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); + } + ); + } + + createContractAgreementFromPublicContract(issuer) { + ContractListActions.fetchContractList(null, null, issuer) + .then((publicContract) => { + // create an agreement with the public contract if there is one + if (publicContract && publicContract.length > 0) { + return this.actions.createContractAgreement(null, publicContract[0]); + } + else { + /* + contractAgreementList in the store is already set to null; + */ + } + }).then((publicContracAgreement) => { + if (publicContracAgreement) { + this.actions.updateContractAgreementList([publicContracAgreement]); + } + }).catch((err) => { + console.logGlobal(err); + }); + } + + createContractAgreement(issuer, contract){ + return Q.Promise((resolve, reject) => { + OwnershipFetcher.createContractAgreement(issuer, contract).then( + (contractAgreement) => { + resolve(contractAgreement); + } + ).catch((err) => { + console.logGlobal(err); + reject(err); + }); + }); + } +} + +export default alt.createActions(ContractAgreementListActions); diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js new file mode 100644 index 00000000..5b874caf --- /dev/null +++ b/js/actions/contract_list_actions.js @@ -0,0 +1,58 @@ +'use strict'; + +import alt from '../alt'; +import OwnershipFetcher from '../fetchers/ownership_fetcher'; +import Q from 'q'; + +class ContractListActions { + constructor() { + this.generateActions( + 'updateContractList', + 'flushContractList' + ); + } + + fetchContractList(isActive, isPublic, issuer) { + return Q.Promise((resolve, reject) => { + OwnershipFetcher.fetchContractList(isActive, isPublic, issuer) + .then((contracts) => { + this.actions.updateContractList(contracts.results); + resolve(contracts.results); + }) + .catch((err) => { + console.logGlobal(err); + this.actions.updateContractList([]); + reject(err); + }); + }); + } + + + changeContract(contract){ + return Q.Promise((resolve, reject) => { + OwnershipFetcher.changeContract(contract) + .then((res) => { + resolve(res); + }) + .catch((err)=> { + console.logGlobal(err); + reject(err); + }); + }); + } + + removeContract(contractId){ + return Q.Promise( (resolve, reject) => { + OwnershipFetcher.deleteContract(contractId) + .then((res) => { + resolve(res); + }) + .catch( (err) => { + console.logGlobal(err); + reject(err); + }); + }); + } +} + +export default alt.createActions(ContractListActions); diff --git a/js/actions/loan_contract_actions.js b/js/actions/loan_contract_actions.js deleted file mode 100644 index cc7e5a5b..00000000 --- a/js/actions/loan_contract_actions.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import alt from '../alt'; -import OwnershipFetcher from '../fetchers/ownership_fetcher'; - - -class LoanContractActions { - constructor() { - this.generateActions( - 'updateLoanContract', - 'flushLoanContract' - ); - } - - fetchLoanContract(email) { - if(email.match(/.+\@.+\..+/)) { - OwnershipFetcher.fetchLoanContract(email) - .then((contracts) => { - if (contracts && contracts.length > 0) { - this.actions.updateLoanContract({ - contractKey: contracts[0].s3Key, - contractUrl: contracts[0].s3Url, - contractEmail: email - }); - } - else { - this.actions.updateLoanContract({ - contractKey: null, - contractUrl: null, - contractEmail: null - }); - } - }) - .catch((err) => { - console.logGlobal(err); - this.actions.updateLoanContract({ - contractKey: null, - contractUrl: null, - contractEmail: null - }); - }); - } else { - /* No email was entered - Ignore and keep going*/ - } - } -} - -export default alt.createActions(LoanContractActions); 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/actions/ownership_actions.js b/js/actions/ownership_actions.js new file mode 100644 index 00000000..222309bb --- /dev/null +++ b/js/actions/ownership_actions.js @@ -0,0 +1,38 @@ +'use strict'; + +import alt from '../alt'; +import OwnershipFetcher from '../fetchers/ownership_fetcher'; + + +class OwnershipActions { + constructor() { + this.generateActions( + 'updateLoanPieceRequestList', + 'updateLoanPieceRequest', + 'flushLoanPieceRequest' + ); + } + + fetchLoanRequestList() { + OwnershipFetcher.fetchLoanPieceRequestList() + .then((data) => { + this.actions.updateLoanPieceRequestList(data.loan_requests); + }) + .catch((err) => { + console.logGlobal(err); + this.actions.updateLoanPieceRequestList(null); + }); + } + + fetchLoanRequest(pieceId) { + OwnershipFetcher.fetchLoanPieceRequestList() + .then((data) => { + this.actions.updateLoanPieceRequest({loanRequests: data.loan_requests, pieceId: pieceId}); + }) + .catch((err) => { + console.logGlobal(err); + }); + } +} + +export default alt.createActions(OwnershipActions); diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 56e42952..31f1bca5 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -29,8 +29,9 @@ class PieceListActions extends ActionQueue { orderBy, orderAsc, filterBy, - 'pieceList': [], - 'pieceListCount': -1 + pieceList: [], + pieceListCount: -1, + unfilteredPieceListCount: -1 }); // afterwards, we can load the list @@ -46,8 +47,9 @@ class PieceListActions extends ActionQueue { orderBy, orderAsc, filterBy, - 'pieceList': res.pieces, - 'pieceListCount': res.count + pieceList: res.pieces, + pieceListCount: res.count, + unfilteredPieceListCount: res.unfiltered_count }); resolve(); }) @@ -59,7 +61,7 @@ class PieceListActions extends ActionQueue { PieceListFetcher .fetchRequestActions() .then((res) => { - this.actions.updatePieceListRequestActions(res.piece_ids); + this.actions.updatePieceListRequestActions(res); }) .catch((err) => console.logGlobal(err)); } diff --git a/js/app.js b/js/app.js index 81a8fb82..30a57d2b 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(); @@ -44,9 +45,7 @@ requests.defaults({ }); - class AppGateway { - start() { let settings; let subdomain = window.location.host.split('.')[0]; @@ -66,12 +65,17 @@ class AppGateway { load(settings) { let type = 'default'; + let subdomain = 'www'; + if (settings) { type = settings.type; + subdomain = settings.subdomain; } + window.document.body.classList.add('client--' + subdomain); + EventActions.applicationWillBoot(settings); - Router.run(getRoutes(type), Router.HistoryLocation, (App) => { + window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => { React.render( , document.getElementById('main') diff --git a/js/components/acl_proxy.js b/js/components/acl_proxy.js index a04e499d..4fc90a9b 100644 --- a/js/components/acl_proxy.js +++ b/js/components/acl_proxy.js @@ -20,25 +20,32 @@ let AclProxy = React.createClass({ show: React.PropTypes.bool }, - render() { - if(this.props.show) { + getChildren() { + if (React.Children.count(this.props.children) > 1){ + /* + This might ruin styles for header items in the navbar etc + */ return ( {this.props.children} ); + } + /* can only do this when there is only 1 child, but will preserve styles */ + return this.props.children; + }, + + render() { + if(this.props.show) { + return this.getChildren(); } else { if(this.props.aclObject) { if(this.props.aclObject[this.props.aclName]) { - return ( - - {this.props.children} - - ); + return this.getChildren(); } else { - if(typeof this.props.aclObject[this.props.aclName] === 'undefined') { + /* if(typeof this.props.aclObject[this.props.aclName] === 'undefined') { console.warn('The aclName you\'re filtering for was not present (or undefined) in the aclObject.'); - } + } */ return null; } } diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index 85084b5f..fe300702 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -21,14 +21,14 @@ let AccordionList = React.createClass({ ); } else if(this.props.count === 0) { return ( -
+

{getLangText('We could not find any works related to you...')}

{getLangText('To register one, click')} {getLangText('here')}!

); } else { return ( -
+
{this.props.loadingElement}
); diff --git a/js/components/ascribe_accordion_list/accordion_list_item.js b/js/components/ascribe_accordion_list/accordion_list_item.js index 6c07a891..6204f57d 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item.js +++ b/js/components/ascribe_accordion_list/accordion_list_item.js @@ -3,151 +3,24 @@ import React from 'react'; import Router from 'react-router'; -import Glyphicon from 'react-bootstrap/lib/Glyphicon'; -import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; -import Tooltip from 'react-bootstrap/lib/Tooltip'; - -import AccordionListItemEditionWidget from './accordion_list_item_edition_widget'; -import CreateEditionsForm from '../ascribe_forms/create_editions_form'; - -import PieceListActions from '../../actions/piece_list_actions'; -import PieceListStore from '../../stores/piece_list_store'; - -import WhitelabelStore from '../../stores/whitelabel_store'; - -import EditionListActions from '../../actions/edition_list_actions'; - -import GlobalNotificationModel from '../../models/global_notification_model'; -import GlobalNotificationActions from '../../actions/global_notification_actions'; - -import AclProxy from '../acl_proxy'; -import SubmitToPrizeButton from '../whitelabel/prize/components/ascribe_buttons/submit_to_prize_button'; - -import { getLangText } from '../../utils/lang_utils'; -import { mergeOptions } from '../../utils/general_utils'; - -let Link = Router.Link; - let AccordionListItem = React.createClass({ propTypes: { + badge: React.PropTypes.object, className: React.PropTypes.string, - content: React.PropTypes.object, - children: React.PropTypes.object + thumbnail: React.PropTypes.object, + heading: React.PropTypes.object, + subheading: React.PropTypes.object, + subsubheading: React.PropTypes.object, + buttons: React.PropTypes.object, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) }, mixins: [Router.Navigation], - getInitialState() { - return mergeOptions( - { - showCreateEditionsDialog: false - }, - PieceListStore.getState(), - WhitelabelStore.getState() - ); - }, - - componentDidMount() { - PieceListStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - }, - - componentWillUnmount() { - PieceListStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - getGlyphicon(){ - if (this.props.content.requestAction) { - return ( - {getLangText('You have actions pending in one of your editions')}}> - - ); - } - return null; - }, - - toggleCreateEditionsDialog() { - this.setState({ - showCreateEditionsDialog: !this.state.showCreateEditionsDialog - }); - }, - - handleEditionCreationSuccess() { - PieceListActions.updatePropertyForPiece({pieceId: this.props.content.id, key: 'num_editions', value: 0}); - - this.toggleCreateEditionsDialog(); - }, - - handleSubmitPrizeSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - onPollingSuccess(pieceId) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - EditionListActions.toggleEditionList(pieceId); - - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - getCreateEditionsDialog() { - if (this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog) { - return ( -
- -
- ); - } - }, - - getLicences() { - // convert this to acl_view_licences later - if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') { - return ( - - , - - {getLangText('%s license', this.props.content.license_type.code)} - - - ); - } - }, - render() { - let linkData; - - if (this.props.content.num_editions < 1 || !this.props.content.first_edition) { - linkData = { - to: 'piece', - params: { - pieceId: this.props.content.id - } - }; - } else { - linkData = { - to: 'edition', - params: { - editionId: this.props.content.first_edition.bitcoin_id - } - }; - } return (
@@ -155,52 +28,22 @@ let AccordionListItem = React.createClass({
- - - + {this.props.thumbnail}
- -

{this.props.content.title}

- - -

{getLangText('by %s', this.props.content.artist_name)}

- -
- {this.props.content.date_created.split('-')[0]} - - - - - - - - {this.getLicences()} -
+ {this.props.heading} + {this.props.subheading} + {this.props.subsubheading} + {this.props.buttons}
-
- {this.getGlyphicon()} +
+ {this.props.badge}
- - {this.getCreateEditionsDialog()} - - {/* this.props.children is AccordionListItemTableEditions */} {this.props.children}
); diff --git a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js index e4ba0d2b..709160b9 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -6,7 +6,6 @@ import classNames from 'classnames'; import EditionListActions from '../../actions/edition_list_actions'; import EditionListStore from '../../stores/edition_list_store'; -import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; import Button from 'react-bootstrap/lib/Button'; @@ -16,6 +15,7 @@ import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; + let AccordionListItemEditionWidget = React.createClass({ propTypes: { className: React.PropTypes.string, diff --git a/js/components/ascribe_accordion_list/accordion_list_item_piece.js b/js/components/ascribe_accordion_list/accordion_list_item_piece.js new file mode 100644 index 00000000..d0b16c9f --- /dev/null +++ b/js/components/ascribe_accordion_list/accordion_list_item_piece.js @@ -0,0 +1,77 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; + +import AccordionListItem from './accordion_list_item'; + +import { getLangText } from '../../utils/lang_utils'; + +let Link = Router.Link; + + +let AccordionListItemPiece = React.createClass({ + propTypes: { + className: React.PropTypes.string, + artistName: React.PropTypes.string, + piece: React.PropTypes.object, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]), + subsubheading: React.PropTypes.object, + buttons: React.PropTypes.object, + badge: React.PropTypes.object + }, + + mixins: [Router.Navigation], + + getLinkData() { + + if(this.props.piece && this.props.piece.first_edition) { + return { + to: 'edition', + params: { + editionId: this.props.piece.first_edition.bitcoin_id + } + }; + } else { + return { + to: 'piece', + params: { + pieceId: this.props.piece.id + } + }; + } + + }, + + render() { + return ( + + + } + heading={ + +

{this.props.piece.title}

+ } + subheading={ +

+ {getLangText('by ')} + {this.props.artistName ? this.props.artistName : this.props.piece.artist_name} +

+ } + subsubheading={this.props.subsubheading} + buttons={this.props.buttons} + badge={this.props.badge} + > + {this.props.children} +
+ ); + } +}); + +export default AccordionListItemPiece; 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 new file mode 100644 index 00000000..dde5c43d --- /dev/null +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -0,0 +1,156 @@ +'use strict'; + +import React from 'react'; + +import Glyphicon from 'react-bootstrap/lib/Glyphicon'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; + +import AccordionListItemPiece from './accordion_list_item_piece'; +import AccordionListItemEditionWidget from './accordion_list_item_edition_widget'; +import CreateEditionsForm from '../ascribe_forms/create_editions_form'; + +import PieceListActions from '../../actions/piece_list_actions'; +import PieceListStore from '../../stores/piece_list_store'; + +import WhitelabelStore from '../../stores/whitelabel_store'; + +import EditionListActions from '../../actions/edition_list_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import AclProxy from '../acl_proxy'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; + + +let AccordionListItemWallet = React.createClass({ + propTypes: { + className: React.PropTypes.string, + content: React.PropTypes.object, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) + }, + + getInitialState() { + return mergeOptions( + { + showCreateEditionsDialog: false + }, + PieceListStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentDidMount() { + PieceListStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + }, + + componentWillUnmount() { + PieceListStore.unlisten(this.onChange); + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + getGlyphicon(){ + if ((this.props.content.notifications && this.props.content.notifications.length > 0)){ + return ( + {getLangText('You have actions pending')}}> + + ); + } + return null; + }, + + toggleCreateEditionsDialog() { + this.setState({ + showCreateEditionsDialog: !this.state.showCreateEditionsDialog + }); + }, + + handleEditionCreationSuccess() { + PieceListActions.updatePropertyForPiece({pieceId: this.props.content.id, key: 'num_editions', value: 0}); + + this.toggleCreateEditionsDialog(); + }, + + onPollingSuccess(pieceId) { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + EditionListActions.toggleEditionList(pieceId); + + let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getCreateEditionsDialog() { + if (this.props.content.num_editions < 1 && this.state.showCreateEditionsDialog) { + return ( +
+ +
+ ); + } + }, + + getLicences() { + // convert this to acl_view_licences later + if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') { + return ( + + , + + {getLangText('%s license', this.props.content.license_type.code)} + + + ); + } + }, + + render() { + + return ( + + {this.props.content.date_created.split('-')[0]} + {this.getLicences()} +
} + buttons={ +
+ + + +
} + badge={this.getGlyphicon()}> + {this.getCreateEditionsDialog()} + {/* this.props.children is AccordionListItemTableEditions */} + {this.props.children} + + ); + } +}); + +export default AccordionListItemWallet; diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index b4a894a3..789399b0 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -6,15 +6,16 @@ import Header from '../components/header'; import Footer from '../components/footer'; import GlobalNotification from './global_notification'; -// let Link = Router.Link; -let RouteHandler = Router.RouteHandler; +import getRoutes from '../routes'; +let RouteHandler = Router.RouteHandler; + let AscribeApp = React.createClass({ render() { return (
-
+
diff --git a/js/components/ascribe_buttons/acl_button.js b/js/components/ascribe_buttons/acl_button.js index d9423889..e3c7fa1c 100644 --- a/js/components/ascribe_buttons/acl_button.js +++ b/js/components/ascribe_buttons/acl_button.js @@ -6,6 +6,7 @@ import ConsignForm from '../ascribe_forms/form_consign'; import UnConsignForm from '../ascribe_forms/form_unconsign'; import TransferForm from '../ascribe_forms/form_transfer'; import LoanForm from '../ascribe_forms/form_loan'; +import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer'; import ShareForm from '../ascribe_forms/form_share_email'; import ModalWrapper from '../ascribe_modal/modal_wrapper'; import AppConstants from '../../constants/application_constants'; @@ -13,8 +14,10 @@ import AppConstants from '../../constants/application_constants'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import { getLangText } from '../../utils/lang_utils.js'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; + +import { getAclFormMessage } from '../../utils/form_utils'; +import { getLangText } from '../../utils/lang_utils'; let AclButton = React.createClass({ propTypes: { @@ -25,6 +28,8 @@ let AclButton = React.createClass({ React.PropTypes.array ]).isRequired, currentUser: React.PropTypes.object, + buttonAcceptName: React.PropTypes.string, + buttonAcceptClassName: React.PropTypes.string, handleSuccess: React.PropTypes.func.isRequired, className: React.PropTypes.string }, @@ -34,15 +39,18 @@ let AclButton = React.createClass({ }, actionProperties(){ + + let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username); + if (this.props.action === 'acl_consign'){ return { title: getLangText('Consign artwork'), tooltip: getLangText('Have someone else sell the artwork'), form: ( + url={ApiUrls.ownership_consigns}/> ), handleSuccess: this.showNotification }; @@ -53,9 +61,9 @@ let AclButton = React.createClass({ tooltip: getLangText('Have the owner manage his sales again'), form: ( + url={ApiUrls.ownership_unconsigns}/> ), handleSuccess: this.showNotification }; @@ -65,9 +73,9 @@ let AclButton = React.createClass({ tooltip: getLangText('Transfer the ownership of the artwork'), form: ( + url={ApiUrls.ownership_transfers}/> ), handleSuccess: this.showNotification }; @@ -77,9 +85,21 @@ let AclButton = React.createClass({ title: getLangText('Loan artwork'), tooltip: getLangText('Loan your artwork for a limited period of time'), form: ( + url={this.isPiece() ? ApiUrls.ownership_loans_pieces : ApiUrls.ownership_loans_editions}/> + ), + handleSuccess: this.showNotification + }; + } + else if (this.props.action === 'acl_loan_request'){ + return { + title: getLangText('Loan artwork'), + tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time'), + form: ( ), handleSuccess: this.showNotification }; @@ -90,9 +110,9 @@ let AclButton = React.createClass({ tooltip: getLangText('Share the artwork'), form: ( + url={this.isPiece() ? ApiUrls.ownership_shares_pieces : ApiUrls.ownership_shares_editions }/> ), handleSuccess: this.showNotification }; @@ -133,98 +153,33 @@ let AclButton = React.createClass({ } }, -// plz move to transfer form - getTransferMessage(){ - return ( - `${getLangText('Hi')}, - -${getLangText('I transfer ownership of')}: -${this.getTitlesString()} ${getLangText('to you')}. - -${getLangText('Truly yours')}, -${this.props.currentUser.username} - ` - ); - }, - - // plz move to transfer form - getLoanMessage(){ - return ( - `${getLangText('Hi')}, - -${getLangText('I loan')}: -${this.getTitlesString()} ${getLangText('to you')}. - -${getLangText('Truly yours')}, -${this.props.currentUser.username} - ` - ); - }, - - // plz move to consign form - getConsignMessage(){ - return ( - `${getLangText('Hi')}, - -${getLangText('I consign')}: -${this.getTitlesString()} ${getLangText('to you')}. - -${getLangText('Truly yours')}, -${this.props.currentUser.username} - ` - ); - }, - - // plz move to consign form - getUnConsignMessage(){ - return ( - `${getLangText('Hi')}, - -${getLangText('I un-consign')}: -${this.getTitlesString()} ${getLangText('from you')}. - -${getLangText('Truly yours')}, -${this.props.currentUser.username} - ` - ); - }, - -// plz move to share form - getShareMessage(){ - return ( - `${getLangText('Hi')}, - -${getLangText('I am sharing')}: -${this.getTitlesString()} ${getLangText('with you')}. - -${getLangText('Truly yours')}, -${this.props.currentUser.username} - ` - ); - }, - // Removes the acl_ prefix and converts to upper case sanitizeAction() { + if (this.props.buttonAcceptName) { + return this.props.buttonAcceptName; + } return this.props.action.split('acl_')[1].toUpperCase(); }, render() { - let shouldDisplay = this.props.availableAcls[this.props.action]; - let aclProps = this.actionProperties(); - - return ( - - {this.sanitizeAction()} - - } - handleSuccess={aclProps.handleSuccess} - title={aclProps.title} - tooltip={aclProps.tooltip}> - {aclProps.form} - - ); + if (this.props.availableAcls){ + let shouldDisplay = this.props.availableAcls[this.props.action]; + let aclProps = this.actionProperties(); + let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : ''; + return ( + + {this.sanitizeAction()} + + } + handleSuccess={aclProps.handleSuccess} + title={aclProps.title}> + {aclProps.form} + + ); + } + return null; } }); diff --git a/js/components/ascribe_buttons/button_submit.js b/js/components/ascribe_buttons/button_submit.js deleted file mode 100644 index ef5999cd..00000000 --- a/js/components/ascribe_buttons/button_submit.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -import React from 'react'; - -let ButtonSubmitOrClose = React.createClass({ - propTypes: { - submitted: React.PropTypes.bool.isRequired, - text: React.PropTypes.string.isRequired - }, - - render() { - if (this.props.submitted){ - return ( -
- -
- ); - } - return ( -
- -
- ); - } -}); - -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 11d3c0a4..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 ( -
- - -
- ); - } -}); - -export default ButtonSubmitOrClose; diff --git a/js/components/ascribe_buttons/delete_button.js b/js/components/ascribe_buttons/delete_button.js index a60344df..b0b64427 100644 --- a/js/components/ascribe_buttons/delete_button.js +++ b/js/components/ascribe_buttons/delete_button.js @@ -26,7 +26,7 @@ let DeleteButton = React.createClass({ mixins: [Router.Navigation], - render: function () { + render() { let availableAcls; let btnDelete; let content; @@ -61,13 +61,14 @@ let DeleteButton = React.createClass({ } btnDelete = ; - } - else { + + } else { return null; } + return ( {content} @@ -77,4 +78,3 @@ let DeleteButton = React.createClass({ }); export default DeleteButton; - diff --git a/js/components/ascribe_buttons/unconsign_request_button.js b/js/components/ascribe_buttons/unconsign_request_button.js index 11cbfa51..e5e1c661 100644 --- a/js/components/ascribe_buttons/unconsign_request_button.js +++ b/js/components/ascribe_buttons/unconsign_request_button.js @@ -8,7 +8,7 @@ import ModalWrapper from '../ascribe_modal/modal_wrapper'; import UnConsignRequestForm from './../ascribe_forms/form_unconsign_request'; import { getLangText } from '../../utils/lang_utils.js'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; let UnConsignRequestButton = React.createClass({ @@ -21,16 +21,15 @@ let UnConsignRequestButton = React.createClass({ render: function () { return ( REQUEST UNCONSIGN } handleSuccess={this.props.handleSuccess} - title='Request to Un-Consign' - tooltip='Ask the consignee to return the ownership of the work back to you'> + title='Request to Un-Consign'> -
+
{text} {this.props.title}
-
+ {this.props.children} -
+
); diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js index f220fc98..828ed81a 100644 --- a/js/components/ascribe_detail/detail_property.js +++ b/js/components/ascribe_detail/detail_property.js @@ -17,9 +17,9 @@ let DetailProperty = React.createClass({ getDefaultProps() { return { - separator: ':', - labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2', - valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10' + separator: '', + labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-bottom ascribe-detail-property-label', + valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10 col-xs-height col-bottom ascribe-detail-property-value' }; }, @@ -52,11 +52,11 @@ let DetailProperty = React.createClass({ return (
-
- { this.props.label + this.props.separator} +
+ { this.props.label } { this.props.separator}
{value}
diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 25e8a97c..97f9fb3d 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -16,6 +16,7 @@ import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; import EditionListActions from '../../actions/edition_list_actions'; +import HistoryIterator from './history_iterator'; import MediaContainer from './media_container'; @@ -24,11 +25,10 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph import Form from './../ascribe_forms/form'; import Property from './../ascribe_forms/property'; import EditionDetailProperty from './detail_property'; -import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable'; - +import LicenseDetail from './license_detail'; import EditionFurtherDetails from './further_details'; -import RequestActionForm from './../ascribe_forms/form_request_action'; +import ListRequestActions from './../ascribe_forms/list_form_request_actions'; import AclButtonList from './../ascribe_buttons/acl_button_list'; import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button'; import DeleteButton from '../ascribe_buttons/delete_button'; @@ -36,7 +36,9 @@ import DeleteButton from '../ascribe_buttons/delete_button'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import apiUrls from '../../constants/api_urls'; +import Note from './note'; + +import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils'; @@ -86,10 +88,8 @@ let Edition = React.createClass({ }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + this.refreshCollection(); - EditionListActions.refreshEditionList(this.props.edition.parent); EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); @@ -99,6 +99,12 @@ let Edition = React.createClass({ this.transitionTo('pieces'); }, + refreshCollection() { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); + }, + render() { return ( @@ -108,14 +114,15 @@ let Edition = React.createClass({
-

{this.props.edition.title}


+

{this.props.edition.title}


@@ -130,42 +137,55 @@ let Edition = React.createClass({ 0}> - 0}> - 0}> - - - + show={!!(this.state.currentUser.username + || this.props.edition.acl.acl_edit + || this.props.edition.public_note)}> + {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} + label={getLangText('Personal note (private)')} + defaultValue={this.props.edition.private_note ? this.props.edition.private_note : null} + placeholder={getLangText('Enter your comments ...')} + editable={true} + successMessage={getLangText('Private note saved')} + url={ApiUrls.note_private_edition} + currentUser={this.state.currentUser}/> + {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} + label={getLangText('Edition note (public)')} + defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null} + placeholder={getLangText('Enter your comments ...')} + editable={!!this.props.edition.acl.acl_edit} + show={!!this.props.edition.public_note || !!this.props.edition.acl.acl_edit} + successMessage={getLangText('Public edition note saved')} + url={ApiUrls.note_public_edition} + currentUser={this.state.currentUser}/> 0 - || this.props.edition.other_data !== null}> + || this.props.edition.other_data.length > 0}> 0){ + if (this.props.edition && + this.props.edition.notifications && + this.props.edition.notifications.length > 0){ actions = ( - ); + handleSuccess={this.showNotification} + notifications={this.props.edition.notifications}/>); } else { @@ -233,10 +264,11 @@ let EditionSummary = React.createClass({ if (this.props.edition.status.length > 0 && this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer) { withdrawButton = (
+ className='inline' + isInline={true}> @@ -259,7 +291,7 @@ let EditionSummary = React.createClass({ className="text-center ascribe-button-list" availableAcls={this.props.edition.acl} editions={[this.props.edition]} - handleSuccess={this.props.handleSuccess}> + handleSuccess={this.handleSuccess}> {withdrawButton} + {this.getStatus()} {this.getActions()}
); - } }); -let EditionDetailHistoryIterator = React.createClass({ - propTypes: { - history: React.PropTypes.array - }, - - render() { - return ( - - {this.props.history.map((historicalEvent, i) => { - return ( - -
{ historicalEvent[1] }
-
- ); - })} -
- - ); - } -}); - -let EditionPersonalNote = React.createClass({ - propTypes: { - edition: React.PropTypes.object, - currentUser: React.PropTypes.object, - handleSuccess: React.PropTypes.func - }, - showNotification(){ - this.props.handleSuccess(); - let notification = new GlobalNotificationModel(getLangText('Private note saved'), 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - - render() { - if (this.props.currentUser.username && true || false) { - return ( -
- - - - -
-
- ); - } - return null; - } -}); - -let EditionPublicEditionNote = React.createClass({ - propTypes: { - edition: React.PropTypes.object, - handleSuccess: React.PropTypes.func - }, - showNotification(){ - this.props.handleSuccess(); - let notification = new GlobalNotificationModel(getLangText('Public note saved'), 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); - }, - render() { - let isEditable = this.props.edition.acl.acl_edit; - if (isEditable || this.props.edition.public_note){ - return ( -
- - - - -
-
- ); - } - return null; - } -}); - let CoaDetails = React.createClass({ propTypes: { edition: React.PropTypes.object diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 15086434..0f726ae5 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -9,6 +9,8 @@ import Edition from './edition'; import AppConstants from '../../constants/application_constants'; + + /** * This is the component that implements resource/data specific functionality */ @@ -34,6 +36,15 @@ let EditionContainer = React.createClass({ EditionActions.fetchOne(this.props.params.editionId); }, + // This is done to update the container when the user clicks on the prev or next + // button to update the URL parameter (and therefore to switch pieces) + componentWillReceiveProps(nextProps) { + if(this.props.params.editionId !== nextProps.params.editionId) { + EditionActions.updateEdition({}); + EditionActions.fetchOne(nextProps.params.editionId); + } + }, + componentWillUnmount() { // Every time we're leaving the edition detail page, // just reset the edition that is saved in the edition store @@ -50,7 +61,7 @@ let EditionContainer = React.createClass({ }, render() { - if('title' in this.state.edition) { + if(this.state.edition && this.state.edition.title) { return ( file.status !== 'deleted' && file.status !== 'canceled'); - if(files.length > 0 && files[0].status === 'upload successful') { - return true; - } else { - return false; - } - }, - render() { - //return (); return ( @@ -90,93 +76,23 @@ let FurtherDetails = React.createClass({ editable={this.props.editable} pieceId={this.props.pieceId} extraData={this.props.extraData} /> - +
+ +
); } }); -let FileUploader = React.createClass({ - propTypes: { - pieceId: React.PropTypes.number, - otherData: React.PropTypes.object, - setIsUploadReady: React.PropTypes.func, - submitKey: React.PropTypes.func, - isReadyForFormSubmission: React.PropTypes.func, - editable: React.PropTypes.bool - }, - render() { - // Essentially there a three cases important to the fileuploader - // - // 1. there is no other_data => do not show the fileuploader at all - // 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons - // 3. both other_data and editable are defined or true => show fileuploade with all action buttons - if (!this.props.editable && !this.props.otherData){ - return null; - } - return ( -
- - - -
-
- ); - } -}); export default FurtherDetails; diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js new file mode 100644 index 00000000..9bf0bd5b --- /dev/null +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -0,0 +1,97 @@ +'use strict'; + +import React from 'react'; + +import Property from './../ascribe_forms/property'; + +import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getCookie } from '../../utils/fetch_api_utils'; + +let FurtherDetailsFileuploader = React.createClass({ + propTypes: { + uploadStarted: React.PropTypes.func, + pieceId: React.PropTypes.number, + otherData: React.PropTypes.arrayOf(React.PropTypes.object), + setIsUploadReady: React.PropTypes.func, + submitFile: React.PropTypes.func, + isReadyForFormSubmission: React.PropTypes.func, + editable: React.PropTypes.bool, + multiple: React.PropTypes.bool + }, + + getDefaultProps() { + return { + multiple: false + }; + }, + + render() { + // Essentially there a three cases important to the fileuploader + // + // 1. there is no other_data => do not show the fileuploader at all (where otherData is now an array) + // 2. there is other_data, but user has no edit rights => show fileuploader but without action buttons + // 3. both other_data and editable are defined or true => show fileuploader with all action buttons + if (!this.props.editable && (!this.props.otherData || this.props.otherData.length === 0)) { + return null; + } + + let otherDataIds = this.props.otherData ? this.props.otherData.map((data) => data.id).join() : null; + + return ( + + + + ); + } +}); + +export default FurtherDetailsFileuploader; \ No newline at end of file diff --git a/js/components/ascribe_detail/history_iterator.js b/js/components/ascribe_detail/history_iterator.js new file mode 100644 index 00000000..54d11a5b --- /dev/null +++ b/js/components/ascribe_detail/history_iterator.js @@ -0,0 +1,33 @@ +'use strict'; + +import React from 'react'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +let HistoryIterator = React.createClass({ + propTypes: { + history: React.PropTypes.array + }, + + render() { + return ( +
+ {this.props.history.map((historicalEvent, i) => { + return ( + +
{ historicalEvent[1] }
+
+ ); + })} +
+
+ ); + } +}); + +export default HistoryIterator; diff --git a/js/components/ascribe_detail/license_detail.js b/js/components/ascribe_detail/license_detail.js new file mode 100644 index 00000000..c3cc9f62 --- /dev/null +++ b/js/components/ascribe_detail/license_detail.js @@ -0,0 +1,31 @@ +'use strict'; + +import React from 'react'; + +import DetailProperty from './detail_property'; + +/** + * This is the component that implements display-specific functionality + */ +let LicenseDetail = React.createClass({ + propTypes: { + license: React.PropTypes.object + }, + render () { + if (this.props.license.code === 'default') { + return null; + } + return ( + + { this.props.license.code.toUpperCase() + ': ' + this.props.license.name} + + } + /> + ); + } +}); + +export default LicenseDetail; diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 2ff5a55d..6ac2f745 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -19,7 +19,33 @@ const EMBED_IFRAME_HEIGHT = { let MediaContainer = React.createClass({ propTypes: { - content: React.PropTypes.object + content: React.PropTypes.object, + refreshObject: React.PropTypes.func + }, + + getInitialState() { + return {timerId: null}; + }, + + componentDidMount() { + if (!this.props.content.digital_work) { + return; + } + let isEncoding = this.props.content.digital_work.isEncoding; + if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { + let timerId = window.setInterval(this.props.refreshObject, 10000); + this.setState({timerId: timerId}); + } + }, + + componentWillUpdate() { + if (this.props.content.digital_work.isEncoding === 100) { + window.clearInterval(this.state.timerId); + } + }, + + componentWillUnmount() { + window.clearInterval(this.state.timerId); }, render() { @@ -46,7 +72,7 @@ let MediaContainer = React.createClass({ } panel={
-                            {''}
                         
}/> diff --git a/js/components/ascribe_detail/note.js b/js/components/ascribe_detail/note.js new file mode 100644 index 00000000..c739b937 --- /dev/null +++ b/js/components/ascribe_detail/note.js @@ -0,0 +1,65 @@ +'use strict'; + +import React from 'react'; + +import Form from './../ascribe_forms/form'; +import Property from './../ascribe_forms/property'; +import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import { getLangText } from '../../utils/lang_utils'; + +let Note = React.createClass({ + propTypes: { + url: React.PropTypes.string, + id: React.PropTypes.func, + label: React.PropTypes.string, + currentUser: React.PropTypes.object, + defaultValue: React.PropTypes.string, + editable: React.PropTypes.bool, + show: React.PropTypes.bool, + placeholder: React.PropTypes.string, + successMessage: React.PropTypes.string + }, + + getDefaultProps() { + return { + editable: true, + show: true, + placeholder: getLangText('Enter a note'), + successMessage: getLangText('Note saved') + }; + }, + + showNotification(){ + let notification = new GlobalNotificationModel(this.props.successMessage, 'success'); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + render() { + if ((!!this.props.currentUser.username && this.props.editable || !this.props.editable ) && this.props.show) { + return ( +
+ + + +
+
+ ); + } + return null; + } +}); + +export default Note; \ No newline at end of file diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js index 3fabb055..ed312f5f 100644 --- a/js/components/ascribe_detail/piece.js +++ b/js/components/ascribe_detail/piece.js @@ -1,38 +1,14 @@ 'use strict'; import React from 'react'; -import Router from 'react-router'; import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; -import DetailProperty from './detail_property'; - -import UserActions from '../../actions/user_actions'; -import UserStore from '../../stores/user_store'; - -import PieceListActions from '../../actions/piece_list_actions'; -import PieceListStore from '../../stores/piece_list_store'; - -import EditionListActions from '../../actions/edition_list_actions'; - import PieceActions from '../../actions/piece_actions'; import MediaContainer from './media_container'; -import EditionDetailProperty from './detail_property'; - -import AclButtonList from './../ascribe_buttons/acl_button_list'; -import CreateEditionsForm from '../ascribe_forms/create_editions_form'; -import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; -import DeleteButton from '../ascribe_buttons/delete_button'; - -import GlobalNotificationModel from '../../models/global_notification_model'; -import GlobalNotificationActions from '../../actions/global_notification_actions'; - -import { getLangText } from '../../utils/lang_utils'; -import { mergeOptions } from '../../utils/general_utils'; - /** * This is the component that implements display-specific functionality @@ -40,97 +16,16 @@ import { mergeOptions } from '../../utils/general_utils'; let Piece = React.createClass({ propTypes: { piece: React.PropTypes.object, + header: React.PropTypes.object, + subheader: React.PropTypes.object, + buttons: React.PropTypes.object, loadPiece: React.PropTypes.func, children: React.PropTypes.object }, - mixins: [Router.Navigation], - getInitialState() { - return mergeOptions( - UserStore.getState(), - PieceListStore.getState(), - { - showCreateEditionsDialog: false - } - ); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - PieceListStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - PieceListStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - toggleCreateEditionsDialog() { - this.setState({ - showCreateEditionsDialog: !this.state.showCreateEditionsDialog - }); - }, - - handleEditionCreationSuccess() { - PieceActions.updateProperty({key: 'num_editions', value: 0}); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - this.toggleCreateEditionsDialog(); - }, - - handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - - // since we're deleting a piece, we just need to close - // all editions dialogs and not reload them - EditionListActions.closeAllEditionLists(); - EditionListActions.clearAllEditionSelections(); - - let notification = new GlobalNotificationModel(response.notification, 'success'); - GlobalNotificationActions.appendGlobalNotification(notification); - - this.transitionTo('pieces'); - }, - - getCreateEditionsDialog() { - if(this.props.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { - return ( -
- -
-
- ); - } else { - return (
); - } - }, - - handlePollingSuccess(pieceId, numEditions) { - - // we need to refresh the num_editions property of the actual piece we're looking at - PieceActions.updateProperty({ - key: 'num_editions', - value: numEditions - }); - - // as well as its representation in the collection - // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion - // list item also uses the firstEdition property which we can only get from the server in that case. - // Therefore we need to at least refetch the changed piece from the server or on our case simply all - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); - GlobalNotificationActions.appendGlobalNotification(notification); + updateObject() { + return PieceActions.fetchOne(this.props.piece.id); }, render() { @@ -138,38 +33,14 @@ let Piece = React.createClass({ -
-

{this.props.piece.title}

-
- - - {this.props.piece.num_editions > 0 ? : null} -
-
-
- -
+ {this.props.header} + {this.props.subheader} + {this.props.buttons} - - - - - - {this.getCreateEditionsDialog()} {this.props.children} diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 8e3a5750..c2eb1759 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -1,37 +1,66 @@ 'use strict'; import React from 'react'; +import Router from 'react-router'; import PieceActions from '../../actions/piece_actions'; import PieceStore from '../../stores/piece_store'; +import PieceListActions from '../../actions/piece_list_actions'; +import PieceListStore from '../../stores/piece_list_store'; + +import UserActions from '../../actions/user_actions'; +import UserStore from '../../stores/user_store'; + +import EditionListActions from '../../actions/edition_list_actions'; + import Piece from './piece'; import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; import FurtherDetails from './further_details'; +import DetailProperty from './detail_property'; +import LicenseDetail from './license_detail'; +import HistoryIterator from './history_iterator'; + +import AclButtonList from './../ascribe_buttons/acl_button_list'; +import CreateEditionsForm from '../ascribe_forms/create_editions_form'; +import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; +import DeleteButton from '../ascribe_buttons/delete_button'; + +import ListRequestActions from '../ascribe_forms/list_form_request_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Note from './note'; + +import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; +import { mergeOptions } from '../../utils/general_utils'; +import { getLangText } from '../../utils/lang_utils'; /** * This is the component that implements resource/data specific functionality */ let PieceContainer = React.createClass({ - getInitialState() { - return PieceStore.getState(); - }, - onChange(state) { - this.setState(state); - if (!state.piece.digital_work) { - return; - } - let isEncoding = state.piece.digital_work.isEncoding; - if (state.piece.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - let timerId = window.setInterval(() => PieceActions.fetchOne(this.props.params.pieceId), 10000); - this.setState({timerId: timerId}); - } + mixins: [Router.Navigation], + + getInitialState() { + return mergeOptions( + UserStore.getState(), + PieceListStore.getState(), + PieceStore.getState(), + { + showCreateEditionsDialog: false + } + ); }, componentDidMount() { + UserStore.listen(this.onChange); + PieceListStore.listen(this.onChange); + UserActions.fetchCurrentUser(); PieceStore.listen(this.onChange); PieceActions.fetchOne(this.props.params.pieceId); }, @@ -42,26 +71,190 @@ let PieceContainer = React.createClass({ // as it will otherwise display wrong/old data once the user loads // the piece detail a second time PieceActions.updatePiece({}); - window.clearInterval(this.state.timerId); PieceStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + PieceListStore.unlisten(this.onChange); }, + onChange(state) { + /* + + ATTENTION: + THIS IS JUST A TEMPORARY USABILITY FIX THAT ESSENTIALLY REMOVES THE LOAN BUTTON + FROM THE PIECE DETAIL PAGE SO THAT USERS DO NOT CONFUSE A PIECE WITH AN EDITION. + + IT SHOULD BE REMOVED AND REPLACED WITH A BETTER SOLUTION ASAP! + + ALSO, WE ENABLED THE LOAN BUTTON FOR IKONOTV TO LET THEM LOAN ON A PIECE LEVEL + + */ + if(state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { + + let pieceState = mergeOptions({}, state.piece); + pieceState.acl.acl_loan = false; + + this.setState({ + piece: pieceState + }); + + } else { + this.setState(state); + } + }, loadPiece() { PieceActions.fetchOne(this.props.params.pieceId); }, + + toggleCreateEditionsDialog() { + this.setState({ + showCreateEditionsDialog: !this.state.showCreateEditionsDialog + }); + }, + + handleEditionCreationSuccess() { + PieceActions.updateProperty({key: 'num_editions', value: 0}); + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + this.toggleCreateEditionsDialog(); + }, + + handleDeleteSuccess(response) { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + + // since we're deleting a piece, we just need to close + // all editions dialogs and not reload them + EditionListActions.closeAllEditionLists(); + EditionListActions.clearAllEditionSelections(); + + let notification = new GlobalNotificationModel(response.notification, 'success'); + GlobalNotificationActions.appendGlobalNotification(notification); + + this.transitionTo('pieces'); + }, + + getCreateEditionsDialog() { + if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { + return ( +
+ +
+
+ ); + } else { + return (
); + } + }, + + handlePollingSuccess(pieceId, numEditions) { + + // we need to refresh the num_editions property of the actual piece we're looking at + PieceActions.updateProperty({ + key: 'num_editions', + value: numEditions + }); + + // as well as its representation in the collection + // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion + // list item also uses the firstEdition property which we can only get from the server in that case. + // Therefore we need to at least refetch the changed piece from the server or on our case simply all + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + + let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getId() { + return {'id': this.state.piece.id}; + }, + + getActions() { + if (this.state.piece && + this.state.piece.notifications && + this.state.piece.notifications.length > 0) { + return ( + ); + } + else { + return ( + + + + + ); + } + }, + render() { - if('title' in this.state.piece) { + if(this.state.piece && this.state.piece.title) { return ( + loadPiece={this.loadPiece} + header={ +
+
+

{this.state.piece.title}

+ + + {this.state.piece.num_editions > 0 ? : null} +
+
+ } + subheader={ +
+ + + +
+ } + buttons={this.getActions()}> + {this.getCreateEditionsDialog()} + 0}> + + + + + + 0 - || this.state.piece.other_data !== null} + || this.state.piece.other_data.length > 0} defaultExpanded={true}> +
); } else { diff --git a/js/components/ascribe_forms/create_editions_form.js b/js/components/ascribe_forms/create_editions_form.js index a9c44993..cd5a22d3 100644 --- a/js/components/ascribe_forms/create_editions_form.js +++ b/js/components/ascribe_forms/create_editions_form.js @@ -8,12 +8,11 @@ import Property from '../ascribe_forms/property'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; import { getLangText } from '../../utils/lang_utils'; let CreateEditionsForm = React.createClass({ - propTypes: { handleSuccess: React.PropTypes.func, pieceId: React.PropTypes.number @@ -38,9 +37,15 @@ let CreateEditionsForm = React.createClass({ return (
+ {getLangText('Create editions')} + } spinner={ - + +

); @@ -143,6 +224,7 @@ let Form = React.createClass({ } return buttons; }, + getErrors() { let errors = null; if (this.state.errors.length > 0){ @@ -152,16 +234,41 @@ let Form = React.createClass({ } return errors; }, + renderChildren() { return ReactAddons.Children.map(this.props.children, (child) => { if (child) { return ReactAddons.addons.cloneWithProps(child, { handleChange: this.handleChangeChild, - ref: child.props.name + ref: child.props.name, + + // We need this in order to make editable be overridable when setting it directly + // on Property + editable: child.props.overrideForm ? child.props.editable : !this.props.disabled }); } }); }, + + /** + * All webkit-based browsers are ignoring the attribute autoComplete="off", + * as stated here: http://stackoverflow.com/questions/15738259/disabling-chrome-autofill/15917221#15917221 + * So what we actually have to do is depended on whether or not this.props.autoComplete is set to "on" or "off" + * insert two fake hidden inputs that mock password and username so that chrome/safari is filling those + */ + getFakeAutocompletableInputs() { + if(this.props.autoComplete === 'off') { + return ( + + + + + ); + } else { + return null; + } + }, + render() { let className = 'ascribe-form'; @@ -174,7 +281,9 @@ let Form = React.createClass({ role="form" className={className} onSubmit={this.submit} - autoComplete="on"> + onReset={this.reset} + autoComplete={this.props.autoComplete}> + {this.getFakeAutocompletableInputs()} {this.getErrors()} {this.renderChildren()} {this.getButtons()} diff --git a/js/components/ascribe_forms/form_consign.js b/js/components/ascribe_forms/form_consign.js index 5815efdd..de4a4788 100644 --- a/js/components/ascribe_forms/form_consign.js +++ b/js/components/ascribe_forms/form_consign.js @@ -8,7 +8,6 @@ import Form from './form'; import Property from './property'; import InputTextAreaToggable from './input_textarea_toggable'; - import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils.js'; @@ -18,7 +17,6 @@ let ConsignForm = React.createClass({ url: React.PropTypes.string, id: React.PropTypes.object, message: React.PropTypes.string, - onRequestHide: React.PropTypes.func, handleSuccess: React.PropTypes.func }, @@ -27,7 +25,6 @@ let ConsignForm = React.createClass({ }, render() { - return ( - + type="submit"> + {getLangText('CONSIGN')} +

} spinner={ @@ -61,10 +56,10 @@ let ConsignForm = React.createClass({ + editable={true} + overrideForm={true}> diff --git a/js/components/ascribe_forms/form_contract_agreement.js b/js/components/ascribe_forms/form_contract_agreement.js new file mode 100644 index 00000000..7064a40c --- /dev/null +++ b/js/components/ascribe_forms/form_contract_agreement.js @@ -0,0 +1,149 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; + +import ContractListActions from '../../actions/contract_list_actions'; +import ContractListStore from '../../stores/contract_list_store'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Form from './form'; +import Property from './property'; +import PropertyCollapsible from './property_collapsible'; +import InputTextAreaToggable from './input_textarea_toggable'; + +import ApiUrls from '../../constants/api_urls'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; + + +let ContractAgreementForm = React.createClass({ + propTypes: { + handleSuccess: React.PropTypes.func + }, + + mixins: [Router.Navigation, Router.State], + + getInitialState() { + return mergeOptions( + ContractListStore.getState(), + { + selectedContract: 0 + } + ); + }, + + componentDidMount() { + ContractListStore.listen(this.onChange); + ContractListActions.fetchContractList(true, false); + }, + + componentWillUnmount() { + ContractListStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + onContractChange(event){ + this.setState({selectedContract: event.target.selectedIndex}); + }, + + handleSubmitSuccess() { + let notification = 'Contract agreement send'; + notification = new GlobalNotificationModel(notification, 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + this.transitionTo('pieces'); + }, + + getFormData(){ + return {'appendix': {'default': this.refs.form.refs.appendix.state.value}}; + }, + + getContracts() { + if (this.state.contractList && this.state.contractList.length > 0) { + let contractList = this.state.contractList; + return ( + + + ); + } + return null; + }, + + render() { + if (this.state.contractList && this.state.contractList.length > 0) { + return ( + + {getLangText('Send contract')} + } + spinner={ + + + + }> +
+

{getLangText('Contract form')}

+
+ + + + {this.getContracts()} + + {getLangText('Appendix')} + {/* We're using disabled on a form here as PropertyCollapsible currently + does not support the disabled + overrideForm functionality */} + + + + ); + } + return ( +
+

+ {getLangText('No contracts uploaded yet, please go to the ')} + {getLangText('settings page')} + {getLangText(' and create them.')} +

+
+ ); + } +}); + +export default ContractAgreementForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_copyright_association.js b/js/components/ascribe_forms/form_copyright_association.js new file mode 100644 index 00000000..da14e76d --- /dev/null +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -0,0 +1,80 @@ +'use strict'; + +import React from 'react'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Form from './form'; +import Property from './property'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + +let CopyrightAssociationForm = React.createClass({ + propTypes: { + currentUser: React.PropTypes.object + }, + + handleSubmitSuccess(){ + let notification = getLangText('Copyright association updated'); + notification = new GlobalNotificationModel(notification, 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getProfileFormData(){ + return {email: this.props.currentUser.email}; + }, + + render() { + let selectedState; + let selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- '; + + if (this.props.currentUser && this.props.currentUser.profile + && this.props.currentUser.profile.copyright_association) { + selectedState = AppConstants.copyrightAssociations.indexOf(this.props.currentUser.profile.copyright_association); + selectedState = selectedState !== -1 ? AppConstants.copyrightAssociations[selectedState] : selectDefaultValue; + } + + if (this.props.currentUser && this.props.currentUser.email){ + return ( +
+ + + +
+
+ ); + } + return null; + } +}); + +export default CopyrightAssociationForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_create_contract.js b/js/components/ascribe_forms/form_create_contract.js new file mode 100644 index 00000000..b19cb050 --- /dev/null +++ b/js/components/ascribe_forms/form_create_contract.js @@ -0,0 +1,111 @@ +'use strict'; + +import React from 'react'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +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 { getLangText } from '../../utils/lang_utils'; +import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; + + +let CreateContractForm = React.createClass({ + propTypes: { + isPublic: React.PropTypes.bool, + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }) + }, + + getInitialState() { + return { + isUploadReady: false, + contractName: '' + }; + }, + + setIsUploadReady(isReady) { + this.setState({ + isUploadReady: isReady + }); + }, + + handleCreateSuccess(response) { + ContractListActions.fetchContractList(true); + let notification = new GlobalNotificationModel(getLangText('Contract %s successfully created', response.name), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + this.refs.form.reset(); + }, + + submitFileName(fileName) { + this.setState({ + contractName: fileName + }); + + this.refs.form.submit(); + }, + + render() { + return ( +
+ + + + + +
+ ); + } +}); + +export default CreateContractForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_delete_edition.js b/js/components/ascribe_forms/form_delete_edition.js index c05a20bc..9eb721f6 100644 --- a/js/components/ascribe_forms/form_delete_edition.js +++ b/js/components/ascribe_forms/form_delete_edition.js @@ -2,33 +2,65 @@ import React from 'react'; -import requests from '../../utils/requests'; +import Form from './form'; + import ApiUrls from '../../constants/api_urls'; -import FormMixin from '../../mixins/form_mixin'; +import AppConstants from '../../constants/application_constants'; + import { getLangText } from '../../utils/lang_utils'; + let EditionDeleteForm = React.createClass({ - mixins: [FormMixin], + propTypes: { + editions: React.PropTypes.arrayOf(React.PropTypes.object), - url() { - return requests.prepareUrl(ApiUrls.edition_delete, {edition_id: this.getBitcoinIds().join()}); - }, - httpVerb(){ - return 'delete'; + // Propagated by ModalWrapper in most cases + handleSuccess: React.PropTypes.func }, - renderForm () { + getBitcoinIds() { + return this.props.editions.map(function(edition){ + return edition.bitcoin_id; + }); + }, + + // Since this form can be used for either deleting a single edition or multiple + // we need to call getBitcoinIds to get the value of edition_id + getFormData() { + return { + edition_id: this.getBitcoinIds().join(',') + }; + }, + + render () { return ( -
+
+

+ +

+
+ } + spinner={ +
+ +
+ }>

{getLangText('Are you sure you would like to permanently delete this edition')}?

{getLangText('This is an irrevocable action%s', '.')}

-
- - -
-
+ ); } }); diff --git a/js/components/ascribe_forms/form_delete_piece.js b/js/components/ascribe_forms/form_delete_piece.js index 168d9261..552c38c0 100644 --- a/js/components/ascribe_forms/form_delete_piece.js +++ b/js/components/ascribe_forms/form_delete_piece.js @@ -2,37 +2,56 @@ import React from 'react'; -import requests from '../../utils/requests'; +import Form from '../ascribe_forms/form'; + import ApiUrls from '../../constants/api_urls'; -import FormMixin from '../../mixins/form_mixin'; +import AppConstants from '../../constants/application_constants'; + import { getLangText } from '../../utils/lang_utils'; + let PieceDeleteForm = React.createClass({ propTypes: { - pieceId: React.PropTypes.number + pieceId: React.PropTypes.number, + + // Propagated by ModalWrapper in most cases + handleSuccess: React.PropTypes.func }, - mixins: [FormMixin], - - url() { - return requests.prepareUrl(ApiUrls.piece, {piece_id: this.props.pieceId}); + getFormData() { + return { + piece_id: this.props.pieceId + }; }, - httpVerb() { - return 'delete'; - }, - - renderForm () { + render() { return ( -
+
+

+ +

+
+ } + spinner={ +
+ +
+ }>

{getLangText('Are you sure you would like to permanently delete this piece')}?

{getLangText('This is an irrevocable action%s', '.')}

-
- - -
-
+ ); } }); diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index 7aae656a..ef2fbd13 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -2,6 +2,8 @@ import React from 'react'; +import classnames from 'classnames'; + import Button from 'react-bootstrap/lib/Button'; import Form from './form'; @@ -10,70 +12,150 @@ import InputTextAreaToggable from './input_textarea_toggable'; import InputDate from './input_date'; import InputCheckbox from './input_checkbox'; -import LoanContractStore from '../../stores/loan_contract_store'; -import LoanContractActions from '../../actions/loan_contract_actions'; +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; +import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; import AppConstants from '../../constants/application_constants'; +import { mergeOptions } from '../../utils/general_utils'; import { getLangText } from '../../utils/lang_utils'; let LoanForm = React.createClass({ propTypes: { + loanHeading: React.PropTypes.string, + email: React.PropTypes.string, + gallery: React.PropTypes.string, + startdate: React.PropTypes.object, + enddate: React.PropTypes.object, + showPersonalMessage: React.PropTypes.bool, + showEndDate: React.PropTypes.bool, + showStartDate: React.PropTypes.bool, + showPassword: React.PropTypes.bool, url: React.PropTypes.string, id: React.PropTypes.object, message: React.PropTypes.string, - onRequestHide: React.PropTypes.func, + createPublicContractAgreement: React.PropTypes.bool, handleSuccess: React.PropTypes.func }, + getDefaultProps() { + return { + loanHeading: '', + showPersonalMessage: true, + showEndDate: true, + showStartDate: true, + showPassword: true, + createPublicContractAgreement: true + }; + }, + getInitialState() { - return LoanContractStore.getState(); + return ContractAgreementListStore.getState(); }, componentDidMount() { - LoanContractStore.listen(this.onChange); - LoanContractActions.flushLoanContract(); + ContractAgreementListStore.listen(this.onChange); + this.getContractAgreementsOrCreatePublic(this.props.email); + }, + + /** + * This method needs to be in form_loan as some whitelabel pages (Cyland) load + * the loanee's email async! + * + * SO LEAVE IT IN! + */ + componentWillReceiveProps(nextProps) { + if(nextProps && nextProps.email && this.props.email !== nextProps.email) { + this.getContractAgreementsOrCreatePublic(nextProps.email); + } }, componentWillUnmount() { - LoanContractStore.unlisten(this.onChange); + ContractAgreementListStore.unlisten(this.onChange); }, onChange(state) { this.setState(state); }, - getFormData(){ - return this.props.id; + getContractAgreementsOrCreatePublic(email){ + ContractAgreementListActions.flushContractAgreementList.defer(); + if (email) { + // fetch the available contractagreements (pending/accepted) + ContractAgreementListActions.fetchAvailableContractAgreementList(email, true); + } }, - handleOnBlur(event) { - LoanContractActions.fetchLoanContract(event.target.value); + getFormData(){ + return mergeOptions( + this.props.id, + this.getContractAgreementId() + ); + }, + + handleOnChange(event) { + // event.target.value is the submitted email of the loanee + if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) { + this.getContractAgreementsOrCreatePublic(event.target.value); + } else { + ContractAgreementListActions.flushContractAgreementList(); + } + }, + + getContractAgreementId() { + if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { + return {'contract_agreement_id': this.state.contractAgreementList[0].id}; + } + return {}; }, getContractCheckbox() { - if(this.state.contractKey && this.state.contractUrl) { + if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { // we need to define a key on the InputCheckboxes as otherwise // react is not rerendering them on a store switch and is keeping // the default value of the component (which is in that case true) - return ( - - - - {getLangText('I agree to the')}  - - {getLangText('terms of')} {this.state.contractEmail} - - - - - ); + let contractAgreement = this.state.contractAgreementList[0]; + let contract = contractAgreement.contract; + + if(contractAgreement.datetime_accepted) { + return ( + + ); + } else { + return ( + + + + {getLangText('I agree to the')}  + + {getLangText('terms of ')} {contract.issuer} + + + + + ); + } } else { return ( 0) { + let appendix = this.state.contractAgreementList[0].appendix; + if (appendix && appendix.default) { + return ( + +
{appendix.default}
+
+ ); + } + } + return null; + }, + getButtons() { + if(this.props.loanHeading) { + return ( + + ); + } else { + return ( +
+

+ +

+
+ ); + } + }, + + render() { return (
-

- - -

- } + buttons={this.getButtons()} spinner={
}> +
+

{this.props.loanHeading}

+
+ editable={!this.props.email} + onChange={this.handleOnChange} + overrideForm={!!this.props.email}> + name='gallery' + label={getLangText('Gallery/exhibition (optional)')} + editable={!this.props.gallery} + overrideForm={!!this.props.gallery}> + label={getLangText('Start date')} + editable={!this.props.startdate} + overrideForm={!!this.props.startdate} + hidden={!this.props.showStartDate}> + editable={!this.props.enddate} + overrideForm={!!this.props.enddate} + label={getLangText('End date')} + hidden={!this.props.showEndDate}> + editable={true} + overrideForm={true} + hidden={!this.props.showPersonalMessage}> + required={this.props.showPersonalMessage ? 'required' : ''}/> - + {this.getContractCheckbox()} + {this.getAppendix()} + label={getLangText('Password')} + hidden={!this.props.showPassword}> + required={this.props.showPassword ? 'required' : ''}/> - {this.getContractCheckbox()} + {this.props.children}
); } diff --git a/js/components/ascribe_forms/form_loan_request_answer.js b/js/components/ascribe_forms/form_loan_request_answer.js new file mode 100644 index 00000000..1bfe90db --- /dev/null +++ b/js/components/ascribe_forms/form_loan_request_answer.js @@ -0,0 +1,79 @@ +'use strict'; + +import React from 'react'; +import Moment from 'moment'; + +import LoanForm from './form_loan'; + +import OwnershipActions from '../../actions/ownership_actions'; +import OwnershipStore from '../../stores/ownership_store'; + +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + + +let LoanRequestAnswerForm = React.createClass({ + propTypes: { + url: React.PropTypes.string, + id: React.PropTypes.object, + message: React.PropTypes.string, + handleSuccess: React.PropTypes.func.isRequired + }, + + getDefaultProps() { + return { + loanHeading: '', + showPersonalMessage: true, + showEndDate: false, + showStartDate: false, + showPassword: true + }; + }, + + getInitialState() { + return OwnershipStore.getState(); + }, + + componentDidMount() { + OwnershipStore.listen(this.onChange); + OwnershipActions.fetchLoanRequest(this.props.id.piece_id); + }, + + componentWillUnmount() { + OwnershipStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + + render() { + let startDate = null; + let endDate = null; + + if (this.state.loanRequest) { + startDate = new Moment(this.state.loanRequest.datetime_from, Moment.ISO_8601); + endDate = new Moment(this.state.loanRequest.datetime_to, Moment.ISO_8601); + + return ( + + ); + } + return ; + } +}); + +export default LoanRequestAnswerForm; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 24b0eb93..79bced6c 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -11,16 +11,14 @@ import UserActions from '../../actions/user_actions'; import Form from './form'; import Property from './property'; -import FormPropertyHeader from './form_property_header'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils'; let LoginForm = React.createClass({ - propTypes: { headerMessage: React.PropTypes.string, submitMessage: React.PropTypes.string, @@ -29,7 +27,7 @@ let LoginForm = React.createClass({ onLogin: React.PropTypes.func }, - mixins: [Router.Navigation], + mixins: [Router.Navigation, Router.State], getDefaultProps() { return { @@ -97,12 +95,14 @@ let LoginForm = React.createClass({ }, render() { + let email = this.getQuery().email || null; return (
}> - +

{this.props.headerMessage}

- +
diff --git a/js/components/ascribe_forms/form_piece_extradata.js b/js/components/ascribe_forms/form_piece_extradata.js index bbec9dca..45f684ad 100644 --- a/js/components/ascribe_forms/form_piece_extradata.js +++ b/js/components/ascribe_forms/form_piece_extradata.js @@ -3,9 +3,9 @@ import React from 'react'; import requests from '../../utils/requests'; -import { getLangText } from '../../utils/lang_utils.js' +import { getLangText } from '../../utils/lang_utils.js'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; import Form from './form'; import Property from './property'; @@ -20,7 +20,8 @@ let PieceExtraDataForm = React.createClass({ title: React.PropTypes.string, editable: React.PropTypes.bool }, - getFormData(){ + + getFormData() { let extradata = {}; extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value; return { @@ -28,25 +29,25 @@ let PieceExtraDataForm = React.createClass({ piece_id: this.props.pieceId }; }, + render() { let defaultValue = this.props.extraData[this.props.name] || ''; if (defaultValue.length === 0 && !this.props.editable){ return null; } - let url = requests.prepareUrl(apiUrls.piece_extradata, {piece_id: this.props.pieceId}); + let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId}); return ( + getFormData={this.getFormData} + disabled={!this.props.editable}> + label={this.props.title}> diff --git a/js/components/ascribe_forms/form_property_header.js b/js/components/ascribe_forms/form_property_header.js deleted file mode 100644 index 85e027c1..00000000 --- a/js/components/ascribe_forms/form_property_header.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -import React from 'react'; - -let FormPropertyHeader = React.createClass({ - propTypes: { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) - }, - - render() { - return ( -
- {this.props.children} -
- ); - } -}); - -export default FormPropertyHeader; \ No newline at end of file diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 853506f6..118b3968 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -7,16 +7,14 @@ import UserActions from '../../actions/user_actions'; import Form from './form'; import Property from './property'; -import FormPropertyHeader from './form_property_header'; - -import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader'; +import InputFineUploader from './input_fineuploader'; +import ApiUrls from '../../constants/api_urls'; 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'; let RegisterPieceForm = React.createClass({ @@ -25,9 +23,13 @@ let RegisterPieceForm = React.createClass({ submitMessage: React.PropTypes.string, handleSuccess: React.PropTypes.func, isFineUploaderActive: React.PropTypes.bool, + isFineUploaderEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, children: React.PropTypes.element, - onLoggedOut: React.PropTypes.func + onLoggedOut: React.PropTypes.func, + + // For this form to work with SlideContainer, we sometimes have to disable it + disabled: React.PropTypes.bool }, getDefaultProps() { @@ -41,7 +43,6 @@ let RegisterPieceForm = React.createClass({ getInitialState(){ return mergeOptions( { - digitalWorkKey: null, isUploadReady: false }, UserStore.getState() @@ -61,67 +62,57 @@ 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 }); }, - isReadyForFormSubmission(files) { - files = files.filter((file) => file.status !== 'deleted' && file.status !== 'canceled'); - if (files.length > 0 && files[0].status === 'upload successful') { - return true; - } else { - return false; - } - }, - render() { let currentUser = this.state.currentUser; let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false; enableLocalHashing = enableLocalHashing && this.props.enableLocalHashing; + return ( - {this.props.submitMessage} - } + buttons={ + + } spinner={ }> - +

{this.props.headerMessage}

- +
- {this.props.children} @@ -155,60 +146,4 @@ let RegisterPieceForm = React.createClass({ } }); -let FileUploader = React.createClass({ - propTypes: { - setIsUploadReady: React.PropTypes.func, - submitKey: 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 - }, - - render() { - return ( - - ); - } -}); - export default RegisterPieceForm; diff --git a/js/components/ascribe_forms/form_remove_editions_from_collection.js b/js/components/ascribe_forms/form_remove_editions_from_collection.js index 4ab8fdf7..7c0bee01 100644 --- a/js/components/ascribe_forms/form_remove_editions_from_collection.js +++ b/js/components/ascribe_forms/form_remove_editions_from_collection.js @@ -2,34 +2,63 @@ import React from 'react'; -import { getLangText } from '../../utils/lang_utils.js'; -import requests from '../../utils/requests'; -import apiUrls from '../../constants/api_urls'; -import FormMixin from '../../mixins/form_mixin'; +import Form from './form'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; let EditionRemoveFromCollectionForm = React.createClass({ + propTypes: { + editions: React.PropTypes.arrayOf(React.PropTypes.object), - mixins: [FormMixin], - - url() { - return requests.prepareUrl(apiUrls.edition_remove_from_collection, {edition_id: this.getBitcoinIds().join()}); - }, - - httpVerb(){ - return 'delete'; + // Propagated by ModalWrapper in most cases + handleSuccess: React.PropTypes.func }, - renderForm () { + getBitcoinIds() { + return this.props.editions.map(function(edition){ + return edition.bitcoin_id; + }); + }, + + // Since this form can be used for either removing a single edition or multiple + // we need to call getBitcoinIds to get the value of edition_id + getFormData() { + return { + edition_id: this.getBitcoinIds().join(',') + }; + }, + + render() { return ( -
+ +

+ +

+
+ } + spinner={ +
+ +
+ }>

{getLangText('Are you sure you would like to remove these editions from your collection')}?

{getLangText('This is an irrevocable action%s', '.')}

-
- - -
- + ); } }); diff --git a/js/components/ascribe_forms/form_remove_piece_from_collection.js b/js/components/ascribe_forms/form_remove_piece_from_collection.js index 905cfcf6..d827c2ee 100644 --- a/js/components/ascribe_forms/form_remove_piece_from_collection.js +++ b/js/components/ascribe_forms/form_remove_piece_from_collection.js @@ -2,38 +2,56 @@ import React from 'react'; -import { getLangText } from '../../utils/lang_utils.js'; -import requests from '../../utils/requests'; -import apiUrls from '../../constants/api_urls'; -import FormMixin from '../../mixins/form_mixin'; +import Form from './form'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + let PieceRemoveFromCollectionForm = React.createClass({ - propTypes: { - pieceId: React.PropTypes.number + pieceId: React.PropTypes.number, + + // Propagated by ModalWrapper in most cases + handleSuccess: React.PropTypes.func }, - mixins: [FormMixin], - - url() { - return requests.prepareUrl(apiUrls.piece_remove_from_collection, {piece_id: this.props.pieceId}); - }, - - httpVerb(){ - return 'delete'; + getFormData() { + return { + piece_id: this.props.pieceId + }; }, - renderForm () { + render () { return ( -
+
+

+ +

+
+ } + spinner={ +
+ +
+ }>

{getLangText('Are you sure you would like to remove this piece from your collection')}?

{getLangText('This is an irrevocable action%s', '.')}

-
- - -
- + ); } }); diff --git a/js/components/ascribe_forms/form_request_action.js b/js/components/ascribe_forms/form_request_action.js index 622aa02f..b0f3b6c6 100644 --- a/js/components/ascribe_forms/form_request_action.js +++ b/js/components/ascribe_forms/form_request_action.js @@ -2,98 +2,173 @@ import React from 'react'; -import Alert from 'react-bootstrap/lib/Alert'; - -import apiUrls from '../../constants/api_urls'; -import FormMixin from '../../mixins/form_mixin'; - import AclButton from './../ascribe_buttons/acl_button'; +import ActionPanel from '../ascribe_panel/action_panel'; +import Form from './form'; + +import NotificationActions from '../../actions/notification_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import ApiUrls from '../../constants/api_urls'; -import AppConstants from '../../constants/application_constants'; import { getLangText } from '../../utils/lang_utils.js'; + let RequestActionForm = React.createClass({ - mixins: [FormMixin], + propTypes: { + pieceOrEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]).isRequired, + notifications: React.PropTypes.object, + currentUser: React.PropTypes.object, + handleSuccess: React.PropTypes.func + }, - url(e){ - let edition = this.props.editions[0]; - if (e.target.id === 'request_accept'){ - if (edition.request_action === 'consign'){ - return apiUrls.ownership_consigns_confirm; - } - else if (edition.request_action === 'unconsign'){ - return apiUrls.ownership_unconsigns; - } - else if (edition.request_action === 'loan'){ - return apiUrls.ownership_loans_confirm; - } + isPiece(){ + return this.props.pieceOrEditions.constructor !== Array; + }, + + getUrls() { + let urls = {}; + + if (this.props.notifications.action === 'consign'){ + urls.accept = ApiUrls.ownership_consigns_confirm; + urls.deny = ApiUrls.ownership_consigns_deny; + } else if (this.props.notifications.action === 'unconsign'){ + urls.accept = ApiUrls.ownership_unconsigns; + urls.deny = ApiUrls.ownership_unconsigns_deny; + } 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.notifications.action === 'loan' && this.isPiece()){ + urls.accept = ApiUrls.ownership_loans_pieces_confirm; + urls.deny = ApiUrls.ownership_loans_pieces_deny; + } 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; } - else if(e.target.id === 'request_deny'){ - if (edition.request_action === 'consign') { - return apiUrls.ownership_consigns_deny; - } - else if (edition.request_action === 'unconsign') { - return apiUrls.ownership_unconsigns_deny; - } - else if (edition.request_action === 'loan') { - return apiUrls.ownership_loans_deny; - } + + return urls; + }, + + getFormData(){ + if (this.isPiece()) { + return {piece_id: this.props.pieceOrEditions.id}; + } + else { + return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){ + return edition.bitcoin_id; + }).join()}; } }, - handleRequest: function(e){ - e.preventDefault(); - this.submit(e); - }, + showNotification(option, action, owner) { + return () => { + let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner; + + let notifications = new GlobalNotificationModel(message, 'success'); + GlobalNotificationActions.appendGlobalNotification(notifications); + + this.handleSuccess(); - getFormData() { - return { - bitcoin_id: this.getBitcoinIds().join() }; }, - renderForm() { - let edition = this.props.editions[0]; - let buttonAccept = ( -
{getLangText('ACCEPT')} -
); - if (edition.request_action === 'unconsign'){ - console.log(this.props) - buttonAccept = ( + handleSuccess() { + if (this.isPiece()){ + NotificationActions.fetchPieceListNotifications(); + } + else { + NotificationActions.fetchEditionListNotifications(); + } + if(this.props.handleSuccess) { + this.props.handleSuccess(); + } + }, + + getContent() { + return ( + + {this.props.notifications.action_str + ' by ' + this.props.notifications.by} + + ); + }, + + getAcceptButtonForm(urls) { + if(this.props.notifications.action === 'unconsign') { + return ( + handleSuccess={this.handleSuccess} /> ); - } - let buttons = ( - - - {buttonAccept} - - -
{getLangText('REJECT')}
-
-
- ); - if (this.state.submitted){ - buttons = ( - - - + } else if(this.props.notifications.action === 'loan_request') { + return ( + + ); + } else { + return ( +
+ +
); } + }, + + getButtonForm() { + let urls = this.getUrls(); + let acceptButtonForm = this.getAcceptButtonForm(urls); + return ( - -
-
{ edition.owner } {getLangText('requests you')} { edition.request_action } {getLangText('this edition%s', '.')}  
- {buttons} -
-
+
+
+ +
+ {acceptButtonForm} +
+ ); + }, + + render() { + return ( + ); } }); diff --git a/js/components/ascribe_forms/form_share_email.js b/js/components/ascribe_forms/form_share_email.js index 881c9683..46ab97df 100644 --- a/js/components/ascribe_forms/form_share_email.js +++ b/js/components/ascribe_forms/form_share_email.js @@ -2,14 +2,14 @@ import React from 'react'; - - import Form from './form'; import Property from './property'; import InputTextAreaToggable from './input_textarea_toggable'; + import Button from 'react-bootstrap/lib/Button'; import AppConstants from '../../constants/application_constants'; + import { getLangText } from '../../utils/lang_utils.js'; @@ -20,7 +20,6 @@ let ShareForm = React.createClass({ message: React.PropTypes.string, editions: React.PropTypes.array, currentUser: React.PropTypes.object, - onRequestHide: React.PropTypes.func, handleSuccess: React.PropTypes.func }, @@ -41,11 +40,9 @@ let ShareForm = React.createClass({

- + type="submit"> + SHARE +

} spinner={ @@ -63,10 +60,10 @@ let ShareForm = React.createClass({ + editable={true} + overrideForm={true}> diff --git a/js/components/ascribe_forms/form_signup.js b/js/components/ascribe_forms/form_signup.js index 55aff4b8..790e8e2f 100644 --- a/js/components/ascribe_forms/form_signup.js +++ b/js/components/ascribe_forms/form_signup.js @@ -12,10 +12,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import Form from './form'; import Property from './property'; -import FormPropertyHeader from './form_property_header'; import InputCheckbox from './input_checkbox'; -import apiUrls from '../../constants/api_urls'; +import ApiUrls from '../../constants/api_urls'; let SignupForm = React.createClass({ @@ -56,10 +55,6 @@ let SignupForm = React.createClass({ } }, - getFormData() { - return this.getQuery(); - }, - handleSuccess(response){ if (response.user) { let notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000); @@ -71,16 +66,23 @@ let SignupForm = React.createClass({ } }, + getFormData() { + if (this.getQuery().token){ + return {token: this.getQuery().token}; + } + return null; + }, + render() { let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' + getLangText('This password is securing your digital property like a bank account') + '.\n ' + getLangText('Store it in a safe place') + '!'; - let email = this.getQuery().email ? this.getQuery().email : null; + let email = this.getQuery().email || null; return (
}> - +

{this.props.headerMessage}

- +
@@ -132,7 +134,7 @@ let SignupForm = React.createClass({ style={{paddingBottom: 0}}> - {' ' + getLangText('I agree to the Terms of Service') + ' '} + {' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '} ( {getLangText('read')} ) diff --git a/js/components/ascribe_forms/form_submit_to_prize.js b/js/components/ascribe_forms/form_submit_to_prize.js index 7f991af3..23075791 100644 --- a/js/components/ascribe_forms/form_submit_to_prize.js +++ b/js/components/ascribe_forms/form_submit_to_prize.js @@ -19,10 +19,7 @@ import requests from '../../utils/requests'; let PieceSubmitToPrizeForm = React.createClass({ propTypes: { piece: React.PropTypes.object, - handleSuccess: React.PropTypes.func, - - // this is set by ModalWrapper automatically - onRequestHide: React.PropTypes.func + handleSuccess: React.PropTypes.func }, render() { @@ -36,7 +33,9 @@ let PieceSubmitToPrizeForm = React.createClass({

+ type="submit"> + {getLangText('SUBMIT TO PRIZE')} +

} spinner={ @@ -46,20 +45,20 @@ let PieceSubmitToPrizeForm = React.createClass({ + editable={true} + overrideForm={true}> + editable={true} + overrideForm={true}> @@ -80,7 +79,6 @@ let PieceSubmitToPrizeForm = React.createClass({

{getLangText('Are you sure you want to submit to the prize?')}

{getLangText('This is an irrevocable action%s', '.')}

- ); } diff --git a/js/components/ascribe_forms/form_transfer.js b/js/components/ascribe_forms/form_transfer.js index 07821475..8bbcf110 100644 --- a/js/components/ascribe_forms/form_transfer.js +++ b/js/components/ascribe_forms/form_transfer.js @@ -21,7 +21,6 @@ let TransferForm = React.createClass({ message: React.PropTypes.string, editions: React.PropTypes.array, currentUser: React.PropTypes.object, - onRequestHide: React.PropTypes.func, handleSuccess: React.PropTypes.func }, @@ -42,11 +41,9 @@ let TransferForm = React.createClass({

- + type="submit"> + {getLangText('TRANSFER')} +

} spinner={ @@ -64,10 +61,10 @@ let TransferForm = React.createClass({ + editable={true} + overrideForm={true}> diff --git a/js/components/ascribe_forms/form_unconsign.js b/js/components/ascribe_forms/form_unconsign.js index d33ccedf..ed6362da 100644 --- a/js/components/ascribe_forms/form_unconsign.js +++ b/js/components/ascribe_forms/form_unconsign.js @@ -18,7 +18,6 @@ let UnConsignForm = React.createClass({ id: React.PropTypes.object, message: React.PropTypes.string, editions: React.PropTypes.array, - onRequestHide: React.PropTypes.func, handleSuccess: React.PropTypes.func }, @@ -39,11 +38,9 @@ let UnConsignForm = React.createClass({

- + type="submit"> + {getLangText('UNCONSIGN')} +

} spinner={ @@ -53,10 +50,10 @@ let UnConsignForm = React.createClass({ + editable={true} + overrideForm={true}> diff --git a/js/components/ascribe_forms/form_unconsign_request.js b/js/components/ascribe_forms/form_unconsign_request.js index 1978e151..750ee72d 100644 --- a/js/components/ascribe_forms/form_unconsign_request.js +++ b/js/components/ascribe_forms/form_unconsign_request.js @@ -3,7 +3,6 @@ import React from 'react'; import Button from 'react-bootstrap/lib/Button'; -import Alert from 'react-bootstrap/lib/Alert'; import Form from './form'; import Property from './property'; @@ -19,7 +18,6 @@ let UnConsignRequestForm = React.createClass({ url: React.PropTypes.string, id: React.PropTypes.object, message: React.PropTypes.string, - onRequestHide: React.PropTypes.func, handleSuccess: React.PropTypes.func }, @@ -40,11 +38,9 @@ let UnConsignRequestForm = React.createClass({

- + type="submit"> + {getLangText('REQUEST UNCONSIGN')} +

} spinner={ @@ -54,10 +50,10 @@ let UnConsignRequestForm = React.createClass({ + editable={true} + overrideForm={true}> diff --git a/js/components/ascribe_forms/input_checkbox.js b/js/components/ascribe_forms/input_checkbox.js index a71dd4b6..38885441 100644 --- a/js/components/ascribe_forms/input_checkbox.js +++ b/js/components/ascribe_forms/input_checkbox.js @@ -21,7 +21,14 @@ let InputCheckbox = React.createClass({ children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) + ]), + + // provided by Property + disabled: React.PropTypes.bool, + onChange: React.PropTypes.func, + + // can be used to style the component from the outside + style: React.PropTypes.object }, // As HTML inputs, we're setting the default value for an input to checked === false @@ -56,6 +63,12 @@ let InputCheckbox = React.createClass({ }, onChange() { + // if this.props.disabled is true, we're just going to ignore every click, + // as the value should then not be changable by the user + if(this.props.disabled) { + return; + } + // On every change, we're inversing the input's value let inverseValue = !this.refs.checkbox.getDOMNode().checked; @@ -74,8 +87,21 @@ let InputCheckbox = React.createClass({ }, render() { + + let style = {}; + + // Some conditional styling if the component is disabled + if(!this.props.disabled) { + style.cursor = 'pointer'; + style.color = 'rgba(0, 0, 0, 0.5)'; + } else { + style.cursor = 'not-allowed'; + style.color = 'rgba(0, 0, 0, 0.35)'; + } + return ( - + {this.props.children} diff --git a/js/components/ascribe_forms/input_date.js b/js/components/ascribe_forms/input_date.js index 32ffb5eb..3e2892c0 100644 --- a/js/components/ascribe_forms/input_date.js +++ b/js/components/ascribe_forms/input_date.js @@ -7,15 +7,31 @@ import DatePicker from 'react-datepicker/dist/react-datepicker'; let InputDate = React.createClass({ propTypes: { submitted: React.PropTypes.bool, - placeholderText: React.PropTypes.string + placeholderText: React.PropTypes.string, + onChange: React.PropTypes.func, + defaultValue: React.PropTypes.object, + + // DatePicker implements the disabled attribute + // https://github.com/Hacker0x01/react-datepicker/blob/master/src/datepicker.jsx#L30 + disabled: React.PropTypes.bool }, getInitialState() { return { - value: null + value: null, + value_moment: null }; }, + // InputDate needs to support setting a defaultValue from outside. + // If this is the case, we need to call handleChange to propagate this + // to the outer Property + componentWillReceiveProps(nextProps) { + if(!this.state.value && !this.state.value_moment && nextProps.defaultValue) { + this.handleChange(this.props.defaultValue); + } + }, + handleChange(date) { let formattedDate = date.format('YYYY-MM-DD'); this.setState({ @@ -30,10 +46,15 @@ let InputDate = React.createClass({ }); }, - render: function () { + reset() { + this.setState(this.getInitialState()); + }, + + render() { return (
+ ); + } +}); + +export default InputFineUploader; \ 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 fe372bdd..ee7dfe95 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -4,10 +4,10 @@ import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -let InputTextAreaToggable = React.createClass({ +let InputTextAreaToggable = React.createClass({ propTypes: { - editable: React.PropTypes.bool.isRequired, + disabled: React.PropTypes.bool, rows: React.PropTypes.number.isRequired, required: React.PropTypes.string, defaultValue: React.PropTypes.string @@ -15,17 +15,36 @@ let InputTextAreaToggable = React.createClass({ getInitialState() { return { - value: this.props.defaultValue + value: null }; }, + + componentDidMount() { + this.setState({ + value: this.props.defaultValue + }); + }, + + componentDidUpdate() { + // If the initial value of state.value is null, we want to set props.defaultValue + // as a value. In all other cases TextareaAutosize.onChange is updating.handleChange already + if(this.state.value === null && this.props.defaultValue) { + this.setState({ + 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.disabled) { 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 new file mode 100644 index 00000000..082ae8ef --- /dev/null +++ b/js/components/ascribe_forms/list_form_request_actions.js @@ -0,0 +1,36 @@ +'use strict'; + +import React from 'react'; +import RequestActionForm from './form_request_action'; + +let ListRequestActions = React.createClass({ + + propTypes: { + pieceOrEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]).isRequired, + currentUser: React.PropTypes.object.isRequired, + handleSuccess: React.PropTypes.func.isRequired, + notifications: React.PropTypes.array.isRequired + }, + + render () { + if (this.props.notifications && + this.props.notifications.length > 0) { + return ( +
+ {this.props.notifications.map((notification) => + )} +
+ ); + } + return null; + } +}); + +export default ListRequestActions; \ No newline at end of file diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 5a72270c..f3c26935 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -6,10 +6,22 @@ import ReactAddons from 'react/addons'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Tooltip from 'react-bootstrap/lib/Tooltip'; +import AppConstants from '../../constants/application_constants'; + +import { mergeOptions } from '../../utils/general_utils'; + + let Property = React.createClass({ propTypes: { hidden: React.PropTypes.bool, + editable: React.PropTypes.bool, + + // If we want Form to have a different value for disabled as Property has one for + // editable, we need to set overrideForm to true, as it will then override Form's + // disabled value for individual Properties + overrideForm: React.PropTypes.bool, + tooltip: React.PropTypes.element, label: React.PropTypes.string, value: React.PropTypes.oneOfType([ @@ -20,8 +32,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 @@ -55,7 +70,7 @@ let Property = React.createClass({ // In order to set this.state.value from another component // the state of value should only be set if its not undefined and // actually references something - if(typeof childInput.getDOMNode().value !== 'undefined') { + if(childInput && typeof childInput.getDOMNode().value !== 'undefined') { this.setState({ value: childInput.getDOMNode().value }); @@ -78,21 +93,41 @@ let Property = React.createClass({ }, reset() { + let input = this.refs.input; + // 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(input.state && input.state.value) { + // resets the value of a custom react component input + input.state.value = this.state.initialValue; + } - // resets the value of a plain HTML5 input - this.refs.input.getDOMNode().value = this.state.initialValue; + // For some reason, if we set the value of a non HTML element (but a custom input), + // after a reset, the value will be be propagated to this component. + // + // Therefore we have to make sure only to reset the initial value + // of HTML inputs (which we determine by checking if there 'type' attribute matches + // the ones included in AppConstants.possibleInputTypes). + let inputDOMNode = input.getDOMNode(); + if(inputDOMNode.type && typeof inputDOMNode.type === 'string' && + AppConstants.possibleInputTypes.indexOf(inputDOMNode.type.toLowerCase()) > -1) { + inputDOMNode.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(typeof input.reset === 'function') { + input.reset(); + } }, handleChange(event) { - this.props.handleChange(event); - if ('onChange' in this.props) { + if (typeof this.props.onChange === 'function') { this.props.onChange(event); } @@ -108,7 +143,7 @@ let Property = React.createClass({ // if onClick is defined from the outside, // just call it - if(this.props.onClick) { + if(typeof this.props.onClick === 'function') { this.props.onClick(); } @@ -123,7 +158,7 @@ let Property = React.createClass({ isFocused: false }); - if(this.props.onBlur) { + if(typeof this.props.onBlur === 'function') { this.props.onBlur(event); } }, @@ -167,9 +202,10 @@ let Property = React.createClass({ } }, - renderChildren() { + renderChildren(style) { return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.addons.cloneWithProps(child, { + style, onChange: this.handleChange, onFocus: this.handleFocus, onBlur: this.handleBlur, @@ -180,34 +216,42 @@ let Property = React.createClass({ }, render() { + let footer = null; let tooltip = ; - if (this.props.tooltip){ + let style = this.props.style ? mergeOptions({}, this.props.style) : {}; + + if(this.props.tooltip){ tooltip = ( {this.props.tooltip} ); } - let footer = null; - if (this.props.footer){ + + if(this.props.footer){ footer = (
{this.props.footer}
); } + + if(!this.props.editable) { + style.cursor = 'not-allowed'; + } + return (
+ style={style}>
{this.state.errors} - { this.props.label} - {this.renderChildren()} + {this.props.label} + {this.renderChildren(style)} {footer}
diff --git a/js/components/ascribe_forms/property_collapsible.js b/js/components/ascribe_forms/property_collapsible.js index ba6c0a1e..ef9a1329 100644 --- a/js/components/ascribe_forms/property_collapsible.js +++ b/js/components/ascribe_forms/property_collapsible.js @@ -3,11 +3,9 @@ import React from 'react'; import ReactAddons from 'react/addons'; -import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Tooltip from 'react-bootstrap/lib/Tooltip'; - -import classNames from 'classnames'; +import Panel from 'react-bootstrap/lib/Panel'; let PropertyCollapsile = React.createClass({ @@ -17,22 +15,12 @@ let PropertyCollapsile = React.createClass({ tooltip: React.PropTypes.string }, - mixins: [CollapsibleMixin], - getInitialState() { return { show: false }; }, - getCollapsibleDOMNode(){ - return React.findDOMNode(this.refs.panel); - }, - - getCollapsibleDimensionValue(){ - return React.findDOMNode(this.refs.panel).scrollHeight; - }, - handleFocus() { this.refs.checkboxCollapsible.getDOMNode().checked = !this.refs.checkboxCollapsible.getDOMNode().checked; this.setState({ @@ -54,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){ @@ -85,11 +80,14 @@ let PropertyCollapsile = React.createClass({ {this.props.checkboxLabel}
-
+ +
{this.renderChildren()} -
+
+
); } diff --git a/js/components/ascribe_media/media_player.js b/js/components/ascribe_media/media_player.js index ad53b61f..e767a800 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}

); @@ -200,7 +208,8 @@ let MediaPlayer = React.createClass({
You can leave this page and check back on the status later.

+ label="%(percent)s%" + className="ascribe-progress-bar" /> ); } else { diff --git a/js/components/ascribe_modal/modal_password_request_reset.js b/js/components/ascribe_modal/modal_password_request_reset.js index fffcb3d7..d941bcce 100644 --- a/js/components/ascribe_modal/modal_password_request_reset.js +++ b/js/components/ascribe_modal/modal_password_request_reset.js @@ -7,9 +7,13 @@ import PasswordResetRequestForm from '../ascribe_forms/form_password_reset_reque import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import { getLangText } from '../../utils/lang_utils.js' +import { getLangText } from '../../utils/lang_utils.js'; let PasswordResetRequestModal = React.createClass({ + propTypes: { + button: React.PropTypes.element + }, + handleResetSuccess(){ let notificationText = getLangText('Request successfully sent, check your email'); let notification = new GlobalNotificationModel(notificationText, 'success', 50000); @@ -18,10 +22,9 @@ let PasswordResetRequestModal = React.createClass({ render() { return ( + handleSuccess={this.handleResetSuccess}> ); diff --git a/js/components/ascribe_modal/modal_wrapper.js b/js/components/ascribe_modal/modal_wrapper.js index a8f7b182..f00eee9e 100644 --- a/js/components/ascribe_modal/modal_wrapper.js +++ b/js/components/ascribe_modal/modal_wrapper.js @@ -4,92 +4,74 @@ import React from 'react'; import ReactAddons from 'react/addons'; import Modal from 'react-bootstrap/lib/Modal'; -import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; -import ModalTrigger from 'react-bootstrap/lib/ModalTrigger'; -import Tooltip from 'react-bootstrap/lib/Tooltip'; - -import ModalMixin from '../../mixins/modal_mixin'; let ModalWrapper = React.createClass({ propTypes: { - title: React.PropTypes.string.isRequired, - onRequestHide: React.PropTypes.func, + trigger: React.PropTypes.element.isRequired, + title: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element, + React.PropTypes.string + ]).isRequired, handleSuccess: React.PropTypes.func.isRequired, - button: React.PropTypes.object.isRequired, - children: React.PropTypes.object, - tooltip: React.PropTypes.string + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) }, - getModalTrigger() { - return ( - - {this.props.children} - - }> - {this.props.button} - - ); + getInitialState() { + return { + showModal: false + }; }, - render() { - if(this.props.tooltip) { - return ( - {this.props.tooltip}}> - {this.getModalTrigger()} - - ); - } else { - return ( - - {/* This needs to be some kind of inline-block */} - {this.getModalTrigger()} - - ); - } - } -}); - - -let ModalBody = React.createClass({ - propTypes: { - onRequestHide: React.PropTypes.func, - handleSuccess: React.PropTypes.func, - children: React.PropTypes.object, - title: React.PropTypes.string.isRequired + show() { + this.setState({ + showModal: true + }); }, - mixins: [ModalMixin], + hide() { + this.setState({ + showModal: false + }); + }, handleSuccess(response){ this.props.handleSuccess(response); - this.props.onRequestHide(); + this.hide(); }, renderChildren() { return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.addons.cloneWithProps(child, { - onRequestHide: this.props.onRequestHide, handleSuccess: this.handleSuccess }); }); }, render() { + // this adds the onClick method show of modal_wrapper to the trigger component + // which is in most cases a button. + let trigger = React.cloneElement(this.props.trigger, {onClick: this.show}); + return ( - -
- {this.renderChildren()} -
-
+ + {trigger} + + + + {this.props.title} + + +
+ {this.renderChildren()} +
+
+
); } }); - export default ModalWrapper; diff --git a/js/components/ascribe_panel/action_panel.js b/js/components/ascribe_panel/action_panel.js index a63af530..f6fe9a70 100644 --- a/js/components/ascribe_panel/action_panel.js +++ b/js/components/ascribe_panel/action_panel.js @@ -1,15 +1,21 @@ 'use strict'; import React from 'react'; - +import classnames from 'classnames'; let ActionPanel = React.createClass({ propTypes: { title: React.PropTypes.string, - content: React.PropTypes.string, + content: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]), buttons: React.PropTypes.element, onClick: React.PropTypes.func, - ignoreFocus: React.PropTypes.bool + ignoreFocus: React.PropTypes.bool, + + leftColumnWidth: React.PropTypes.string, + rightColumnWidth: React.PropTypes.string }, getInitialState() { @@ -37,31 +43,25 @@ let ActionPanel = React.createClass({ }); }, - getClassName() { - if(this.state.isFocused) { - return 'is-focused'; - } else { - return ''; - } - }, - render() { + let { leftColumnWidth, rightColumnWidth } = this.props; + return ( -
-
- {this.props.title} -
-
- +
+
+
{this.props.content} - - +
+
+
+
{this.props.buttons} - +
); diff --git a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js index 3642a667..452a9bd8 100644 --- a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js +++ b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js @@ -81,7 +81,7 @@ let PieceListBulkModal = React.createClass({ this.fetchSelectedPieceEditionList() .forEach((pieceId) => { - EditionListActions.refreshEditionList(pieceId); + EditionListActions.refreshEditionList({pieceId, filterBy: {}}); }); EditionListActions.clearAllEditionSelections(); }, diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js index 22034f0d..0890db00 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar.js @@ -3,6 +3,7 @@ import React from 'react'; import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget'; +import PieceListToolbarOrderWidget from './piece_list_toolbar_order_widget'; import Input from 'react-bootstrap/lib/Input'; import Glyphicon from 'react-bootstrap/lib/Glyphicon'; @@ -13,8 +14,25 @@ let PieceListToolbar = React.createClass({ propTypes: { className: React.PropTypes.string, searchFor: React.PropTypes.func, + filterParams: React.PropTypes.arrayOf( + React.PropTypes.shape({ + label: React.PropTypes.string, + items: React.PropTypes.arrayOf( + React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.shape({ + key: React.PropTypes.string, + label: React.PropTypes.string + }) + ]) + ) + }) + ), filterBy: React.PropTypes.object, applyFilterBy: React.PropTypes.func, + orderParams: React.PropTypes.array, + orderBy: React.PropTypes.string, + applyOrderBy: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -26,6 +44,29 @@ let PieceListToolbar = React.createClass({ this.props.searchFor(searchTerm); }, + getFilterWidget(){ + if (this.props.filterParams){ + return ( + + ); + } + return null; + }, + getOrderWidget(){ + if (this.props.orderParams){ + return ( + + ); + } + return null; + }, + render() { let searchIcon = ; @@ -37,7 +78,7 @@ let PieceListToolbar = React.createClass({ {this.props.children} - + - + {this.getOrderWidget()} + {this.getFilterWidget()}
diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index 13bfd9a0..9cb8b94f 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -3,20 +3,26 @@ import React from 'react'; import DropdownButton from 'react-bootstrap/lib/DropdownButton'; -import MenuItem from 'react-bootstrap/lib/MenuItem'; import { getLangText } from '../../utils/lang_utils.js'; + let PieceListToolbarFilterWidgetFilter = React.createClass({ propTypes: { - // An array of either strings (which represent acl enums) or objects of the form - // - // { - // key: , - // label: - // } - // - filterParams: React.PropTypes.arrayOf(React.PropTypes.any).isRequired, + filterParams: React.PropTypes.arrayOf( + React.PropTypes.shape({ + label: React.PropTypes.string, + items: React.PropTypes.arrayOf( + React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.shape({ + key: React.PropTypes.string, + label: React.PropTypes.string + }) + ]) + ) + }) + ).isRequired, filterBy: React.PropTypes.object, applyFilterBy: React.PropTypes.func }, @@ -79,35 +85,53 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({ -
  • - {getLangText('Show works that')}: -
  • - {this.props.filterParams.map((param, i) => { - let label; - - if(typeof param !== 'string') { - label = param.label; - param = param.key; - } else { - param = param; - label = param.split('_')[1]; - } - + {/* We iterate over filterParams, to receive the label and then for each + label also iterate over its items, to get all filterable options */} + {this.props.filterParams.map(({ label, items }, i) => { return ( - -
    - - {getLangText('I can') + ' ' + getLangText(label)} - - -
    -
    +
    +
  • + {label}: +
  • + {items.map((param, j) => { + + // As can be seen in the PropTypes, a param can either + // be a string or an object of the shape: + // + // { + // key: , + // label: + // } + // + // This is why we need to distinguish between both here. + if(typeof param !== 'string') { + label = param.label; + param = param.key; + } else { + param = param; + label = param.split('acl_')[1].replace(/_/g, ' '); + } + + return ( +
  • +
    + + {getLangText(label)} + + +
    +
  • + ); + })} +
    ); })}
    diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js new file mode 100644 index 00000000..a44b8ca2 --- /dev/null +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js @@ -0,0 +1,87 @@ +'use strict'; + +import React from 'react'; + +import DropdownButton from 'react-bootstrap/lib/DropdownButton'; + +import { getLangText } from '../../utils/lang_utils.js'; + +let PieceListToolbarOrderWidget = React.createClass({ + propTypes: { + // An array of either strings (which represent acl enums) or objects of the form + // + // { + // key: , + // label:
    + // } + orderParams: React.PropTypes.arrayOf(React.PropTypes.any).isRequired, + orderBy: React.PropTypes.string, + applyOrderBy: React.PropTypes.func + }, + + generateOrderByStatement(param) { + let orderBy = this.props.orderBy; + return orderBy; + }, + + /** + * We need overloading here to find the correct parameter of the label + * the user is clicking on. + */ + orderBy(orderBy) { + return () => { + this.props.applyOrderBy(orderBy); + }; + }, + + isOrderActive() { + // We're hiding the star in that complicated matter so that, + // the surrounding button is not resized up on appearance + if(this.props.orderBy.length > 0) { + return { visibility: 'visible'}; + } else { + return { visibility: 'hidden' }; + } + }, + + render() { + let filterIcon = ( + + + * + + ); + return ( + + +
  • + {getLangText('Sort by')}: +
  • + {this.props.orderParams.map((param) => { + return ( +
    +
  • +
    + + {getLangText(param.replace('_', ' '))} + + -1} /> +
    +
  • +
    + ); + })} +
    + ); + } +}); + +export default PieceListToolbarOrderWidget; \ No newline at end of file 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/account_settings.js b/js/components/ascribe_settings/account_settings.js new file mode 100644 index 00000000..1898c599 --- /dev/null +++ b/js/components/ascribe_settings/account_settings.js @@ -0,0 +1,106 @@ +'use strict'; + +import React from 'react'; + +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 InputCheckbox from '../ascribe_forms/input_checkbox'; +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import AclProxy from '../acl_proxy'; + +import CopyrightAssociationForm from '../ascribe_forms/form_copyright_association'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + +let AccountSettings = React.createClass({ + propTypes: { + currentUser: React.PropTypes.object.isRequired, + loadUser: React.PropTypes.func.isRequired, + whitelabel: React.PropTypes.object.isRequired + }, + + handleSuccess(){ + this.props.loadUser(); + let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getFormDataProfile(){ + return {'email': this.props.currentUser.email}; + }, + + render() { + let content = ; + let profile = null; + + if (this.props.currentUser.username) { + content = ( +
    + + + + + + +
    +
    + ); + profile = ( + +
    + + + + {' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')} + + + +
    +
    + ); + } + return ( + + {content} + + {profile} + + ); + } +}); + +export default AccountSettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/api_settings.js b/js/components/ascribe_settings/api_settings.js new file mode 100644 index 00000000..0f638675 --- /dev/null +++ b/js/components/ascribe_settings/api_settings.js @@ -0,0 +1,123 @@ +'use strict'; + +import React from 'react'; + +import ApplicationStore from '../../stores/application_store'; +import ApplicationActions from '../../actions/application_actions'; + +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 ActionPanel from '../ascribe_panel/action_panel'; +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import ApiUrls from '../../constants/api_urls'; +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + + +let APISettings = React.createClass({ + propTypes: { + defaultExpanded: React.PropTypes.bool + }, + + getInitialState() { + return ApplicationStore.getState(); + }, + + componentDidMount() { + ApplicationStore.listen(this.onChange); + ApplicationActions.fetchApplication(); + }, + + componentWillUnmount() { + ApplicationStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + handleCreateSuccess() { + ApplicationActions.fetchApplication(); + let notification = new GlobalNotificationModel(getLangText('Application successfully created'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + handleTokenRefresh(event) { + let applicationName = event.target.getAttribute('data-id'); + ApplicationActions.refreshApplicationToken(applicationName); + + let notification = new GlobalNotificationModel(getLangText('Token refreshed'), 'success', 2000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getApplications(){ + let content = ; + + if (this.state.applications.length > -1) { + content = this.state.applications.map(function(app, i) { + return ( + +
    + {app.name} +
    +
    + {'Bearer ' + app.bearer_token.token} +
    + + } + buttons={ +
    +
    + +
    +
    + }/> + ); + }, this); + } + return content; + }, + + render() { + return ( + +
    + + + +
    +
    +
    +                    Usage: curl <url> -H 'Authorization: Bearer <token>'
    +                
    + {this.getApplications()} +
    + ); + } +}); + +export default APISettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/bitcoin_wallet_settings.js b/js/components/ascribe_settings/bitcoin_wallet_settings.js new file mode 100644 index 00000000..ea528709 --- /dev/null +++ b/js/components/ascribe_settings/bitcoin_wallet_settings.js @@ -0,0 +1,71 @@ +'use strict'; + +import React from 'react'; + +import WalletSettingsStore from '../../stores/wallet_settings_store'; +import WalletSettingsActions from '../../actions/wallet_settings_actions'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import AppConstants from '../../constants/application_constants'; + +import { getLangText } from '../../utils/lang_utils'; + + +let BitcoinWalletSettings = React.createClass({ + propTypes: { + defaultExpanded: React.PropTypes.bool + }, + + getInitialState() { + return WalletSettingsStore.getState(); + }, + + componentDidMount() { + WalletSettingsStore.listen(this.onChange); + WalletSettingsActions.fetchWalletSettings(); + }, + + componentWillUnmount() { + WalletSettingsStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + let content = ; + + if (this.state.walletSettings.btc_public_key) { + content = ( +
    + +
    {this.state.walletSettings.btc_public_key}
    +
    + +
    {this.state.walletSettings.btc_root_address}
    +
    +
    +
    ); + } + return ( + + {content} + + ); + } +}); + +export default BitcoinWalletSettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js new file mode 100644 index 00000000..7196032b --- /dev/null +++ b/js/components/ascribe_settings/contract_settings.js @@ -0,0 +1,186 @@ +'use strict'; + +import React from 'react'; + +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; +import CreateContractForm from '../ascribe_forms/form_create_contract'; + +import ContractListStore from '../../stores/contract_list_store'; +import ContractListActions from '../../actions/contract_list_actions'; + +import UserStore from '../../stores/user_store'; +import UserActions from '../../actions/user_actions'; + +import WhitelabelStore from '../../stores/whitelabel_store'; +import WhitelabelActions from '../../actions/whitelabel_actions'; + +import ActionPanel from '../ascribe_panel/action_panel'; +import ContractSettingsUpdateButton from './contract_settings_update_button'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import AclProxy from '../acl_proxy'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils'; + + +let ContractSettings = React.createClass({ + getInitialState(){ + return mergeOptions( + ContractListStore.getState(), + UserStore.getState() + ); + }, + + componentDidMount() { + ContractListStore.listen(this.onChange); + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + UserActions.fetchCurrentUser(); + ContractListActions.fetchContractList(true); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + ContractListStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + removeContract(contract) { + return () => { + ContractListActions.removeContract(contract.id) + .then((response) => { + ContractListActions.fetchContractList(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.is_public); + }, + + getPrivateContracts(){ + return this.state.contractList.filter((contract) => !contract.is_public); + }, + + render() { + let publicContracts = this.getPublicContracts(); + let privateContracts = this.getPrivateContracts(); + let createPublicContractForm = null; + + if(publicContracts.length === 0) { + createPublicContractForm = ( + + ); + } + + return ( +
    + + + + } + leftColumnWidth="40%" + rightColumnWidth="60%"/> + ); + })} +
    + + +
    + + {privateContracts.map((contract, i) => { + return ( + + + + + + {getLangText('PREVIEW')} + + +
    + } + leftColumnWidth="60%" + rightColumnWidth="40%"/> + ); + })} + +
    + + + ); + } +}); + +export default ContractSettings; \ No newline at end of file diff --git a/js/components/ascribe_settings/contract_settings_update_button.js b/js/components/ascribe_settings/contract_settings_update_button.js new file mode 100644 index 00000000..f2e54c50 --- /dev/null +++ b/js/components/ascribe_settings/contract_settings_update_button.js @@ -0,0 +1,98 @@ +'use strict'; + +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 { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { getCookie } from '../../utils/fetch_api_utils'; +import { getLangText } from '../../utils/lang_utils'; + + +let ContractSettingsUpdateButton = React.createClass({ + propTypes: { + contract: React.PropTypes.object + }, + + submitFile(file) { + let contract = this.props.contract; + + // override the blob with the key's value + contract.blob = file.key; + + // send it to the server + ContractListActions + .changeContract(contract) + .then((res) => { + + // Display feedback to the user + let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + // and refresh the contract list to get the updated contracs + return ContractListActions.fetchContractList(true); + }) + .then(() => { + // Also, reset the fineuploader component so that the user can again 'update' his contract + this.refs.fineuploader.reset(); + }) + .catch((err) => { + console.logGlobal(err); + let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }); + }, + + render() { + return ( + {/* So that ReactS3FineUploader is not complaining */}} + signature={{ + endpoint: AppConstants.serverUrl + 's3/signature/', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + deleteFile={{ + enabled: true, + method: 'DELETE', + endpoint: AppConstants.serverUrl + 's3/delete', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + fileClassToUpload={{ + singular: getLangText('UPDATE'), + plural: getLangText('UPDATE') + }} + isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + submitFile={this.submitFile} + /> + ); + } +}); + +export default ContractSettingsUpdateButton; \ No newline at end of file diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js new file mode 100644 index 00000000..2b9ae2a1 --- /dev/null +++ b/js/components/ascribe_settings/settings_container.js @@ -0,0 +1,84 @@ +'use strict'; + +import React from 'react'; +import Router from 'react-router'; + +import UserStore from '../../stores/user_store'; +import UserActions from '../../actions/user_actions'; + +import WhitelabelStore from '../../stores/whitelabel_store'; +import WhitelabelActions from '../../actions/whitelabel_actions'; + +import AccountSettings from './account_settings'; +import BitcoinWalletSettings from './bitcoin_wallet_settings'; +import APISettings from './api_settings'; + +import AclProxy from '../acl_proxy'; + +import { mergeOptions } from '../../utils/general_utils'; + + +let SettingsContainer = React.createClass({ + propTypes: { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element]) + }, + + mixins: [Router.Navigation], + + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + UserActions.fetchCurrentUser(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + }, + + loadUser(){ + UserActions.fetchCurrentUser(); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + if (this.state.currentUser && this.state.currentUser.username) { + return ( +
    + + {this.props.children} + + + + + + +
    + ); + } + return null; + } +}); + +export default SettingsContainer; diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 67286385..53092a38 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -4,12 +4,21 @@ import React from 'react'; import Router from 'react-router'; import ReactAddons from 'react/addons'; +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) + children: React.PropTypes.arrayOf(React.PropTypes.element), + forwardProcess: React.PropTypes.bool.isRequired, + + glyphiconClassNames: React.PropTypes.shape({ + pending: React.PropTypes.string, + complete: React.PropTypes.string + }) }, mixins: [State, Navigation], @@ -18,15 +27,25 @@ let SlidesContainer = React.createClass({ // handle queryParameters let queryParams = this.getQuery(); 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); + } + return { + slideNum, + startFrom, containerWidth: 0, - slideNum: slideNum, historyLength: window.history.length }; }, @@ -34,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 @@ -51,9 +73,23 @@ let SlidesContainer = React.createClass({ window.addEventListener('resize', this.handleContainerResize); }, - componentDidUpdate() { - // check if slide_num was defined, and if not then default to 0 + componentWillReceiveProps() { let queryParams = this.getQuery(); + + // also check if start_from was updated + // This applies for example when the user tries to submit a already existing piece + // (starting from slide 1 for example) and then clicking on + NEW WORK + if(queryParams && !('start_from' in queryParams)) { + this.setState({ + startFrom: -1 + }); + } + }, + + componentDidUpdate() { + let queryParams = this.getQuery(); + + // check if slide_num was defined, and if not then default to 0 this.setSlideNum(queryParams.slide_num); }, @@ -68,6 +104,12 @@ let SlidesContainer = React.createClass({ }); }, + // When the start_from parameter is used, this.setSlideNum can not simply be used anymore. + nextSlide() { + let nextSlide = this.state.slideNum + 1; + this.setSlideNum(nextSlide); + }, + // We let every one from the outsite set the page number of the slider, // though only if the slideNum is actually in the range of our children-list. setSlideNum(slideNum) { @@ -84,7 +126,6 @@ let SlidesContainer = React.createClass({ // then we want to "replace" (in this case append) the current url with ?slide_num=0 if(isNaN(slideNum) && this.state.slideNum === -1) { slideNum = 0; - queryParams.slide_num = slideNum; this.replaceWith(this.getPathname(), null, queryParams); @@ -98,7 +139,7 @@ let SlidesContainer = React.createClass({ // if slideNum is within the range of slides and none of the previous cases // where matched, we can actually do transitions - } else if(slideNum >= 0 || slideNum < React.Children.count(this.props.children)) { + } else if(slideNum >= 0 || slideNum < this.customChildrenCount()) { if(slideNum !== this.state.slideNum - 1) { // Bootstrapping the component, getInitialState is called once to save @@ -108,13 +149,17 @@ let SlidesContainer = React.createClass({ // we push a new state on it ONCE (ever). // Otherwise, we're able to use the browsers history.forward() method // to keep the stack clean - if(this.state.historyLength === window.history.length) { - + + if(this.props.forwardProcess) { queryParams.slide_num = slideNum; - this.transitionTo(this.getPathname(), null, queryParams); } else { - window.history.forward(); + if(this.state.historyLength === window.history.length) { + queryParams.slide_num = slideNum; + this.transitionTo(this.getPathname(), null, queryParams); + } else { + window.history.forward(); + } } } @@ -127,34 +172,108 @@ let SlidesContainer = React.createClass({ } }, + // breadcrumbs are defined as attributes of the slides. + // To extract them we have to read the DOM element's attributes + extractBreadcrumbs() { + let breadcrumbs = []; + + ReactAddons.Children.map(this.props.children, (child, i) => { + if(child && i >= this.state.startFrom && child.props['data-slide-title']) { + breadcrumbs.push(child.props['data-slide-title']); + } + }); + + return breadcrumbs; + }, + + // If startFrom is defined as a URL parameter, this can manipulate + // the number of children that are injected into the DOM. + // Therefore React.Children.count does not work anymore and we + // need to implement our own method. + customChildrenCount() { + let count = 0; + React.Children.forEach(this.props.children, (child, i) => { + if(i >= this.state.startFrom) { + count++; + } + }); + + return count; + }, + + renderBreadcrumbs() { + let breadcrumbs = this.extractBreadcrumbs(); + let numOfChildren = this.customChildrenCount(); + + // check if every child/slide has a title, + // otherwise do not display the breadcrumbs at all + // Also, if there is only one child, do not display the breadcrumbs + if(breadcrumbs.length === numOfChildren && breadcrumbs.length > 1 && numOfChildren > 1) { + return ( + + ); + } else { + return null; + } + }, + // Since we need to give the slides a width, we need to call ReactAddons.addons.cloneWithProps // Also, a key is nice to have! renderChildren() { return ReactAddons.Children.map(this.props.children, (child, i) => { - return ReactAddons.addons.cloneWithProps(child, { - className: 'ascribe-slide', - style: { - width: this.state.containerWidth - }, - key: i - }); + + // since the default parameter of startFrom is -1, we do not need to check + // if its actually present in the url bar, as it will just not match + if(child && i >= this.state.startFrom) { + return ReactAddons.addons.cloneWithProps(child, { + className: 'ascribe-slide', + style: { + width: this.state.containerWidth + }, + key: i + }); + } else { + // Abortions are bad mkay + return null; + } + }); }, 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.renderBreadcrumbs()}
    -
    - {this.renderChildren()} -
    +
    + {this.renderChildren()} +
    ); diff --git a/js/components/ascribe_slides_container/slides_container_breadcrumbs.js b/js/components/ascribe_slides_container/slides_container_breadcrumbs.js new file mode 100644 index 00000000..6d361ca2 --- /dev/null +++ b/js/components/ascribe_slides_container/slides_container_breadcrumbs.js @@ -0,0 +1,85 @@ +'use strict'; + +import React from 'react'; + +import classnames from 'classnames'; + +import Col from 'react-bootstrap/lib/Col'; + + +// Note: +// +// If we ever need generic breadcrumbs component, we should refactor this +let SlidesContainerBreadcrumbs = React.createClass({ + propTypes: { + breadcrumbs: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + + slideNum: React.PropTypes.number.isRequired, + numOfSlides: React.PropTypes.number.isRequired, + + containerWidth: React.PropTypes.number.isRequired, + + glyphiconClassNames: React.PropTypes.shape({ + pending: React.PropTypes.string, + complete: React.PropTypes.string + }) + }, + + getDefaultProps() { + return { + glyphiconClassNames: { + pending: 'glyphicon glyphicon-chevron-right', + complete: 'glyphicon glyphicon-lock' + } + }; + }, + + render() { + let breadcrumbs = this.props.breadcrumbs; + let numSlides = breadcrumbs.length; + let columnWidth = Math.floor(12 / numSlides); + + return ( +
    +
    +
    + {breadcrumbs.map((breadcrumb, i) => { + + // Depending on the progress the user has already made, we display different + // glyphicons that can also be specified from the outside + let glyphiconClassName; + + if(i >= this.props.slideNum) { + glyphiconClassName = this.props.glyphiconClassNames.pending; + } else { + glyphiconClassName = this.props.glyphiconClassNames.completed; + } + + return ( + + + + ); + })} +
    +
    +
    + ); + } +}); + +export default SlidesContainerBreadcrumbs; \ No newline at end of file diff --git a/js/components/ascribe_table/table_item_acl_filtered.js b/js/components/ascribe_table/table_item_acl_filtered.js index 9a684e36..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){ + if (this.props.notifications && this.props.notifications.length > 0){ return ( - {this.props.requestAction + ' request pending'} + {this.props.notifications[0].action_str} ); } diff --git a/js/components/ascribe_uploader/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js similarity index 66% rename from js/components/ascribe_uploader/file_drag_and_drop.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 7d6c8a66..4c9211c5 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -1,25 +1,19 @@ 'use strict'; import React from 'react'; -import ProgressBar from 'react-progressbar'; +import ProgressBar from 'react-bootstrap/lib/ProgressBar'; import FileDragAndDropDialog from './file_drag_and_drop_dialog'; import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; -import { getLangText } from '../../utils/lang_utils'; +import { getLangText } from '../../../utils/lang_utils'; + // Taken from: https://github.com/fedosejev/react-file-drag-and-drop let FileDragAndDrop = React.createClass({ propTypes: { - className: React.PropTypes.string, - onDragStart: React.PropTypes.func, onDrop: React.PropTypes.func.isRequired, - onDrag: React.PropTypes.func, - onDragEnter: React.PropTypes.func, - onLeave: React.PropTypes.func, - onDragLeave: React.PropTypes.func, onDragOver: React.PropTypes.func, - onDragEnd: React.PropTypes.func, onInactive: React.PropTypes.func, filesToUpload: React.PropTypes.array, handleDeleteFile: React.PropTypes.func, @@ -37,37 +31,16 @@ let FileDragAndDrop = React.createClass({ hashingProgress: React.PropTypes.number, // sets the value of this.state.hashingProgress in reactfineuploader // to -1 which is code for: aborted - handleCancelHashing: React.PropTypes.func - }, + handleCancelHashing: React.PropTypes.func, - handleDragStart(event) { - if (typeof this.props.onDragStart === 'function') { - this.props.onDragStart(event); - } - }, + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }), - handleDrag(event) { - if (typeof this.props.onDrag === 'function') { - this.props.onDrag(event); - } - }, - - handleDragEnd(event) { - if (typeof this.props.onDragEnd === 'function') { - this.props.onDragEnd(event); - } - }, - - handleDragEnter(event) { - if (typeof this.props.onDragEnter === 'function') { - this.props.onDragEnter(event); - } - }, - - handleDragLeave(event) { - if (typeof this.props.onDragLeave === 'function') { - this.props.onDragLeave(event); - } + allowedExtensions: React.PropTypes.string }, handleDragOver(event) { @@ -159,50 +132,64 @@ let FileDragAndDrop = React.createClass({ }, render: function () { + let { filesToUpload, + dropzoneInactive, + className, + hashingProgress, + handleCancelHashing, + multiple, + enableLocalHashing, + fileClassToUpload, + areAssetsDownloadable, + areAssetsEditable, + allowedExtensions + } = this.props; + // has files only is true if there are files that do not have the status deleted or canceled - let hasFiles = this.props.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; - let className = hasFiles ? 'has-files ' : ''; - className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; - className += this.props.className ? ' ' + this.props.className : ''; + let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0; + let updatedClassName = hasFiles ? 'has-files ' : ''; + updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; + updatedClassName += ' file-drag-and-drop'; // if !== -2: triggers a FileDragAndDrop-global spinner - if(this.props.hashingProgress !== -2) { + if(hashingProgress !== -2) { return (
    -

    {getLangText('Computing hash(es)... This may take a few minutes.')}

    -

    - {Math.ceil(this.props.hashingProgress)}% - {getLangText('Cancel hashing')} -

    - +
    +

    {getLangText('Computing hash(es)... This may take a few minutes.')}

    +

    + {getLangText('Cancel hashing')} +

    + +
    ); } else { return (
    + onDrop={this.handleDrop}> + enableLocalHashing={enableLocalHashing} + fileClassToUpload={fileClassToUpload}/> + areAssetsDownloadable={areAssetsDownloadable} + areAssetsEditable={areAssetsEditable}/> + onChange={this.handleDrop} + accept={allowedExtensions}/>
    ); } diff --git a/js/components/ascribe_uploader/file_drag_and_drop_dialog.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js similarity index 67% rename from js/components/ascribe_uploader/file_drag_and_drop_dialog.js rename to js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js index 306eb6f1..f74eb713 100644 --- a/js/components/ascribe_uploader/file_drag_and_drop_dialog.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_dialog.js @@ -3,7 +3,7 @@ import React from 'react'; import Router from 'react-router'; -import { getLangText } from '../../utils/lang_utils'; +import { getLangText } from '../../../utils/lang_utils'; let Link = Router.Link; @@ -12,7 +12,14 @@ let FileDragAndDropDialog = React.createClass({ hasFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool, onClick: React.PropTypes.func, - enableLocalHashing: React.PropTypes.bool + enableLocalHashing: React.PropTypes.bool, + + // A class of a file the user has to upload + // Needs to be defined both in singular as well as in plural + fileClassToUpload: React.PropTypes.shape({ + singular: React.PropTypes.string, + plural: React.PropTypes.string + }) }, mixins: [Router.State], @@ -32,7 +39,7 @@ let FileDragAndDropDialog = React.createClass({ queryParamsUpload.method = 'upload'; return ( - +

    {getLangText('Would you rather')}

    - +
    ); } else { if(this.props.multipleFiles) { return ( - {getLangText('Click or drag to add files')} +

    {getLangText('Drag %s here', this.props.fileClassToUpload.plural)}

    +

    {getLangText('or')}

    + + {getLangText('choose %s to upload', this.props.fileClassToUpload.plural)} +
    ); } else { - let dialog = queryParams.method === 'hash' ? getLangText('choose a file to hash') : getLangText('choose a file to upload'); + let dialog = queryParams.method === 'hash' ? getLangText('choose a %s to hash', this.props.fileClassToUpload.singular) : getLangText('choose a %s to upload', this.props.fileClassToUpload.singular); return ( -

    {getLangText('Drag a file here')}

    +

    {getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}

    {getLangText('or')}

    -