1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-29 00:58:03 +02:00

Merge branch 'master' into AD-744-search-filter-not-working-i-type-

This commit is contained in:
Tim Daubenschütz 2015-10-12 11:06:47 +02:00
commit 1c48b8b828
223 changed files with 12007 additions and 4094 deletions

3
.gitignore vendored
View File

@ -7,6 +7,9 @@ lib-cov
*.pid *.pid
*.gz *.gz
*.sublime-project *.sublime-project
.idea
spool-project.sublime-project
*.sublime-workspace
*.sublime-workspace *.sublime-workspace
webapp-dependencies.txt webapp-dependencies.txt

224
.scss-lint.yml Normal file
View File

@ -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

View File

@ -24,13 +24,16 @@ gulp serve
Additionally, to work on the white labeling functionality, you need to edit your `/etc/hosts` file and add: 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 localhost.com
127.0.0.1 cc.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: For this project, we're using:
* 4 Spaces * 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 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) * 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 Testing
=============== ===============
We're using Facebook's jest to do testing as it integrates nicely with react.js as well. 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/) - [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/) - [Babel: Learn ES6](https://babeljs.io/docs/learn-es6/)
- [egghead's awesome reactjs and flux tutorials](https://egghead.io/) - [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) - [Crockford's genious Javascript: The Good Parts (Tim has a copy)](http://www.amazon.de/JavaScript-Parts-Working-Shallow-Grain/dp/0596517742)

63
docs/feature_list.md Normal file
View File

@ -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))

View File

@ -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* *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) - 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) - 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? - 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) 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 - Refactor string-templating for api_urls
- Use classNames plugin instead of if-conditional-classes - 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 ## React-S3-Fineuploader
- implementation should enable to define all important methods outside - implementation should enable to define all important methods outside
- and: maybe create a utility class for all methods to avoid code duplication - and: maybe create a utility class for all methods to avoid code duplication

View File

@ -23,7 +23,7 @@ var argv = require('yargs').argv;
var server = require('./server.js').app; var server = require('./server.js').app;
var minifyCss = require('gulp-minify-css'); var minifyCss = require('gulp-minify-css');
var uglify = require('gulp-uglify'); var uglify = require('gulp-uglify');
var opn = require('opn');
var config = { var config = {
@ -48,17 +48,16 @@ var config = {
}, },
filesToWatch: [ filesToWatch: [
'build/css/*.css', 'build/css/*.css',
'build/js/*.js', 'build/js/*.js'
'node_modules/react-s3-fine_uploader/*.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 = { var constants = {
BASE_URL: (function () { var baseUrl = process.env.ONION_BASE_URL || '/'; return baseUrl + (baseUrl.match(/\/$/) ? '' : '/'); })(), BASE_URL: (function () { var baseUrl = process.env.ONION_BASE_URL || '/'; return baseUrl + (baseUrl.match(/\/$/) ? '' : '/'); })(),
SERVER_URL: SERVER_URL, 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, DEBUG: !argv.production,
CREDENTIALS: 'ZGltaUBtYWlsaW5hdG9yLmNvbTowMDAwMDAwMDAw' // dimi@mailinator:0000000000 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() { gulp.task('serve', ['browser-sync', 'run-server', 'sass:build', 'sass:watch', 'copy'], function() {
bundle(true); 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) { gulp.task('jest', function(done) {
@ -93,7 +95,9 @@ gulp.task('browser-sync', function() {
browserSync({ browserSync({
files: config.filesToWatch, files: config.filesToWatch,
proxy: 'http://localhost:4000', 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 .pipe(gulpif(!argv.production, sourcemaps.write())) // writes .map file
.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
.pipe(gulpif(argv.production, uglify({ .pipe(gulpif(argv.production, uglify({
mangle: true, mangle: true
compress: {
sequences: true,
dead_code: true,
conditionals: true,
booleans: true,
unused: true,
if_return: true,
join_vars: true,
drop_console: true
}
}))) })))
.on('error', notify.onError('Error: <%= error.message %>')) .on('error', notify.onError('Error: <%= error.message %>'))
.pipe(gulp.dest('./build/js')) .pipe(gulp.dest('./build/js'))

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -29,8 +29,9 @@ class PieceListActions extends ActionQueue {
orderBy, orderBy,
orderAsc, orderAsc,
filterBy, filterBy,
'pieceList': [], pieceList: [],
'pieceListCount': -1 pieceListCount: -1,
unfilteredPieceListCount: -1
}); });
// afterwards, we can load the list // afterwards, we can load the list
@ -46,8 +47,9 @@ class PieceListActions extends ActionQueue {
orderBy, orderBy,
orderAsc, orderAsc,
filterBy, filterBy,
'pieceList': res.pieces, pieceList: res.pieces,
'pieceListCount': res.count pieceListCount: res.count,
unfilteredPieceListCount: res.unfiltered_count
}); });
resolve(); resolve();
}) })
@ -59,7 +61,7 @@ class PieceListActions extends ActionQueue {
PieceListFetcher PieceListFetcher
.fetchRequestActions() .fetchRequestActions()
.then((res) => { .then((res) => {
this.actions.updatePieceListRequestActions(res.piece_ids); this.actions.updatePieceListRequestActions(res);
}) })
.catch((err) => console.logGlobal(err)); .catch((err) => console.logGlobal(err));
} }

View File

@ -26,6 +26,7 @@ import EventActions from './actions/event_actions';
import GoogleAnalyticsHandler from './third_party/ga'; import GoogleAnalyticsHandler from './third_party/ga';
import RavenHandler from './third_party/raven'; import RavenHandler from './third_party/raven';
import IntercomHandler from './third_party/intercom'; import IntercomHandler from './third_party/intercom';
import NotificationsHandler from './third_party/notifications';
/* eslint-enable */ /* eslint-enable */
initLogging(); initLogging();
@ -44,9 +45,7 @@ requests.defaults({
}); });
class AppGateway { class AppGateway {
start() { start() {
let settings; let settings;
let subdomain = window.location.host.split('.')[0]; let subdomain = window.location.host.split('.')[0];
@ -66,12 +65,17 @@ class AppGateway {
load(settings) { load(settings) {
let type = 'default'; let type = 'default';
let subdomain = 'www';
if (settings) { if (settings) {
type = settings.type; type = settings.type;
subdomain = settings.subdomain;
} }
window.document.body.classList.add('client--' + subdomain);
EventActions.applicationWillBoot(settings); EventActions.applicationWillBoot(settings);
Router.run(getRoutes(type), Router.HistoryLocation, (App) => { window.appRouter = Router.run(getRoutes(type, subdomain), Router.HistoryLocation, (App) => {
React.render( React.render(
<App />, <App />,
document.getElementById('main') document.getElementById('main')

View File

@ -20,25 +20,32 @@ let AclProxy = React.createClass({
show: React.PropTypes.bool show: React.PropTypes.bool
}, },
render() { getChildren() {
if(this.props.show) { if (React.Children.count(this.props.children) > 1){
/*
This might ruin styles for header items in the navbar etc
*/
return ( return (
<span> <span>
{this.props.children} {this.props.children}
</span> </span>
); );
}
/* 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 { } else {
if(this.props.aclObject) { if(this.props.aclObject) {
if(this.props.aclObject[this.props.aclName]) { if(this.props.aclObject[this.props.aclName]) {
return ( return this.getChildren();
<span>
{this.props.children}
</span>
);
} else { } 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.'); console.warn('The aclName you\'re filtering for was not present (or undefined) in the aclObject.');
} } */
return null; return null;
} }
} }

View File

@ -21,14 +21,14 @@ let AccordionList = React.createClass({
); );
} else if(this.props.count === 0) { } else if(this.props.count === 0) {
return ( return (
<div> <div className="ascribe-accordion-list-placeholder">
<p className="text-center">{getLangText('We could not find any works related to you...')}</p> <p className="text-center">{getLangText('We could not find any works related to you...')}</p>
<p className="text-center">{getLangText('To register one, click')} <a href="register_piece">{getLangText('here')}</a>!</p> <p className="text-center">{getLangText('To register one, click')} <a href="register_piece">{getLangText('here')}</a>!</p>
</div> </div>
); );
} else { } else {
return ( return (
<div className={this.props.className + ' ascribe-accordion-list-loading'}> <div className={this.props.className + ' ascribe-loading-position'}>
{this.props.loadingElement} {this.props.loadingElement}
</div> </div>
); );

View File

@ -3,151 +3,24 @@
import React from 'react'; import React from 'react';
import Router from 'react-router'; 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({ let AccordionListItem = React.createClass({
propTypes: { propTypes: {
badge: React.PropTypes.object,
className: React.PropTypes.string, className: React.PropTypes.string,
content: React.PropTypes.object, thumbnail: React.PropTypes.object,
children: 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], 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 (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending in one of your editions')}</Tooltip>}>
<Glyphicon glyph='bell'/>
</OverlayTrigger>);
}
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 (
<div
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<CreateEditionsForm
pieceId={this.props.content.id}
handleSuccess={this.handleEditionCreationSuccess}/>
</div>
);
}
},
getLicences() {
// convert this to acl_view_licences later
if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') {
return (
<span>
<span>, </span>
<a href={this.props.content.license_type.url} target="_blank">
{getLangText('%s license', this.props.content.license_type.code)}
</a>
</span>
);
}
},
render() { 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 ( return (
<div className="row"> <div className="row">
@ -155,52 +28,22 @@ let AccordionListItem = React.createClass({
<div className="wrapper"> <div className="wrapper">
<div className="col-xs-4 col-sm-3 col-md-2 col-lg-2 clear-paddings"> <div className="col-xs-4 col-sm-3 col-md-2 col-lg-2 clear-paddings">
<div className="thumbnail-wrapper"> <div className="thumbnail-wrapper">
<Link {...linkData}> {this.props.thumbnail}
<img src={this.props.content.thumbnail.url_safe}/>
</Link>
</div> </div>
</div> </div>
<div className="col-xs-8 col-sm-9 col-md-9 col-lg-9 col-md-offset-1 col-lg-offset-1 accordion-list-item-header"> <div className="col-xs-8 col-sm-9 col-md-9 col-lg-9 col-md-offset-1 col-lg-offset-1 accordion-list-item-header">
<Link {...linkData}> {this.props.heading}
<h1>{this.props.content.title}</h1> {this.props.subheading}
</Link> {this.props.subsubheading}
{this.props.buttons}
<h3>{getLangText('by %s', this.props.content.artist_name)}</h3>
<div>
<span className="pull-left">{this.props.content.date_created.split('-')[0]}</span>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_view_editions">
<AccordionListItemEditionWidget
className="pull-right"
piece={this.props.content}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.onPollingSuccess}/>
</AclProxy>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_submit_to_prize">
<SubmitToPrizeButton
className="pull-right"
piece={this.props.content}
handleSuccess={this.handleSubmitPrizeSuccess}/>
</AclProxy>
{this.getLicences()}
</div>
</div> </div>
<span style={{'clear': 'both'}}></span> <span style={{'clear': 'both'}}></span>
<div className="request-action-batch"> <div className="request-action-badge">
{this.getGlyphicon()} {this.props.badge}
</div> </div>
</div> </div>
</div> </div>
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children} {this.props.children}
</div> </div>
); );

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store'; import EditionListStore from '../../stores/edition_list_store';
import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store'; import PieceListStore from '../../stores/piece_list_store';
import Button from 'react-bootstrap/lib/Button'; 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 { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let AccordionListItemEditionWidget = React.createClass({ let AccordionListItemEditionWidget = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,

View File

@ -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 (
<AccordionListItem
className={this.props.className}
thumbnail={
<Link {...this.getLinkData()}>
<img src={this.props.piece.thumbnail.url_safe}/>
</Link>}
heading={
<Link {...this.getLinkData()}>
<h1>{this.props.piece.title}</h1>
</Link>}
subheading={
<h3>
{getLangText('by ')}
{this.props.artistName ? this.props.artistName : this.props.piece.artist_name}
</h3>
}
subsubheading={this.props.subsubheading}
buttons={this.props.buttons}
badge={this.props.badge}
>
{this.props.children}
</AccordionListItem>
);
}
});
export default AccordionListItemPiece;

View File

@ -160,7 +160,7 @@ let AccordionListItemTableEditions = React.createClass({
let content = item.acl; let content = item.acl;
return { return {
'content': content, 'content': content,
'requestAction': item.request_action 'notifications': item.notifications
}; }, }; },
'acl', 'acl',
getLangText('Actions'), getLangText('Actions'),

View File

@ -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 (
<OverlayTrigger
delay={500}
placement="left"
overlay={<Tooltip>{getLangText('You have actions pending')}</Tooltip>}>
<Glyphicon glyph='bell' color="green"/>
</OverlayTrigger>);
}
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 (
<div
className="ascribe-accordion-list-item-table col-xs-12 col-sm-10 col-md-8 col-lg-8 col-sm-offset-1 col-md-offset-2 col-lg-offset-2">
<CreateEditionsForm
pieceId={this.props.content.id}
handleSuccess={this.handleEditionCreationSuccess}/>
</div>
);
}
},
getLicences() {
// convert this to acl_view_licences later
if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') {
return (
<span>
<span>, </span>
<a href={this.props.content.license_type.url} target="_blank">
{getLangText('%s license', this.props.content.license_type.code)}
</a>
</span>
);
}
},
render() {
return (
<AccordionListItemPiece
className={this.props.className}
piece={this.props.content}
subsubheading={
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
{this.getLicences()}
</div>}
buttons={
<div>
<AclProxy
aclObject={this.props.content.acl}
aclName="acl_view_editions">
<AccordionListItemEditionWidget
className="pull-right"
piece={this.props.content}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.onPollingSuccess}/>
</AclProxy>
</div>}
badge={this.getGlyphicon()}>
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children}
</AccordionListItemPiece>
);
}
});
export default AccordionListItemWallet;

View File

@ -6,15 +6,16 @@ import Header from '../components/header';
import Footer from '../components/footer'; import Footer from '../components/footer';
import GlobalNotification from './global_notification'; import GlobalNotification from './global_notification';
// let Link = Router.Link; import getRoutes from '../routes';
let RouteHandler = Router.RouteHandler;
let RouteHandler = Router.RouteHandler;
let AscribeApp = React.createClass({ let AscribeApp = React.createClass({
render() { render() {
return ( return (
<div className="container ascribe-default-app"> <div className="container ascribe-default-app">
<Header /> <Header routes={getRoutes()} />
<RouteHandler /> <RouteHandler />
<Footer /> <Footer />
<GlobalNotification /> <GlobalNotification />

View File

@ -6,6 +6,7 @@ import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign'; import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer'; import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan'; 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 ShareForm from '../ascribe_forms/form_share_email';
import ModalWrapper from '../ascribe_modal/modal_wrapper'; import ModalWrapper from '../ascribe_modal/modal_wrapper';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
@ -13,8 +14,10 @@ import AppConstants from '../../constants/application_constants';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import { 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({ let AclButton = React.createClass({
propTypes: { propTypes: {
@ -25,6 +28,8 @@ let AclButton = React.createClass({
React.PropTypes.array React.PropTypes.array
]).isRequired, ]).isRequired,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
handleSuccess: React.PropTypes.func.isRequired, handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string className: React.PropTypes.string
}, },
@ -34,15 +39,18 @@ let AclButton = React.createClass({
}, },
actionProperties(){ actionProperties(){
let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username);
if (this.props.action === 'acl_consign'){ if (this.props.action === 'acl_consign'){
return { return {
title: getLangText('Consign artwork'), title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork'), tooltip: getLangText('Have someone else sell the artwork'),
form: ( form: (
<ConsignForm <ConsignForm
message={this.getConsignMessage()} message={message}
id={this.getFormDataId()} id={this.getFormDataId()}
url={apiUrls.ownership_consigns}/> url={ApiUrls.ownership_consigns}/>
), ),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
@ -53,9 +61,9 @@ let AclButton = React.createClass({
tooltip: getLangText('Have the owner manage his sales again'), tooltip: getLangText('Have the owner manage his sales again'),
form: ( form: (
<UnConsignForm <UnConsignForm
message={this.getUnConsignMessage()} message={message}
id={this.getFormDataId()} id={this.getFormDataId()}
url={apiUrls.ownership_unconsigns}/> url={ApiUrls.ownership_unconsigns}/>
), ),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
@ -65,9 +73,9 @@ let AclButton = React.createClass({
tooltip: getLangText('Transfer the ownership of the artwork'), tooltip: getLangText('Transfer the ownership of the artwork'),
form: ( form: (
<TransferForm <TransferForm
message={this.getTransferMessage()} message={message}
id={this.getFormDataId()} id={this.getFormDataId()}
url={apiUrls.ownership_transfers}/> url={ApiUrls.ownership_transfers}/>
), ),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
@ -77,9 +85,21 @@ let AclButton = React.createClass({
title: getLangText('Loan artwork'), title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time'), tooltip: getLangText('Loan your artwork for a limited period of time'),
form: (<LoanForm form: (<LoanForm
message={this.getLoanMessage()} message={message}
id={this.getFormDataId()} id={this.getFormDataId()}
url={this.isPiece() ? apiUrls.ownership_loans_pieces : apiUrls.ownership_loans_editions}/> 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: (<LoanRequestAnswerForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_loans_pieces_request_confirm}/>
), ),
handleSuccess: this.showNotification handleSuccess: this.showNotification
}; };
@ -90,9 +110,9 @@ let AclButton = React.createClass({
tooltip: getLangText('Share the artwork'), tooltip: getLangText('Share the artwork'),
form: ( form: (
<ShareForm <ShareForm
message={this.getShareMessage()} message={message}
id={this.getFormDataId()} id={this.getFormDataId()}
url={this.isPiece() ? apiUrls.ownership_shares_pieces : apiUrls.ownership_shares_editions }/> url={this.isPiece() ? ApiUrls.ownership_shares_pieces : ApiUrls.ownership_shares_editions }/>
), ),
handleSuccess: this.showNotification 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 // Removes the acl_ prefix and converts to upper case
sanitizeAction() { sanitizeAction() {
if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName;
}
return this.props.action.split('acl_')[1].toUpperCase(); return this.props.action.split('acl_')[1].toUpperCase();
}, },
render() { render() {
let shouldDisplay = this.props.availableAcls[this.props.action]; if (this.props.availableAcls){
let aclProps = this.actionProperties(); let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
return ( let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
<ModalWrapper return (
button={ <ModalWrapper
<button className={shouldDisplay ? 'btn btn-default btn-sm ' : 'hidden'}> trigger={
{this.sanitizeAction()} <button className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
</button> {this.sanitizeAction()}
} </button>
handleSuccess={aclProps.handleSuccess} }
title={aclProps.title} handleSuccess={aclProps.handleSuccess}
tooltip={aclProps.tooltip}> title={aclProps.title}>
{aclProps.form} {aclProps.form}
</ModalWrapper> </ModalWrapper>
); );
}
return null;
} }
}); });

View File

@ -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 (
<div className="modal-footer">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</div>
);
}
return (
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv">{this.props.text}</button>
</div>
);
}
});
export default ButtonSubmitOrClose;

View File

@ -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 (
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
</div>
);
}
return (
<div className="modal-footer">
<button type="submit" className="btn btn-ascribe-inv">{this.props.text}</button>
<button className="btn btn-ascribe-inv" onClick={this.props.onClose}>{getLangText('CLOSE')}</button>
</div>
);
}
});
export default ButtonSubmitOrClose;

View File

@ -26,7 +26,7 @@ let DeleteButton = React.createClass({
mixins: [Router.Navigation], mixins: [Router.Navigation],
render: function () { render() {
let availableAcls; let availableAcls;
let btnDelete; let btnDelete;
let content; let content;
@ -61,13 +61,14 @@ let DeleteButton = React.createClass({
} }
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>; btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>;
}
else { } else {
return null; return null;
} }
return ( return (
<ModalWrapper <ModalWrapper
button={btnDelete} trigger={btnDelete}
handleSuccess={this.props.handleSuccess} handleSuccess={this.props.handleSuccess}
title={title}> title={title}>
{content} {content}
@ -77,4 +78,3 @@ let DeleteButton = React.createClass({
}); });
export default DeleteButton; export default DeleteButton;

View File

@ -8,7 +8,7 @@ import ModalWrapper from '../ascribe_modal/modal_wrapper';
import UnConsignRequestForm from './../ascribe_forms/form_unconsign_request'; import UnConsignRequestForm from './../ascribe_forms/form_unconsign_request';
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';
let UnConsignRequestButton = React.createClass({ let UnConsignRequestButton = React.createClass({
@ -21,16 +21,15 @@ let UnConsignRequestButton = React.createClass({
render: function () { render: function () {
return ( return (
<ModalWrapper <ModalWrapper
button={ trigger={
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit"> <Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
REQUEST UNCONSIGN REQUEST UNCONSIGN
</Button> </Button>
} }
handleSuccess={this.props.handleSuccess} handleSuccess={this.props.handleSuccess}
title='Request to Un-Consign' title='Request to Un-Consign'>
tooltip='Ask the consignee to return the ownership of the work back to you'>
<UnConsignRequestForm <UnConsignRequestForm
url={apiUrls.ownership_unconsigns_request} url={ApiUrls.ownership_unconsigns_request}
id={{'bitcoin_id': this.props.edition.bitcoin_id}} id={{'bitcoin_id': this.props.edition.bitcoin_id}}
message={`${getLangText('Hi')}, message={`${getLangText('Hi')},

View File

@ -2,13 +2,10 @@
import React from 'react'; import React from 'react';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin'; import Panel from 'react-bootstrap/lib/Panel';
import classNames from 'classnames';
const CollapsibleParagraph = React.createClass({ const CollapsibleParagraph = React.createClass({
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
@ -24,14 +21,10 @@ const CollapsibleParagraph = React.createClass({
}; };
}, },
mixins: [CollapsibleMixin], getInitialState() {
return {
getCollapsibleDOMNode(){ expanded: this.props.defaultExpanded
return React.findDOMNode(this.refs.panel); };
},
getCollapsibleDimensionValue(){
return React.findDOMNode(this.refs.panel).scrollHeight;
}, },
handleToggle(e){ handleToggle(e){
@ -40,19 +33,21 @@ const CollapsibleParagraph = React.createClass({
}, },
render() { render() {
let styles = this.getCollapsibleClassSet(); let text = this.state.expanded ? '-' : '+';
let text = this.isExpanded() ? '-' : '+';
if(this.props.show) { if(this.props.show) {
return ( return (
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<div className="ascribe-edition-collapsible-wrapper"> <div className="ascribe-collapsible-wrapper">
<div onClick={this.handleToggle}> <div onClick={this.handleToggle}>
<span>{text} {this.props.title}</span> <span>{text} {this.props.title}</span>
</div> </div>
<div ref='panel' className={classNames(styles) + ' ascribe-edition-collapible-content'}> <Panel
collapsible
expanded={this.state.expanded}
className="ascribe-collapsible-content">
{this.props.children} {this.props.children}
</div> </Panel>
</div> </div>
</div> </div>
); );

View File

@ -17,9 +17,9 @@ let DetailProperty = React.createClass({
getDefaultProps() { getDefaultProps() {
return { return {
separator: ':', separator: '',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2', 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' 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 ( return (
<div className="row ascribe-detail-property"> <div className="row ascribe-detail-property">
<div className="row-same-height"> <div className="row-same-height">
<div className={this.props.labelClassName + ' col-xs-height col-bottom ascribe-detail-property-label'}> <div className={this.props.labelClassName}>
{ this.props.label + this.props.separator} { this.props.label } { this.props.separator}
</div> </div>
<div <div
className={this.props.valueClassName + ' col-xs-height col-bottom ascribe-detail-property-value'} className={this.props.valueClassName}
style={styles}> style={styles}>
{value} {value}
</div> </div>

View File

@ -16,6 +16,7 @@ import PieceListActions from '../../actions/piece_list_actions';
import PieceListStore from '../../stores/piece_list_store'; import PieceListStore from '../../stores/piece_list_store';
import EditionListActions from '../../actions/edition_list_actions'; import EditionListActions from '../../actions/edition_list_actions';
import HistoryIterator from './history_iterator';
import MediaContainer from './media_container'; import MediaContainer from './media_container';
@ -24,11 +25,10 @@ import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph
import Form from './../ascribe_forms/form'; import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property'; import Property from './../ascribe_forms/property';
import EditionDetailProperty from './detail_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 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 AclButtonList from './../ascribe_buttons/acl_button_list';
import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button'; import UnConsignRequestButton from './../ascribe_buttons/unconsign_request_button';
import DeleteButton from '../ascribe_buttons/delete_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 GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; 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 AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -86,10 +88,8 @@ let Edition = React.createClass({
}, },
handleDeleteSuccess(response) { handleDeleteSuccess(response) {
PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.refreshCollection();
this.state.orderBy, this.state.orderAsc, this.state.filterBy);
EditionListActions.refreshEditionList(this.props.edition.parent);
EditionListActions.closeAllEditionLists(); EditionListActions.closeAllEditionLists();
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
@ -99,6 +99,12 @@ let Edition = React.createClass({
this.transitionTo('pieces'); 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() { render() {
return ( return (
<Row> <Row>
@ -108,14 +114,15 @@ let Edition = React.createClass({
</Col> </Col>
<Col md={6} className="ascribe-edition-details"> <Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header"> <div className="ascribe-detail-header">
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
<hr/> <hr/>
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
<EditionDetailProperty label="BY" value={this.props.edition.artist_name} /> <EditionDetailProperty label="BY" value={this.props.edition.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.edition.date_created.slice(0, 4) } /> <EditionDetailProperty label="DATE" value={ this.props.edition.date_created.slice(0, 4) } />
<hr/> <hr/>
</div> </div>
<EditionSummary <EditionSummary
handleSuccess={this.props.loadEdition} handleSuccess={this.props.loadEdition}
refreshCollection={this.refreshCollection}
currentUser={this.state.currentUser} currentUser={this.state.currentUser}
edition={this.props.edition} edition={this.props.edition}
handleDeleteSuccess={this.handleDeleteSuccess}/> handleDeleteSuccess={this.handleDeleteSuccess}/>
@ -130,42 +137,55 @@ let Edition = React.createClass({
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Provenance/Ownership History')} title={getLangText('Provenance/Ownership History')}
show={this.props.edition.ownership_history && this.props.edition.ownership_history.length > 0}> show={this.props.edition.ownership_history && this.props.edition.ownership_history.length > 0}>
<EditionDetailHistoryIterator <HistoryIterator
history={this.props.edition.ownership_history} /> history={this.props.edition.ownership_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Consignment History')} title={getLangText('Consignment History')}
show={this.props.edition.consign_history && this.props.edition.consign_history.length > 0}> show={this.props.edition.consign_history && this.props.edition.consign_history.length > 0}>
<EditionDetailHistoryIterator <HistoryIterator
history={this.props.edition.consign_history} /> history={this.props.edition.consign_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Loan History')} title={getLangText('Loan History')}
show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}> show={this.props.edition.loan_history && this.props.edition.loan_history.length > 0}>
<EditionDetailHistoryIterator <HistoryIterator
history={this.props.edition.loan_history} /> history={this.props.edition.loan_history} />
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title="Notes" title="Notes"
show={(this.state.currentUser.username && true || false) || show={!!(this.state.currentUser.username
(this.props.edition.acl.acl_edit || this.props.edition.public_note)}> || this.props.edition.acl.acl_edit
<EditionPersonalNote || this.props.edition.public_note)}>
currentUser={this.state.currentUser} <Note
handleSuccess={this.props.loadEdition} id={() => {return {'bitcoin_id': this.props.edition.bitcoin_id}; }}
edition={this.props.edition}/> label={getLangText('Personal note (private)')}
<EditionPublicEditionNote defaultValue={this.props.edition.private_note ? this.props.edition.private_note : null}
handleSuccess={this.props.loadEdition} placeholder={getLangText('Enter your comments ...')}
edition={this.props.edition}/> editable={true}
successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_edition}
currentUser={this.state.currentUser}/>
<Note
id={() => {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}/>
</CollapsibleParagraph> </CollapsibleParagraph>
<CollapsibleParagraph <CollapsibleParagraph
title={getLangText('Further Details')} title={getLangText('Further Details')}
show={this.props.edition.acl.acl_edit show={this.props.edition.acl.acl_edit
|| Object.keys(this.props.edition.extra_data).length > 0 || Object.keys(this.props.edition.extra_data).length > 0
|| this.props.edition.other_data !== null}> || this.props.edition.other_data.length > 0}>
<EditionFurtherDetails <EditionFurtherDetails
editable={this.props.edition.acl.acl_edit} editable={this.props.edition.acl.acl_edit}
pieceId={this.props.edition.parent} pieceId={this.props.edition.parent}
@ -191,14 +211,22 @@ let EditionSummary = React.createClass({
edition: React.PropTypes.object, edition: React.PropTypes.object,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
handleDeleteSuccess: React.PropTypes.func handleDeleteSuccess: React.PropTypes.func,
refreshCollection: React.PropTypes.func
}, },
getTransferWithdrawData(){ getTransferWithdrawData(){
return {'bitcoin_id': this.props.edition.bitcoin_id}; return {'bitcoin_id': this.props.edition.bitcoin_id};
}, },
handleSuccess() {
this.props.refreshCollection();
this.props.handleSuccess();
},
showNotification(response){ showNotification(response){
this.props.handleSuccess(); this.props.handleSuccess();
if (response){ if (response){
let notification = new GlobalNotificationModel(response.notification, 'success'); let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
@ -220,12 +248,15 @@ let EditionSummary = React.createClass({
getActions(){ getActions(){
let actions = null; let actions = null;
if (this.props.edition.request_action && this.props.edition.request_action.length > 0){ if (this.props.edition &&
this.props.edition.notifications &&
this.props.edition.notifications.length > 0){
actions = ( actions = (
<RequestActionForm <ListRequestActions
pieceOrEditions={[this.props.edition]}
currentUser={this.props.currentUser} currentUser={this.props.currentUser}
editions={ [this.props.edition] } handleSuccess={this.showNotification}
handleSuccess={this.showNotification}/>); notifications={this.props.edition.notifications}/>);
} }
else { 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) { if (this.props.edition.status.length > 0 && this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer) {
withdrawButton = ( withdrawButton = (
<Form <Form
url={apiUrls.ownership_transfers_withdraw} url={ApiUrls.ownership_transfers_withdraw}
getFormData={this.getTransferWithdrawData} getFormData={this.getTransferWithdrawData}
handleSuccess={this.showNotification} handleSuccess={this.showNotification}
className='inline'> className='inline'
isInline={true}>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit"> <Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
WITHDRAW TRANSFER WITHDRAW TRANSFER
</Button> </Button>
@ -259,7 +291,7 @@ let EditionSummary = React.createClass({
className="text-center ascribe-button-list" className="text-center ascribe-button-list"
availableAcls={this.props.edition.acl} availableAcls={this.props.edition.acl}
editions={[this.props.edition]} editions={[this.props.edition]}
handleSuccess={this.props.handleSuccess}> handleSuccess={this.handleSuccess}>
{withdrawButton} {withdrawButton}
<DeleteButton <DeleteButton
handleSuccess={this.props.handleDeleteSuccess} handleSuccess={this.props.handleDeleteSuccess}
@ -284,119 +316,16 @@ let EditionSummary = React.createClass({
<EditionDetailProperty <EditionDetailProperty
label={getLangText('OWNER')} label={getLangText('OWNER')}
value={ this.props.edition.owner } /> value={ this.props.edition.owner } />
<LicenseDetail license={this.props.edition.license_type}/>
{this.getStatus()} {this.getStatus()}
{this.getActions()} {this.getActions()}
<hr/> <hr/>
</div> </div>
); );
} }
}); });
let EditionDetailHistoryIterator = React.createClass({
propTypes: {
history: React.PropTypes.array
},
render() {
return (
<Form>
{this.props.history.map((historicalEvent, i) => {
return (
<Property
name={i}
key={i}
label={ historicalEvent[0] }
editable={false}>
<pre className="ascribe-pre">{ historicalEvent[1] }</pre>
</Property>
);
})}
<hr />
</Form>
);
}
});
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 (
<Form
url={apiUrls.note_notes}
handleSuccess={this.showNotification}>
<Property
name='note'
label={getLangText('Personal note (private)')}
editable={true}>
<InputTextAreaToggable
rows={1}
editable={true}
defaultValue={this.props.edition.note_from_user}
placeholder={getLangText('Enter a personal note%s', '...')}/>
</Property>
<Property hidden={true} name='bitcoin_id'>
<input defaultValue={this.props.edition.bitcoin_id}/>
</Property>
<hr />
</Form>
);
}
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 (
<Form
url={apiUrls.note_edition}
handleSuccess={this.showNotification}>
<Property
name='note'
label={getLangText('Edition note (public)')}
editable={isEditable}>
<InputTextAreaToggable
rows={1}
editable={isEditable}
defaultValue={this.props.edition.public_note}
placeholder={getLangText('Enter a public note for this edition%s', '...')}
required="required"/>
</Property>
<Property hidden={true} name='bitcoin_id'>
<input defaultValue={this.props.edition.bitcoin_id}/>
</Property>
<hr />
</Form>
);
}
return null;
}
});
let CoaDetails = React.createClass({ let CoaDetails = React.createClass({
propTypes: { propTypes: {
edition: React.PropTypes.object edition: React.PropTypes.object

View File

@ -9,6 +9,8 @@ import Edition from './edition';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
/** /**
* This is the component that implements resource/data specific functionality * This is the component that implements resource/data specific functionality
*/ */
@ -34,6 +36,15 @@ let EditionContainer = React.createClass({
EditionActions.fetchOne(this.props.params.editionId); 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() { componentWillUnmount() {
// Every time we're leaving the edition detail page, // Every time we're leaving the edition detail page,
// just reset the edition that is saved in the edition store // just reset the edition that is saved in the edition store
@ -50,7 +61,7 @@ let EditionContainer = React.createClass({
}, },
render() { render() {
if('title' in this.state.edition) { if(this.state.edition && this.state.edition.title) {
return ( return (
<Edition <Edition
edition={this.state.edition} edition={this.state.edition}

View File

@ -5,28 +5,24 @@ import React from 'react';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; import Col from 'react-bootstrap/lib/Col';
import Form from './../ascribe_forms/form'; import Form from './../ascribe_forms/form';
import Property from './../ascribe_forms/property';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import apiUrls from '../../constants/api_urls'; import FurtherDetailsFileuploader from './further_details_fileuploader';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let FurtherDetails = React.createClass({ let FurtherDetails = React.createClass({
propTypes: { propTypes: {
editable: React.PropTypes.bool, editable: React.PropTypes.bool,
pieceId: React.PropTypes.number, pieceId: React.PropTypes.number,
extraData: React.PropTypes.object, extraData: React.PropTypes.object,
otherData: React.PropTypes.object, otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -42,9 +38,9 @@ let FurtherDetails = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
submitKey(key){ submitFile(file){
this.setState({ this.setState({
otherDataKey: key otherDataKey: file.key
}); });
}, },
@ -54,17 +50,7 @@ let FurtherDetails = React.createClass({
}); });
}, },
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() { render() {
//return (<span />);
return ( return (
<Row> <Row>
<Col md={12} className="ascribe-edition-personal-note"> <Col md={12} className="ascribe-edition-personal-note">
@ -90,93 +76,23 @@ let FurtherDetails = React.createClass({
editable={this.props.editable} editable={this.props.editable}
pieceId={this.props.pieceId} pieceId={this.props.pieceId}
extraData={this.props.extraData} /> extraData={this.props.extraData} />
<FileUploader <Form>
submitKey={this.submitKey} <FurtherDetailsFileuploader
setIsUploadReady={this.setIsUploadReady} submitFile={this.submitFile}
isReadyForFormSubmission={this.isReadyForFormSubmission} setIsUploadReady={this.setIsUploadReady}
editable={this.props.editable} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
pieceId={this.props.pieceId} editable={this.props.editable}
otherData={this.props.otherData}/> overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
multiple={true}/>
</Form>
</Col> </Col>
</Row> </Row>
); );
} }
}); });
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 (
<Form>
<Property
label="Additional files (max. 10MB)">
<ReactS3FineUploader
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'otherdata',
pieceId: this.props.pieceId
}}
createBlobRoutine={{
url: apiUrls.blob_otherdatas,
pieceId: this.props.pieceId
}}
validation={{
itemLimit: 100000,
sizeLimit: '10000000'
}}
submitKey={this.props.submitKey}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
params: {
'pk': this.props.otherData ? this.props.otherData.id : null
},
cors: {
expected: true,
sendCredentials: true
}
}}
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)
}
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}/>
</Property>
<hr />
</Form>
);
}
});
export default FurtherDetails; export default FurtherDetails;

View File

@ -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 (
<Property
label="Additional files (max. 50MB per file)">
<ReactS3FineUploader
uploadStarted={this.props.uploadStarted}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'otherdata',
pieceId: this.props.pieceId
}}
createBlobRoutine={{
url: ApiUrls.blob_otherdatas,
pieceId: this.props.pieceId
}}
validation={AppConstants.fineUploader.validation.additionalData}
submitFile={this.props.submitFile}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
session={{
endpoint: AppConstants.serverUrl + 'api/blob/otherdatas/fineuploader_session/',
customHeaders: {
'X-CSRFToken': getCookie(AppConstants.csrftoken)
},
params: {
'pk': otherDataIds
},
cors: {
expected: true,
sendCredentials: true
}
}}
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)
}
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple}/>
</Property>
);
}
});
export default FurtherDetailsFileuploader;

View File

@ -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 (
<Form>
{this.props.history.map((historicalEvent, i) => {
return (
<Property
name={i}
key={i}
label={ historicalEvent[0] }
editable={false}>
<pre className="ascribe-pre">{ historicalEvent[1] }</pre>
</Property>
);
})}
<hr />
</Form>
);
}
});
export default HistoryIterator;

View File

@ -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 (
<DetailProperty
label="LICENSE"
value={
<a href={this.props.license.url} target="_blank">
{ this.props.license.code.toUpperCase() + ': ' + this.props.license.name}
</a>
}
/>
);
}
});
export default LicenseDetail;

View File

@ -19,7 +19,33 @@ const EMBED_IFRAME_HEIGHT = {
let MediaContainer = React.createClass({ let MediaContainer = React.createClass({
propTypes: { 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() { render() {
@ -46,7 +72,7 @@ let MediaContainer = React.createClass({
} }
panel={ panel={
<pre className=""> <pre className="">
{'<iframe width="560" height="' + height + '" src="http://embed.ascribe.io/content/' {'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/'
+ this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'} + this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
</pre> </pre>
}/> }/>

View File

@ -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 (
<Form
url={this.props.url}
getFormData={this.props.id}
handleSuccess={this.showNotification}
disabled={!this.props.editable}>
<Property
name='note'
label={this.props.label}>
<InputTextAreaToggable
rows={1}
defaultValue={this.props.defaultValue}
placeholder={this.props.placeholder}/>
</Property>
<hr />
</Form>
);
}
return null;
}
});
export default Note;

View File

@ -1,38 +1,14 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
import Row from 'react-bootstrap/lib/Row'; import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col'; 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 PieceActions from '../../actions/piece_actions';
import MediaContainer from './media_container'; 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 * This is the component that implements display-specific functionality
@ -40,97 +16,16 @@ import { mergeOptions } from '../../utils/general_utils';
let Piece = React.createClass({ let Piece = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object, piece: React.PropTypes.object,
header: React.PropTypes.object,
subheader: React.PropTypes.object,
buttons: React.PropTypes.object,
loadPiece: React.PropTypes.func, loadPiece: React.PropTypes.func,
children: React.PropTypes.object children: React.PropTypes.object
}, },
mixins: [Router.Navigation],
getInitialState() { updateObject() {
return mergeOptions( return PieceActions.fetchOne(this.props.piece.id);
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 (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.props.piece.id}
handleSuccess={this.handleEditionCreationSuccess} />
<hr/>
</div>
);
} else {
return (<hr/>);
}
},
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);
}, },
render() { render() {
@ -138,38 +33,14 @@ let Piece = React.createClass({
<Row> <Row>
<Col md={6}> <Col md={6}>
<MediaContainer <MediaContainer
refreshObject={this.updateObject}
content={this.props.piece}/> content={this.props.piece}/>
</Col> </Col>
<Col md={6} className="ascribe-edition-details"> <Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header"> {this.props.header}
<h1 className="ascribe-detail-title">{this.props.piece.title}</h1> {this.props.subheader}
<hr/> {this.props.buttons}
<EditionDetailProperty label="BY" value={this.props.piece.artist_name} />
<EditionDetailProperty label="DATE" value={ this.props.piece.date_created.slice(0, 4) } />
{this.props.piece.num_editions > 0 ? <EditionDetailProperty label="EDITIONS" value={ this.props.piece.num_editions } /> : null}
<hr/>
</div>
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.props.piece.user_registered } />
</div>
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.props.piece.acl}
editions={this.props.piece}
handleSuccess={this.props.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.props.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.props.piece}/>
</AclButtonList>
{this.getCreateEditionsDialog()}
{this.props.children} {this.props.children}
</Col> </Col>

View File

@ -1,37 +1,66 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import Router from 'react-router';
import PieceActions from '../../actions/piece_actions'; import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store'; 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 Piece from './piece';
import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph';
import FurtherDetails from './further_details'; 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 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 * This is the component that implements resource/data specific functionality
*/ */
let PieceContainer = React.createClass({ let PieceContainer = React.createClass({
getInitialState() {
return PieceStore.getState();
},
onChange(state) { mixins: [Router.Navigation],
this.setState(state);
if (!state.piece.digital_work) { getInitialState() {
return; return mergeOptions(
} UserStore.getState(),
let isEncoding = state.piece.digital_work.isEncoding; PieceListStore.getState(),
if (state.piece.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { PieceStore.getState(),
let timerId = window.setInterval(() => PieceActions.fetchOne(this.props.params.pieceId), 10000); {
this.setState({timerId: timerId}); showCreateEditionsDialog: false
} }
);
}, },
componentDidMount() { componentDidMount() {
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
UserActions.fetchCurrentUser();
PieceStore.listen(this.onChange); PieceStore.listen(this.onChange);
PieceActions.fetchOne(this.props.params.pieceId); 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 // as it will otherwise display wrong/old data once the user loads
// the piece detail a second time // the piece detail a second time
PieceActions.updatePiece({}); PieceActions.updatePiece({});
window.clearInterval(this.state.timerId);
PieceStore.unlisten(this.onChange); 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() { loadPiece() {
PieceActions.fetchOne(this.props.params.pieceId); 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 (
<div style={{marginTop: '1em'}}>
<CreateEditionsForm
pieceId={this.state.piece.id}
handleSuccess={this.handleEditionCreationSuccess} />
<hr/>
</div>
);
} else {
return (<hr/>);
}
},
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 (
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
handleSuccess={this.loadPiece}
notifications={this.state.piece.notifications}/>);
}
else {
return (
<AclButtonList
className="text-center ascribe-button-list"
availableAcls={this.state.piece.acl}
editions={this.state.piece}
handleSuccess={this.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={this.state.piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={this.state.piece}/>
</AclButtonList>
);
}
},
render() { render() {
if('title' in this.state.piece) { if(this.state.piece && this.state.piece.title) {
return ( return (
<Piece <Piece
piece={this.state.piece} piece={this.state.piece}
loadPiece={this.loadPiece}> loadPiece={this.loadPiece}
header={
<div className="ascribe-detail-header">
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.state.piece.title}</h1>
<DetailProperty label="BY" value={this.state.piece.artist_name} />
<DetailProperty label="DATE" value={ this.state.piece.date_created.slice(0, 4) } />
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null}
<hr/>
</div>
}
subheader={
<div className="ascribe-detail-header">
<DetailProperty label={getLangText('REGISTREE')} value={ this.state.piece.user_registered } />
<DetailProperty label={getLangText('ID')} value={ this.state.piece.bitcoin_id } ellipsis={true} />
<LicenseDetail license={this.state.piece.license_type} />
</div>
}
buttons={this.getActions()}>
{this.getCreateEditionsDialog()}
<CollapsibleParagraph <CollapsibleParagraph
title="Further Details" title={getLangText('Loan History')}
show={this.state.piece.loan_history && this.state.piece.loan_history.length > 0}>
<HistoryIterator
history={this.state.piece.loan_history} />
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Notes')}
show={!!(this.state.currentUser.username || this.state.piece.public_note)}>
<Note
id={this.getId}
label={getLangText('Personal note (private)')}
defaultValue={this.state.piece.private_note || null}
placeholder={getLangText('Enter your comments ...')}
editable={true}
successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_piece}
currentUser={this.state.currentUser}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Further Details')}
show={this.state.piece.acl.acl_edit show={this.state.piece.acl.acl_edit
|| Object.keys(this.state.piece.extra_data).length > 0 || Object.keys(this.state.piece.extra_data).length > 0
|| this.state.piece.other_data !== null} || this.state.piece.other_data.length > 0}
defaultExpanded={true}> defaultExpanded={true}>
<FurtherDetails <FurtherDetails
editable={this.state.piece.acl.acl_edit} editable={this.state.piece.acl.acl_edit}
@ -70,6 +263,7 @@ let PieceContainer = React.createClass({
otherData={this.state.piece.other_data} otherData={this.state.piece.other_data}
handleSuccess={this.loadPiece}/> handleSuccess={this.loadPiece}/>
</CollapsibleParagraph> </CollapsibleParagraph>
</Piece> </Piece>
); );
} else { } else {

View File

@ -8,12 +8,11 @@ import Property from '../ascribe_forms/property';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import apiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let CreateEditionsForm = React.createClass({ let CreateEditionsForm = React.createClass({
propTypes: { propTypes: {
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
pieceId: React.PropTypes.number pieceId: React.PropTypes.number
@ -38,9 +37,15 @@ let CreateEditionsForm = React.createClass({
return ( return (
<Form <Form
ref='form' ref='form'
url={apiUrls.editions} url={ApiUrls.editions}
getFormData={this.getFormData} getFormData={this.getFormData}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Create editions')}
</button>}
spinner={ spinner={
<button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner"> <button className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" /> <img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />

View File

@ -6,6 +6,9 @@ import ReactAddons from 'react/addons';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import AlertDismissable from './alert'; import AlertDismissable from './alert';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import requests from '../../utils/requests'; import requests from '../../utils/requests';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
@ -15,21 +18,39 @@ import { mergeOptionsWithDuplicates } from '../../utils/general_utils';
let Form = React.createClass({ let Form = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string, url: React.PropTypes.string,
buttons: React.PropTypes.object, method: React.PropTypes.string,
buttonSubmitText: React.PropTypes.string, buttonSubmitText: React.PropTypes.string,
spinner: React.PropTypes.object,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
getFormData: React.PropTypes.func, getFormData: React.PropTypes.func,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.object,
React.PropTypes.array React.PropTypes.array
]), ]),
className: React.PropTypes.string className: React.PropTypes.string,
spinner: React.PropTypes.element,
buttons: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.arrayOf(React.PropTypes.element)
]),
// Can be used to freeze the whole form
disabled: React.PropTypes.bool,
// You can use the form for inline requests, like the submit click on a button.
// For the form to then not display the error on top, you need to enable this option.
// It will make use of the GlobalNotification
isInline: React.PropTypes.bool,
autoComplete: React.PropTypes.string,
onReset: React.PropTypes.func
}, },
getDefaultProps() { getDefaultProps() {
return { return {
buttonSubmitText: 'SAVE' method: 'post',
buttonSubmitText: 'SAVE',
autoComplete: 'off'
}; };
}, },
@ -40,67 +61,110 @@ let Form = React.createClass({
errors: [] errors: []
}; };
}, },
reset(){
for (let ref in this.refs){ reset() {
if (typeof this.refs[ref].reset === 'function'){ // If onReset prop is defined from outside,
// notify component that a form reset is happening.
if(typeof this.props.onReset === 'function') {
this.props.onReset();
}
for(let ref in this.refs) {
if(typeof this.refs[ref].reset === 'function') {
this.refs[ref].reset(); this.refs[ref].reset();
} }
} }
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },
submit(event){ submit(event){
if (event) { if(event) {
event.preventDefault(); event.preventDefault();
} }
this.setState({submitted: true}); this.setState({submitted: true});
this.clearErrors(); this.clearErrors();
let action = (this.httpVerb && this.httpVerb()) || 'post';
window.setTimeout(() => this[action](), 100); // selecting http method based on props
if(this[this.props.method] && typeof this[this.props.method] === 'function') {
window.setTimeout(() => this[this.props.method](), 100);
} else {
throw new Error('This HTTP method is not supported by form.js (' + this.props.method + ')');
}
}, },
post(){
post() {
requests requests
.post(this.props.url, { body: this.getFormData() }) .post(this.props.url, { body: this.getFormData() })
.then(this.handleSuccess) .then(this.handleSuccess)
.catch(this.handleError); .catch(this.handleError);
}, },
getFormData(){ put() {
requests
.put(this.props.url, { body: this.getFormData() })
.then(this.handleSuccess)
.catch(this.handleError);
},
patch() {
requests
.patch(this.props.url, { body: this.getFormData() })
.then(this.handleSuccess)
.catch(this.handleError);
},
delete() {
requests
.delete(this.props.url, this.getFormData())
.then(this.handleSuccess)
.catch(this.handleError);
},
getFormData() {
let data = {}; let data = {};
for (let ref in this.refs){
for(let ref in this.refs) {
data[this.refs[ref].props.name] = this.refs[ref].state.value; data[this.refs[ref].props.name] = this.refs[ref].state.value;
} }
if ('getFormData' in this.props){ if(typeof this.props.getFormData === 'function') {
data = mergeOptionsWithDuplicates(data, this.props.getFormData()); data = mergeOptionsWithDuplicates(data, this.props.getFormData());
} }
return data; return data;
}, },
handleChangeChild(){ handleChangeChild(){
this.setState({edited: true}); this.setState({ edited: true });
}, },
handleSuccess(response){ handleSuccess(response){
if ('handleSuccess' in this.props){ if(typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess(response); this.props.handleSuccess(response);
} }
for (var ref in this.refs){
if ('handleSuccess' in this.refs[ref]){ for(let ref in this.refs) {
if(this.refs[ref] && typeof this.refs[ref].handleSuccess === 'function'){
this.refs[ref].handleSuccess(); this.refs[ref].handleSuccess();
} }
} }
this.setState({edited: false, submitted: false}); this.setState({
edited: false,
submitted: false
});
}, },
handleError(err){ handleError(err){
if (err.json) { if (err.json) {
for (var input in err.json.errors){ for (let input in err.json.errors){
if (this.refs && this.refs[input] && this.refs[input].state) { if (this.refs && this.refs[input] && this.refs[input].state) {
this.refs[input].setErrors( err.json.errors[input]); this.refs[input].setErrors(err.json.errors[input]);
} else { } else {
this.setState({errors: this.state.errors.concat(err.json.errors[input])}); this.setState({errors: this.state.errors.concat(err.json.errors[input])});
} }
} }
} } else {
else {
let formData = this.getFormData(); let formData = this.getFormData();
// sentry shouldn't post the user's password // sentry shouldn't post the user's password
@ -109,18 +173,27 @@ let Form = React.createClass({
} }
console.logGlobal(err, false, formData); console.logGlobal(err, false, formData);
this.setState({errors: [getLangText('Something went wrong, please try again later')]});
if(this.props.isInline) {
let notification = new GlobalNotificationModel(getLangText('Something went wrong, please try again later'), 'danger');
GlobalNotificationActions.appendGlobalNotification(notification);
} else {
this.setState({errors: [getLangText('Something went wrong, please try again later')]});
}
} }
this.setState({submitted: false}); this.setState({submitted: false});
}, },
clearErrors(){ clearErrors(){
for (var ref in this.refs){ for(let ref in this.refs){
if ('clearErrors' in this.refs[ref]){ if (this.refs[ref] && typeof this.refs[ref].clearErrors === 'function'){
this.refs[ref].clearErrors(); this.refs[ref].clearErrors();
} }
} }
this.setState({errors: []}); this.setState({errors: []});
}, },
getButtons() { getButtons() {
if (this.state.submitted){ if (this.state.submitted){
return this.props.spinner; return this.props.spinner;
@ -130,12 +203,20 @@ let Form = React.createClass({
} }
let buttons = null; let buttons = null;
if (this.state.edited){ if (this.state.edited && !this.props.disabled){
buttons = ( buttons = (
<div className="row" style={{margin: 0}}> <div className="row" style={{margin: 0}}>
<p className="pull-right"> <p className="pull-right">
<Button className="btn btn-default btn-sm ascribe-margin-1px" type="submit">{this.props.buttonSubmitText}</Button> <Button
<Button className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.reset}>CANCEL</Button> className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">
{this.props.buttonSubmitText}
</Button>
<Button
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
type="reset">
CANCEL
</Button>
</p> </p>
</div> </div>
); );
@ -143,6 +224,7 @@ let Form = React.createClass({
} }
return buttons; return buttons;
}, },
getErrors() { getErrors() {
let errors = null; let errors = null;
if (this.state.errors.length > 0){ if (this.state.errors.length > 0){
@ -152,16 +234,41 @@ let Form = React.createClass({
} }
return errors; return errors;
}, },
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child) => {
if (child) { if (child) {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
handleChange: this.handleChangeChild, 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 (
<span>
<input style={{display: 'none'}} type="text" name="fakeusernameremembered"/>
<input style={{display: 'none'}} type="password" name="fakepasswordremembered"/>
</span>
);
} else {
return null;
}
},
render() { render() {
let className = 'ascribe-form'; let className = 'ascribe-form';
@ -174,7 +281,9 @@ let Form = React.createClass({
role="form" role="form"
className={className} className={className}
onSubmit={this.submit} onSubmit={this.submit}
autoComplete="on"> onReset={this.reset}
autoComplete={this.props.autoComplete}>
{this.getFakeAutocompletableInputs()}
{this.getErrors()} {this.getErrors()}
{this.renderChildren()} {this.renderChildren()}
{this.getButtons()} {this.getButtons()}

View File

@ -8,7 +8,6 @@ import Form from './form';
import Property from './property'; import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable'; import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
@ -18,7 +17,6 @@ let ConsignForm = React.createClass({
url: React.PropTypes.string, url: React.PropTypes.string,
id: React.PropTypes.object, id: React.PropTypes.object,
message: React.PropTypes.string, message: React.PropTypes.string,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -27,7 +25,6 @@ let ConsignForm = React.createClass({
}, },
render() { render() {
return ( return (
<Form <Form
ref='form' ref='form'
@ -39,11 +36,9 @@ let ConsignForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<Button <Button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('CONSIGN')}</Button> type="submit">
<Button {getLangText('CONSIGN')}
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" </Button>
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -61,10 +56,10 @@ let ConsignForm = React.createClass({
<Property <Property
name='consign_message' name='consign_message'
label={getLangText('Personal Message')} label={getLangText('Personal Message')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required="required"/>

View File

@ -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 (
<Property
name='contract'
label={getLangText('Contract Type')}
onChange={this.onContractChange}>
<select name="contract">
{contractList.map((contract, i) => {
return (
<option
name={i}
key={i}
value={ contract.id }>
{ contract.name }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
if (this.state.contractList && this.state.contractList.length > 0) {
return (
<Form
className="ascribe-form-bordered ascribe-form-wrapper"
ref='form'
url={ApiUrls.ownership_contract_agreements}
getFormData={this.getFormData}
handleSuccess={this.handleSubmitSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Send contract')}
</button>}
spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span>
}>
<div className="ascribe-form-header">
<h3>{getLangText('Contract form')}</h3>
</div>
<Property
name='signee'
label={getLangText('Artist Email')}>
<input
type="email"
placeholder={getLangText('(e.g. andy@warhol.co.uk)')}
required/>
</Property>
{this.getContracts()}
<PropertyCollapsible
name='appendix'
checkboxLabel={getLangText('Add appendix to the contract')}>
<span>{getLangText('Appendix')}</span>
{/* We're using disabled on a form here as PropertyCollapsible currently
does not support the disabled + overrideForm functionality */}
<InputTextAreaToggable
rows={1}
disabled={false}
placeholder={getLangText('This will be appended to the contract selected above')}/>
</PropertyCollapsible>
</Form>
);
}
return (
<div>
<p className="text-center">
{getLangText('No contracts uploaded yet, please go to the ')}
<a href="settings">{getLangText('settings page')}</a>
{getLangText(' and create them.')}
</p>
</div>
);
}
});
export default ContractAgreementForm;

View File

@ -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 (
<Form
ref='form'
url={ApiUrls.users_profile}
getFormData={this.getProfileFormData}
handleSuccess={this.handleSubmitSuccess}>
<Property
name="copyright_association"
className="ascribe-settings-property-collapsible-toggle"
label={getLangText('Copyright Association')}
style={{paddingBottom: 0}}>
<select defaultValue={selectedState} name="contract">
<option
name={0}
key={0}
value={selectDefaultValue}>
{selectDefaultValue}
</option>
{AppConstants.copyrightAssociations.map((association, i) => {
return (
<option
name={i + 1}
key={i + 1}
value={association}>
{ association }
</option>
);
})}
</select>
</Property>
<hr />
</Form>
);
}
return null;
}
});
export default CopyrightAssociationForm;

View File

@ -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 (
<Form
ref='form'
url={ApiUrls.ownership_contract_list}
handleSuccess={this.handleCreateSuccess}>
<Property
name="blob"
label={getLangText('Contract file (*.pdf only, max. 50MB per contract)')}>
<InputFineUploader
submitFileName={this.submitFileName}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'contract'
}}
createBlobRoutine={{
url: ApiUrls.blob_contracts
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.additionalData.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['pdf']
}}
areAssetsDownloadable={true}
areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload}/>
</Property>
<Property
name='name'
label={getLangText('Contract name')}
hidden={true}>
<input
type="text"
value={this.state.contractName}/>
</Property>
<Property
name="is_public"
hidden={true}>
<input
type="checkbox"
value={this.props.isPublic} />
</Property>
</Form>
);
}
});
export default CreateContractForm;

View File

@ -2,33 +2,65 @@
import React from 'react'; import React from 'react';
import requests from '../../utils/requests'; import Form from './form';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let EditionDeleteForm = React.createClass({ let EditionDeleteForm = React.createClass({
mixins: [FormMixin], propTypes: {
editions: React.PropTypes.arrayOf(React.PropTypes.object),
url() { // Propagated by ModalWrapper in most cases
return requests.prepareUrl(ApiUrls.edition_delete, {edition_id: this.getBitcoinIds().join()}); handleSuccess: React.PropTypes.func
},
httpVerb(){
return 'delete';
}, },
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 ( return (
<div className="modal-body"> <Form
ref='form'
url={ApiUrls.edition_delete}
getFormData={this.getFormData}
method="delete"
handleSuccess={this.props.handleSuccess}
buttons={
<div className="modal-footer">
<p className="pull-right">
<button
type="submit"
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
onClick={this.submit}>
{getLangText('YES, DELETE')}
</button>
</p>
</div>
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
}>
<p>{getLangText('Are you sure you would like to permanently delete this edition')}&#63;</p> <p>{getLangText('Are you sure you would like to permanently delete this edition')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p> <p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer"> </Form>
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, DELETE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
); );
} }
}); });

View File

@ -2,37 +2,56 @@
import React from 'react'; import React from 'react';
import requests from '../../utils/requests'; import Form from '../ascribe_forms/form';
import ApiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
import FormMixin from '../../mixins/form_mixin'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let PieceDeleteForm = React.createClass({ let PieceDeleteForm = React.createClass({
propTypes: { propTypes: {
pieceId: React.PropTypes.number pieceId: React.PropTypes.number,
// Propagated by ModalWrapper in most cases
handleSuccess: React.PropTypes.func
}, },
mixins: [FormMixin], getFormData() {
return {
url() { piece_id: this.props.pieceId
return requests.prepareUrl(ApiUrls.piece, {piece_id: this.props.pieceId}); };
}, },
httpVerb() { render() {
return 'delete';
},
renderForm () {
return ( return (
<div className="modal-body"> <Form
ref='form'
url={ApiUrls.piece}
getFormData={this.getFormData}
method="delete"
handleSuccess={this.props.handleSuccess}
buttons={
<div className="modal-footer">
<p className="pull-right">
<button
type="submit"
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
onClick={this.submit}>
{getLangText('YES, DELETE')}
</button>
</p>
</div>
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
}>
<p>{getLangText('Are you sure you would like to permanently delete this piece')}&#63;</p> <p>{getLangText('Are you sure you would like to permanently delete this piece')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p> <p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer"> </Form>
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, DELETE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
); );
} }
}); });

View File

@ -2,6 +2,8 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import Form from './form'; import Form from './form';
@ -10,70 +12,150 @@ import InputTextAreaToggable from './input_textarea_toggable';
import InputDate from './input_date'; import InputDate from './input_date';
import InputCheckbox from './input_checkbox'; import InputCheckbox from './input_checkbox';
import LoanContractStore from '../../stores/loan_contract_store'; import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import LoanContractActions from '../../actions/loan_contract_actions'; import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let LoanForm = React.createClass({ let LoanForm = React.createClass({
propTypes: { 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, url: React.PropTypes.string,
id: React.PropTypes.object, id: React.PropTypes.object,
message: React.PropTypes.string, message: React.PropTypes.string,
onRequestHide: React.PropTypes.func, createPublicContractAgreement: React.PropTypes.bool,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
getDefaultProps() {
return {
loanHeading: '',
showPersonalMessage: true,
showEndDate: true,
showStartDate: true,
showPassword: true,
createPublicContractAgreement: true
};
},
getInitialState() { getInitialState() {
return LoanContractStore.getState(); return ContractAgreementListStore.getState();
}, },
componentDidMount() { componentDidMount() {
LoanContractStore.listen(this.onChange); ContractAgreementListStore.listen(this.onChange);
LoanContractActions.flushLoanContract(); 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() { componentWillUnmount() {
LoanContractStore.unlisten(this.onChange); ContractAgreementListStore.unlisten(this.onChange);
}, },
onChange(state) { onChange(state) {
this.setState(state); this.setState(state);
}, },
getFormData(){ getContractAgreementsOrCreatePublic(email){
return this.props.id; ContractAgreementListActions.flushContractAgreementList.defer();
if (email) {
// fetch the available contractagreements (pending/accepted)
ContractAgreementListActions.fetchAvailableContractAgreementList(email, true);
}
}, },
handleOnBlur(event) { getFormData(){
LoanContractActions.fetchLoanContract(event.target.value); 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() { 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 // we need to define a key on the InputCheckboxes as otherwise
// react is not rerendering them on a store switch and is keeping // react is not rerendering them on a store switch and is keeping
// the default value of the component (which is in that case true) // the default value of the component (which is in that case true)
return ( let contractAgreement = this.state.contractAgreementList[0];
<Property let contract = contractAgreement.contract;
name="terms"
className="ascribe-settings-property-collapsible-toggle" if(contractAgreement.datetime_accepted) {
style={{paddingBottom: 0}}> return (
<InputCheckbox <Property
key="terms_explicitly" name="terms"
defaultChecked={false}> label={getLangText('Loan Contract')}
<span> hidden={false}
{getLangText('I agree to the')}&nbsp; className="notification-contract-pdf">
<a href={this.state.contractUrl} target="_blank"> <embed
{getLangText('terms of')} {this.state.contractEmail} className="loan-form"
</a> src={contract.blob.url_safe}
</span> alt="pdf"
</InputCheckbox> pluginspage="http://www.adobe.com/products/acrobat/readstep2.html"/>
</Property> {/* We still need to send the server information that we're accepting */}
); <InputCheckbox
style={{'display': 'none'}}
key="terms_implicitly"
defaultChecked={true} />
</Property>
);
} else {
return (
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
key="terms_explicitly"
defaultChecked={false}>
<span>
{getLangText('I agree to the')}&nbsp;
<a href={contract.blob.url_safe} target="_blank">
{getLangText('terms of ')} {contract.issuer}
</a>
</span>
</InputCheckbox>
</Property>
);
}
} else { } else {
return ( return (
<Property <Property
@ -88,79 +170,129 @@ let LoanForm = React.createClass({
} }
}, },
render() { getAppendix() {
if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) {
let appendix = this.state.contractAgreementList[0].appendix;
if (appendix && appendix.default) {
return (
<Property
name='appendix'
label={getLangText('Appendix')}>
<pre className="ascribe-pre">{appendix.default}</pre>
</Property>
);
}
}
return null;
},
getButtons() {
if(this.props.loanHeading) {
return (
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
{getLangText('Finish process')}
</button>
);
} else {
return (
<div className="modal-footer">
<p className="pull-right">
<Button
className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">
{getLangText('LOAN')}
</Button>
</p>
</div>
);
}
},
render() {
return ( return (
<Form <Form
className={classnames({'ascribe-form-bordered': this.props.loanHeading})}
ref='form' ref='form'
url={this.props.url} url={this.props.url}
getFormData={this.getFormData} getFormData={this.getFormData}
onReset={this.handleOnChange}
handleSuccess={this.props.handleSuccess} handleSuccess={this.props.handleSuccess}
buttons={ buttons={this.getButtons()}
<div className="modal-footer">
<p className="pull-right">
<Button
className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('LOAN')}</Button>
<Button
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p>
</div>}
spinner={ spinner={
<div className="modal-footer"> <div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} /> <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>}> </div>}>
<div className={classnames({'ascribe-form-header': true, 'hidden': !this.props.loanHeading})}>
<h3>{this.props.loanHeading}</h3>
</div>
<Property <Property
name='loanee' name='loanee'
label={getLangText('Loanee Email')} label={getLangText('Loanee Email')}
onBlur={this.handleOnBlur}> editable={!this.props.email}
onChange={this.handleOnChange}
overrideForm={!!this.props.email}>
<input <input
value={this.props.email}
type="email" type="email"
placeholder={getLangText('Email of the loanee')} placeholder={getLangText('Email of the loanee')}
required/> required/>
</Property> </Property>
<Property <Property
name='gallery_name' name='gallery'
label={getLangText('Gallery/exhibition (optional)')}> label={getLangText('Gallery/exhibition (optional)')}
editable={!this.props.gallery}
overrideForm={!!this.props.gallery}>
<input <input
value={this.props.gallery}
type="text" type="text"
placeholder={getLangText('Gallery/exhibition (optional)')}/> placeholder={getLangText('Gallery/exhibition (optional)')}/>
</Property> </Property>
<Property <Property
name='startdate' name='startdate'
label={getLangText('Start date')}> label={getLangText('Start date')}
editable={!this.props.startdate}
overrideForm={!!this.props.startdate}
hidden={!this.props.showStartDate}>
<InputDate <InputDate
defaultValue={this.props.startdate}
placeholderText={getLangText('Loan start date')} /> placeholderText={getLangText('Loan start date')} />
</Property> </Property>
<Property <Property
name='enddate' name='enddate'
label={getLangText('End date')}> editable={!this.props.enddate}
overrideForm={!!this.props.enddate}
label={getLangText('End date')}
hidden={!this.props.showEndDate}>
<InputDate <InputDate
defaultValue={this.props.enddate}
placeholderText={getLangText('Loan end date')} /> placeholderText={getLangText('Loan end date')} />
</Property> </Property>
<Property <Property
name='loan_message' name='loan_message'
label={getLangText('Personal Message')} label={getLangText('Personal Message')}
editable={true}> editable={true}
overrideForm={true}
hidden={!this.props.showPersonalMessage}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required={this.props.showPersonalMessage ? 'required' : ''}/>
</Property> </Property>
{this.getContractCheckbox()}
{this.getAppendix()}
<Property <Property
name='password' name='password'
label={getLangText('Password')}> label={getLangText('Password')}
hidden={!this.props.showPassword}>
<input <input
type="password" type="password"
placeholder={getLangText('Enter your password')} placeholder={getLangText('Enter your password')}
required/> required={this.props.showPassword ? 'required' : ''}/>
</Property> </Property>
{this.getContractCheckbox()} {this.props.children}
</Form> </Form>
); );
} }

View File

@ -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 (
<LoanForm
loanHeading={null}
message={''}
id={this.props.id}
url={this.props.url}
email={this.state.loanRequest ? this.state.loanRequest.new_owner : null}
gallery={this.state.loanRequest ? this.state.loanRequest.gallery : null}
startdate={startDate}
enddate={endDate}
showPassword={true}
showPersonalMessage={false}
handleSuccess={this.props.handleSuccess}/>
);
}
return <span/>;
}
});
export default LoanRequestAnswerForm;

View File

@ -11,16 +11,14 @@ import UserActions from '../../actions/user_actions';
import Form from './form'; import Form from './form';
import Property from './property'; 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 AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../utils/lang_utils';
let LoginForm = React.createClass({ let LoginForm = React.createClass({
propTypes: { propTypes: {
headerMessage: React.PropTypes.string, headerMessage: React.PropTypes.string,
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
@ -29,7 +27,7 @@ let LoginForm = React.createClass({
onLogin: React.PropTypes.func onLogin: React.PropTypes.func
}, },
mixins: [Router.Navigation], mixins: [Router.Navigation, Router.State],
getDefaultProps() { getDefaultProps() {
return { return {
@ -97,12 +95,14 @@ let LoginForm = React.createClass({
}, },
render() { render() {
let email = this.getQuery().email || null;
return ( return (
<Form <Form
className="ascribe-form-bordered" className="ascribe-form-bordered"
ref="loginForm" ref="loginForm"
url={apiUrls.users_login} url={ApiUrls.users_login}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
autoComplete="on"
buttons={ buttons={
<button <button
type="submit" type="submit"
@ -114,17 +114,17 @@ let LoginForm = React.createClass({
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" /> <img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span> </span>
}> }>
<FormPropertyHeader> <div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3> <h3>{this.props.headerMessage}</h3>
</FormPropertyHeader> </div>
<Property <Property
name='email' name='email'
label={getLangText('Email')}> label={getLangText('Email')}>
<input <input
type="email" type="email"
placeholder={getLangText('Enter your email')} placeholder={getLangText('Enter your email')}
autoComplete="on" name="email"
name="username" defaultValue={email}
required/> required/>
</Property> </Property>
<Property <Property
@ -133,7 +133,6 @@ let LoginForm = React.createClass({
<input <input
type="password" type="password"
placeholder={getLangText('Enter your password')} placeholder={getLangText('Enter your password')}
autoComplete="on"
name="password" name="password"
required/> required/>
</Property> </Property>

View File

@ -3,9 +3,9 @@
import React from 'react'; import React from 'react';
import requests from '../../utils/requests'; 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 Form from './form';
import Property from './property'; import Property from './property';
@ -20,7 +20,8 @@ let PieceExtraDataForm = React.createClass({
title: React.PropTypes.string, title: React.PropTypes.string,
editable: React.PropTypes.bool editable: React.PropTypes.bool
}, },
getFormData(){
getFormData() {
let extradata = {}; let extradata = {};
extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value; extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value;
return { return {
@ -28,25 +29,25 @@ let PieceExtraDataForm = React.createClass({
piece_id: this.props.pieceId piece_id: this.props.pieceId
}; };
}, },
render() { render() {
let defaultValue = this.props.extraData[this.props.name] || ''; let defaultValue = this.props.extraData[this.props.name] || '';
if (defaultValue.length === 0 && !this.props.editable){ if (defaultValue.length === 0 && !this.props.editable){
return null; 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 ( return (
<Form <Form
ref='form' ref='form'
url={url} url={url}
handleSuccess={this.props.handleSuccess} handleSuccess={this.props.handleSuccess}
getFormData={this.getFormData}> getFormData={this.getFormData}
disabled={!this.props.editable}>
<Property <Property
name={this.props.name} name={this.props.name}
label={this.props.title} label={this.props.title}>
editable={this.props.editable}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={this.props.editable}
defaultValue={defaultValue} defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + this.props.title} placeholder={getLangText('Fill in%s', ' ') + this.props.title}
required="required"/> required="required"/>

View File

@ -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 (
<div className="ascribe-form-header">
{this.props.children}
</div>
);
}
});
export default FormPropertyHeader;

View File

@ -7,16 +7,14 @@ import UserActions from '../../actions/user_actions';
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
import FormPropertyHeader from './form_property_header'; import InputFineUploader from './input_fineuploader';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants'; 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 { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils'; import { mergeOptions } from '../../utils/general_utils';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let RegisterPieceForm = React.createClass({ let RegisterPieceForm = React.createClass({
@ -25,9 +23,13 @@ let RegisterPieceForm = React.createClass({
submitMessage: React.PropTypes.string, submitMessage: React.PropTypes.string,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func,
isFineUploaderActive: React.PropTypes.bool, isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
children: React.PropTypes.element, 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() { getDefaultProps() {
@ -41,7 +43,6 @@ let RegisterPieceForm = React.createClass({
getInitialState(){ getInitialState(){
return mergeOptions( return mergeOptions(
{ {
digitalWorkKey: null,
isUploadReady: false isUploadReady: false
}, },
UserStore.getState() UserStore.getState()
@ -61,67 +62,57 @@ let RegisterPieceForm = React.createClass({
this.setState(state); this.setState(state);
}, },
getFormData(){
return {
digital_work_key: this.state.digitalWorkKey
};
},
submitKey(key){
this.setState({
digitalWorkKey: key
});
},
setIsUploadReady(isReady) { setIsUploadReady(isReady) {
this.setState({ this.setState({
isUploadReady: isReady 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() { render() {
let currentUser = this.state.currentUser; let currentUser = this.state.currentUser;
let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false; let enableLocalHashing = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false;
enableLocalHashing = enableLocalHashing && this.props.enableLocalHashing; enableLocalHashing = enableLocalHashing && this.props.enableLocalHashing;
return ( return (
<Form <Form
disabled={this.props.disabled}
className="ascribe-form-bordered" className="ascribe-form-bordered"
ref='form' ref='form'
url={apiUrls.pieces_list} url={ApiUrls.pieces_list}
getFormData={this.getFormData}
handleSuccess={this.props.handleSuccess} handleSuccess={this.props.handleSuccess}
buttons={<button buttons={
type="submit" <button
className="btn ascribe-btn ascribe-btn-login" type="submit"
disabled={!this.state.isUploadReady}> className="btn ascribe-btn ascribe-btn-login"
{this.props.submitMessage} disabled={!this.state.isUploadReady || this.props.disabled}>
</button>} {this.props.submitMessage}
</button>
}
spinner={ spinner={
<span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner"> <span className="btn ascribe-btn ascribe-btn-login ascribe-btn-login-spinner">
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" /> <img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span> </span>
}> }>
<FormPropertyHeader> <div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3> <h3>{this.props.headerMessage}</h3>
</FormPropertyHeader> </div>
<Property <Property
name="digital_work_key"
ignoreFocus={true}> ignoreFocus={true}>
<FileUploader <InputFineUploader
submitKey={this.submitKey} keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: ApiUrls.blob_digitalworks
}}
validation={AppConstants.fineUploader.validation.registerWork}
setIsUploadReady={this.setIsUploadReady} setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={this.isReadyForFormSubmission} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
isFineUploaderActive={this.props.isFineUploaderActive} isFineUploaderActive={this.props.isFineUploaderActive}
onLoggedOut={this.props.onLoggedOut} onLoggedOut={this.props.onLoggedOut}
editable={this.props.isFineUploaderEditable} disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}/> enableLocalHashing={enableLocalHashing}/>
</Property> </Property>
<Property <Property
@ -146,7 +137,7 @@ let RegisterPieceForm = React.createClass({
<input <input
type="number" type="number"
placeholder="(e.g. 1962)" placeholder="(e.g. 1962)"
min={0} min={1}
required/> required/>
</Property> </Property>
{this.props.children} {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 (
<ReactS3FineUploader
onClick={this.props.onClick}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'digitalwork'
}}
createBlobRoutine={{
url: apiUrls.blob_digitalworks
}}
submitKey={this.props.submitKey}
validation={{
itemLimit: 100000,
sizeLimit: '25000000000'
}}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={false}
areAssetsEditable={this.props.isFineUploaderActive}
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)
}
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing} />
);
}
});
export default RegisterPieceForm; export default RegisterPieceForm;

View File

@ -2,34 +2,63 @@
import React from 'react'; import React from 'react';
import { getLangText } from '../../utils/lang_utils.js'; import Form from './form';
import requests from '../../utils/requests';
import apiUrls from '../../constants/api_urls'; 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 EditionRemoveFromCollectionForm = React.createClass({ let EditionRemoveFromCollectionForm = React.createClass({
propTypes: {
editions: React.PropTypes.arrayOf(React.PropTypes.object),
mixins: [FormMixin], // Propagated by ModalWrapper in most cases
handleSuccess: React.PropTypes.func
url() {
return requests.prepareUrl(apiUrls.edition_remove_from_collection, {edition_id: this.getBitcoinIds().join()});
},
httpVerb(){
return 'delete';
}, },
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 ( return (
<div className="modal-body"> <Form
ref='form'
url={ApiUrls.edition_remove_from_collection}
getFormData={this.getFormData}
method="delete"
handleSuccess={this.props.handleSuccess}
buttons={
<div className="modal-footer">
<p className="pull-right">
<button
type="submit"
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
onClick={this.submit}>
{getLangText('YES, REMOVE')}
</button>
</p>
</div>
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
}>
<p>{getLangText('Are you sure you would like to remove these editions from your collection')}&#63;</p> <p>{getLangText('Are you sure you would like to remove these editions from your collection')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p> <p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer"> </Form>
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, REMOVE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
); );
} }
}); });

View File

@ -2,38 +2,56 @@
import React from 'react'; import React from 'react';
import { getLangText } from '../../utils/lang_utils.js'; import Form from './form';
import requests from '../../utils/requests';
import apiUrls from '../../constants/api_urls'; 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 PieceRemoveFromCollectionForm = React.createClass({ let PieceRemoveFromCollectionForm = React.createClass({
propTypes: { propTypes: {
pieceId: React.PropTypes.number pieceId: React.PropTypes.number,
// Propagated by ModalWrapper in most cases
handleSuccess: React.PropTypes.func
}, },
mixins: [FormMixin], getFormData() {
return {
url() { piece_id: this.props.pieceId
return requests.prepareUrl(apiUrls.piece_remove_from_collection, {piece_id: this.props.pieceId}); };
},
httpVerb(){
return 'delete';
}, },
renderForm () { render () {
return ( return (
<div className="modal-body"> <Form
ref='form'
url={ApiUrls.piece_remove_from_collection}
getFormData={this.getFormData}
method="delete"
handleSuccess={this.props.handleSuccess}
buttons={
<div className="modal-footer">
<p className="pull-right">
<button
type="submit"
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px"
onClick={this.submit}>
{getLangText('YES, REMOVE')}
</button>
</p>
</div>
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
</div>
}>
<p>{getLangText('Are you sure you would like to remove this piece from your collection')}&#63;</p> <p>{getLangText('Are you sure you would like to remove this piece from your collection')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p> <p>{getLangText('This is an irrevocable action%s', '.')}</p>
<div className="modal-footer"> </Form>
<button type="submit" className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" onClick={this.submit}>{getLangText('YES, REMOVE')}</button>
<button className="btn btn-default btn-sm ascribe-margin-1px" style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</button>
</div>
</div>
); );
} }
}); });

View File

@ -2,98 +2,173 @@
import React from 'react'; 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 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'; import { getLangText } from '../../utils/lang_utils.js';
let RequestActionForm = React.createClass({ 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){ isPiece(){
let edition = this.props.editions[0]; return this.props.pieceOrEditions.constructor !== Array;
if (e.target.id === 'request_accept'){ },
if (edition.request_action === 'consign'){
return apiUrls.ownership_consigns_confirm; getUrls() {
} let urls = {};
else if (edition.request_action === 'unconsign'){
return apiUrls.ownership_unconsigns; if (this.props.notifications.action === 'consign'){
} urls.accept = ApiUrls.ownership_consigns_confirm;
else if (edition.request_action === 'loan'){ urls.deny = ApiUrls.ownership_consigns_deny;
return apiUrls.ownership_loans_confirm; } 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 urls;
return apiUrls.ownership_consigns_deny; },
}
else if (edition.request_action === 'unconsign') { getFormData(){
return apiUrls.ownership_unconsigns_deny; if (this.isPiece()) {
} return {piece_id: this.props.pieceOrEditions.id};
else if (edition.request_action === 'loan') { }
return apiUrls.ownership_loans_deny; else {
} return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
} }
}, },
handleRequest: function(e){ showNotification(option, action, owner) {
e.preventDefault(); return () => {
this.submit(e); 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() { handleSuccess() {
let edition = this.props.editions[0]; if (this.isPiece()){
let buttonAccept = ( NotificationActions.fetchPieceListNotifications();
<div id="request_accept" }
onClick={this.handleRequest} else {
className='btn btn-default btn-sm ascribe-margin-1px'>{getLangText('ACCEPT')} NotificationActions.fetchEditionListNotifications();
</div>); }
if (edition.request_action === 'unconsign'){ if(this.props.handleSuccess) {
console.log(this.props) this.props.handleSuccess();
buttonAccept = ( }
},
getContent() {
return (
<span>
{this.props.notifications.action_str + ' by ' + this.props.notifications.by}
</span>
);
},
getAcceptButtonForm(urls) {
if(this.props.notifications.action === 'unconsign') {
return (
<AclButton <AclButton
availableAcls={{'acl_unconsign': true}} availableAcls={{'acl_unconsign': true}}
action="acl_unconsign" action="acl_unconsign"
pieceOrEditions={this.props.editions} buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser} currentUser={this.props.currentUser}
handleSuccess={this.props.handleSuccess} /> handleSuccess={this.handleSuccess} />
); );
} } else if(this.props.notifications.action === 'loan_request') {
let buttons = ( return (
<span> <AclButton
<span> availableAcls={{'acl_loan_request': true}}
{buttonAccept} action="acl_loan_request"
</span> buttonAcceptName="LOAN"
<span> buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
<div id="request_deny" onClick={this.handleRequest} className='btn btn-danger btn-delete btn-sm ascribe-margin-1px'>{getLangText('REJECT')}</div> pieceOrEditions={this.props.pieceOrEditions}
</span> currentUser={this.props.currentUser}
</span> handleSuccess={this.handleSuccess} />
); );
if (this.state.submitted){ } else {
buttons = ( return (
<span> <Form
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} /> url={urls.accept}
</span> getFormData={this.getFormData}
handleSuccess={
this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by)
}
isInline={true}
className='inline pull-right'>
<button
type="submit"
className='btn btn-default btn-sm ascribe-margin-1px'>
{getLangText('ACCEPT')}
</button>
</Form>
); );
} }
},
getButtonForm() {
let urls = this.getUrls();
let acceptButtonForm = this.getAcceptButtonForm(urls);
return ( return (
<Alert bsStyle='warning'> <div>
<div style={{textAlign: 'center'}}> <Form
<div>{ edition.owner } {getLangText('requests you')} { edition.request_action } {getLangText('this edition%s', '.')}&nbsp;&nbsp;</div> url={urls.deny}
{buttons} isInline={true}
</div> getFormData={this.getFormData}
</Alert> handleSuccess={
this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by)
}
className='inline pull-right'>
<button
type="submit"
className='btn btn-danger btn-delete btn-sm ascribe-margin-1px'>
{getLangText('REJECT')}
</button>
</Form>
{acceptButtonForm}
</div>
);
},
render() {
return (
<ActionPanel
content={this.getContent()}
buttons={this.getButtonForm()}/>
); );
} }
}); });

View File

@ -2,14 +2,14 @@
import React from 'react'; import React from 'react';
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable'; import InputTextAreaToggable from './input_textarea_toggable';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
@ -20,7 +20,6 @@ let ShareForm = React.createClass({
message: React.PropTypes.string, message: React.PropTypes.string,
editions: React.PropTypes.array, editions: React.PropTypes.array,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -41,11 +40,9 @@ let ShareForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<Button <Button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">SHARE</Button> type="submit">
<Button SHARE
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" </Button>
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>CLOSE</Button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -63,10 +60,10 @@ let ShareForm = React.createClass({
<Property <Property
name='share_message' name='share_message'
label='Personal Message' label='Personal Message'
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required="required"/>

View File

@ -12,10 +12,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
import FormPropertyHeader from './form_property_header';
import InputCheckbox from './input_checkbox'; import InputCheckbox from './input_checkbox';
import apiUrls from '../../constants/api_urls'; import ApiUrls from '../../constants/api_urls';
let SignupForm = React.createClass({ let SignupForm = React.createClass({
@ -56,10 +55,6 @@ let SignupForm = React.createClass({
} }
}, },
getFormData() {
return this.getQuery();
},
handleSuccess(response){ handleSuccess(response){
if (response.user) { if (response.user) {
let notification = new GlobalNotificationModel(getLangText('Sign up successful'), 'success', 50000); 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() { render() {
let tooltipPassword = getLangText('Your password must be at least 10 characters') + '.\n ' + 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('This password is securing your digital property like a bank account') + '.\n ' +
getLangText('Store it in a safe place') + '!'; getLangText('Store it in a safe place') + '!';
let email = this.getQuery().email ? this.getQuery().email : null; let email = this.getQuery().email || null;
return ( return (
<Form <Form
className="ascribe-form-bordered" className="ascribe-form-bordered"
ref='form' ref='form'
url={apiUrls.users_signup} url={ApiUrls.users_signup}
getFormData={this.getFormData} getFormData={this.getFormData}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
buttons={ buttons={
@ -92,9 +94,9 @@ let SignupForm = React.createClass({
<img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" /> <img src="https://s3-us-west-2.amazonaws.com/ascribe0/media/thumbnails/ascribe_animated_medium.gif" />
</span> </span>
}> }>
<FormPropertyHeader> <div className="ascribe-form-header">
<h3>{this.props.headerMessage}</h3> <h3>{this.props.headerMessage}</h3>
</FormPropertyHeader> </div>
<Property <Property
name='email' name='email'
label={getLangText('Email')}> label={getLangText('Email')}>
@ -132,7 +134,7 @@ let SignupForm = React.createClass({
style={{paddingBottom: 0}}> style={{paddingBottom: 0}}>
<InputCheckbox> <InputCheckbox>
<span> <span>
{' ' + getLangText('I agree to the Terms of Service') + ' '} {' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '}
(<a href="https://www.ascribe.io/terms" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}> (<a href="https://www.ascribe.io/terms" target="_blank" style={{fontSize: '0.9em', color: 'rgba(0,0,0,0.7)'}}>
{getLangText('read')} {getLangText('read')}
</a>) </a>)

View File

@ -19,10 +19,7 @@ import requests from '../../utils/requests';
let PieceSubmitToPrizeForm = React.createClass({ let PieceSubmitToPrizeForm = React.createClass({
propTypes: { propTypes: {
piece: React.PropTypes.object, piece: React.PropTypes.object,
handleSuccess: React.PropTypes.func, handleSuccess: React.PropTypes.func
// this is set by ModalWrapper automatically
onRequestHide: React.PropTypes.func
}, },
render() { render() {
@ -36,7 +33,9 @@ let PieceSubmitToPrizeForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<button <button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('SUBMIT TO PRIZE')}</button> type="submit">
{getLangText('SUBMIT TO PRIZE')}
</button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -46,20 +45,20 @@ let PieceSubmitToPrizeForm = React.createClass({
<Property <Property
name='artist_statement' name='artist_statement'
label={getLangText('Artist statement')} label={getLangText('Artist statement')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
placeholder={getLangText('Enter your statement')} placeholder={getLangText('Enter your statement')}
required="required"/> required="required"/>
</Property> </Property>
<Property <Property
name='work_description' name='work_description'
label={getLangText('Work description')} label={getLangText('Work description')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
placeholder={getLangText('Enter the description for your work')} placeholder={getLangText('Enter the description for your work')}
required="required"/> required="required"/>
</Property> </Property>
@ -80,7 +79,6 @@ let PieceSubmitToPrizeForm = React.createClass({
<p>{getLangText('Are you sure you want to submit to the prize?')}</p> <p>{getLangText('Are you sure you want to submit to the prize?')}</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p> <p>{getLangText('This is an irrevocable action%s', '.')}</p>
</Alert> </Alert>
</Form> </Form>
); );
} }

View File

@ -21,7 +21,6 @@ let TransferForm = React.createClass({
message: React.PropTypes.string, message: React.PropTypes.string,
editions: React.PropTypes.array, editions: React.PropTypes.array,
currentUser: React.PropTypes.object, currentUser: React.PropTypes.object,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -42,11 +41,9 @@ let TransferForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<Button <Button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('TRANSFER')}</Button> type="submit">
<Button {getLangText('TRANSFER')}
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" </Button>
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -64,10 +61,10 @@ let TransferForm = React.createClass({
<Property <Property
name='transfer_message' name='transfer_message'
label={getLangText('Personal Message')} label={getLangText('Personal Message')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required="required"/>

View File

@ -18,7 +18,6 @@ let UnConsignForm = React.createClass({
id: React.PropTypes.object, id: React.PropTypes.object,
message: React.PropTypes.string, message: React.PropTypes.string,
editions: React.PropTypes.array, editions: React.PropTypes.array,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -39,11 +38,9 @@ let UnConsignForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<Button <Button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('UNCONSIGN')}</Button> type="submit">
<Button {getLangText('UNCONSIGN')}
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" </Button>
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -53,10 +50,10 @@ let UnConsignForm = React.createClass({
<Property <Property
name='unconsign_message' name='unconsign_message'
label={getLangText('Personal Message')} label={getLangText('Personal Message')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required="required"/>

View File

@ -3,7 +3,6 @@
import React from 'react'; import React from 'react';
import Button from 'react-bootstrap/lib/Button'; import Button from 'react-bootstrap/lib/Button';
import Alert from 'react-bootstrap/lib/Alert';
import Form from './form'; import Form from './form';
import Property from './property'; import Property from './property';
@ -19,7 +18,6 @@ let UnConsignRequestForm = React.createClass({
url: React.PropTypes.string, url: React.PropTypes.string,
id: React.PropTypes.object, id: React.PropTypes.object,
message: React.PropTypes.string, message: React.PropTypes.string,
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func handleSuccess: React.PropTypes.func
}, },
@ -40,11 +38,9 @@ let UnConsignRequestForm = React.createClass({
<p className="pull-right"> <p className="pull-right">
<Button <Button
className="btn btn-default btn-sm ascribe-margin-1px" className="btn btn-default btn-sm ascribe-margin-1px"
type="submit">{getLangText('REQUEST UNCONSIGN')}</Button> type="submit">
<Button {getLangText('REQUEST UNCONSIGN')}
className="btn btn-danger btn-delete btn-sm ascribe-margin-1px" </Button>
style={{marginLeft: '0'}}
onClick={this.props.onRequestHide}>{getLangText('CLOSE')}</Button>
</p> </p>
</div>} </div>}
spinner={ spinner={
@ -54,10 +50,10 @@ let UnConsignRequestForm = React.createClass({
<Property <Property
name='unconsign_request_message' name='unconsign_request_message'
label={getLangText('Personal Message')} label={getLangText('Personal Message')}
editable={true}> editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={1} rows={1}
editable={true}
defaultValue={this.props.message} defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')} placeholder={getLangText('Enter a message...')}
required="required"/> required="required"/>

View File

@ -21,7 +21,14 @@ let InputCheckbox = React.createClass({
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
]) ]),
// 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 // As HTML inputs, we're setting the default value for an input to checked === false
@ -56,6 +63,12 @@ let InputCheckbox = React.createClass({
}, },
onChange() { 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 // On every change, we're inversing the input's value
let inverseValue = !this.refs.checkbox.getDOMNode().checked; let inverseValue = !this.refs.checkbox.getDOMNode().checked;
@ -74,8 +87,21 @@ let InputCheckbox = React.createClass({
}, },
render() { 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 ( return (
<span <span
style={this.props.style}
onClick={this.onChange}> onClick={this.onChange}>
<input <input
type="checkbox" type="checkbox"
@ -83,7 +109,9 @@ let InputCheckbox = React.createClass({
onChange={this.onChange} onChange={this.onChange}
checked={this.state.value} checked={this.state.value}
defaultChecked={this.props.defaultChecked}/> defaultChecked={this.props.defaultChecked}/>
<span className="checkbox"> <span
className="checkbox"
style={style}>
{this.props.children} {this.props.children}
</span> </span>
</span> </span>

View File

@ -7,15 +7,31 @@ import DatePicker from 'react-datepicker/dist/react-datepicker';
let InputDate = React.createClass({ let InputDate = React.createClass({
propTypes: { propTypes: {
submitted: React.PropTypes.bool, 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() { getInitialState() {
return { 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) { handleChange(date) {
let formattedDate = date.format('YYYY-MM-DD'); let formattedDate = date.format('YYYY-MM-DD');
this.setState({ this.setState({
@ -30,10 +46,15 @@ let InputDate = React.createClass({
}); });
}, },
render: function () { reset() {
this.setState(this.getInitialState());
},
render() {
return ( return (
<div> <div>
<DatePicker <DatePicker
disabled={this.props.disabled}
dateFormat="YYYY-MM-DD" dateFormat="YYYY-MM-DD"
selected={this.state.value_moment} selected={this.state.value_moment}
onChange={this.handleChange} onChange={this.handleChange}

View File

@ -0,0 +1,114 @@
'use strict';
import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils';
let InputFineUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
submitFileName: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
onClick: React.PropTypes.func,
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
fileClass: React.PropTypes.string
}),
createBlobRoutine: React.PropTypes.shape({
url: React.PropTypes.string
}),
validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
}),
// 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,
enableLocalHashing: React.PropTypes.bool,
// provided by Property
disabled: 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 {
value: null
};
},
submitFile(file){
this.setState({
value: file.key
});
if(typeof this.props.submitFileName === 'function') {
this.props.submitFileName(file.originalName);
}
},
reset() {
this.refs.fineuploader.reset();
},
render() {
let editable = this.props.isFineUploaderActive;
// if disabled is actually set by property, we want to override
// isFineUploaderActive
if(typeof this.props.disabled !== 'undefined') {
editable = !this.props.disabled;
}
return (
<ReactS3FineUploader
ref="fineuploader"
onClick={this.props.onClick}
keyRoutine={this.props.keyRoutine}
createBlobRoutine={this.props.createBlobRoutine}
validation={this.props.validation}
submitFile={this.submitFile}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={editable}
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)
}
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload}/>
);
}
});
export default InputFineUploader;

View File

@ -4,10 +4,10 @@ import React from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
let InputTextAreaToggable = React.createClass({
let InputTextAreaToggable = React.createClass({
propTypes: { propTypes: {
editable: React.PropTypes.bool.isRequired, disabled: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired, rows: React.PropTypes.number.isRequired,
required: React.PropTypes.string, required: React.PropTypes.string,
defaultValue: React.PropTypes.string defaultValue: React.PropTypes.string
@ -15,17 +15,36 @@ let InputTextAreaToggable = React.createClass({
getInitialState() { getInitialState() {
return { 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) { handleChange(event) {
this.setState({value: event.target.value}); this.setState({value: event.target.value});
this.props.onChange(event); this.props.onChange(event);
}, },
render() { render() {
let className = 'form-control ascribe-textarea'; let className = 'form-control ascribe-textarea';
let textarea = null; let textarea = null;
if (this.props.editable){
if(!this.props.disabled) {
className = className + ' ascribe-textarea-editable'; className = className + ' ascribe-textarea-editable';
textarea = ( textarea = (
<TextareaAutosize <TextareaAutosize
@ -38,10 +57,10 @@ let InputTextAreaToggable = React.createClass({
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
placeholder={this.props.placeholder} /> placeholder={this.props.placeholder} />
); );
} } else {
else{
textarea = <pre className="ascribe-pre">{this.state.value}</pre>; textarea = <pre className="ascribe-pre">{this.state.value}</pre>;
} }
return textarea; return textarea;
} }
}); });

View File

@ -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 (
<div>
{this.props.notifications.map((notification) =>
<RequestActionForm
currentUser={this.props.currentUser}
pieceOrEditions={ this.props.pieceOrEditions }
notifications={notification}
handleSuccess={this.props.handleSuccess}/>)}
</div>
);
}
return null;
}
});
export default ListRequestActions;

View File

@ -6,10 +6,22 @@ import ReactAddons from 'react/addons';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
let Property = React.createClass({ let Property = React.createClass({
propTypes: { propTypes: {
hidden: React.PropTypes.bool, hidden: React.PropTypes.bool,
editable: 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, tooltip: React.PropTypes.element,
label: React.PropTypes.string, label: React.PropTypes.string,
value: React.PropTypes.oneOfType([ value: React.PropTypes.oneOfType([
@ -20,8 +32,11 @@ let Property = React.createClass({
handleChange: React.PropTypes.func, handleChange: React.PropTypes.func,
ignoreFocus: React.PropTypes.bool, ignoreFocus: React.PropTypes.bool,
className: React.PropTypes.string, className: React.PropTypes.string,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
onBlur: React.PropTypes.func,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
@ -55,7 +70,7 @@ let Property = React.createClass({
// In order to set this.state.value from another component // In order to set this.state.value from another component
// the state of value should only be set if its not undefined and // the state of value should only be set if its not undefined and
// actually references something // actually references something
if(typeof childInput.getDOMNode().value !== 'undefined') { if(childInput && typeof childInput.getDOMNode().value !== 'undefined') {
this.setState({ this.setState({
value: childInput.getDOMNode().value value: childInput.getDOMNode().value
}); });
@ -78,21 +93,41 @@ let Property = React.createClass({
}, },
reset() { reset() {
let input = this.refs.input;
// maybe do reset by reload instead of front end state? // maybe do reset by reload instead of front end state?
this.setState({value: this.state.initialValue}); this.setState({value: this.state.initialValue});
// resets the value of a custom react component input if(input.state && input.state.value) {
this.refs.input.state.value = this.state.initialValue; // resets the value of a custom react component input
input.state.value = this.state.initialValue;
}
// resets the value of a plain HTML5 input // For some reason, if we set the value of a non HTML element (but a custom input),
this.refs.input.getDOMNode().value = this.state.initialValue; // 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) { handleChange(event) {
this.props.handleChange(event); this.props.handleChange(event);
if ('onChange' in this.props) { if (typeof this.props.onChange === 'function') {
this.props.onChange(event); this.props.onChange(event);
} }
@ -108,7 +143,7 @@ let Property = React.createClass({
// if onClick is defined from the outside, // if onClick is defined from the outside,
// just call it // just call it
if(this.props.onClick) { if(typeof this.props.onClick === 'function') {
this.props.onClick(); this.props.onClick();
} }
@ -123,7 +158,7 @@ let Property = React.createClass({
isFocused: false isFocused: false
}); });
if(this.props.onBlur) { if(typeof this.props.onBlur === 'function') {
this.props.onBlur(event); 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.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
style,
onChange: this.handleChange, onChange: this.handleChange,
onFocus: this.handleFocus, onFocus: this.handleFocus,
onBlur: this.handleBlur, onBlur: this.handleBlur,
@ -180,34 +216,42 @@ let Property = React.createClass({
}, },
render() { render() {
let footer = null;
let tooltip = <span/>; let tooltip = <span/>;
if (this.props.tooltip){ let style = this.props.style ? mergeOptions({}, this.props.style) : {};
if(this.props.tooltip){
tooltip = ( tooltip = (
<Tooltip> <Tooltip>
{this.props.tooltip} {this.props.tooltip}
</Tooltip>); </Tooltip>);
} }
let footer = null;
if (this.props.footer){ if(this.props.footer){
footer = ( footer = (
<div className="ascribe-property-footer"> <div className="ascribe-property-footer">
{this.props.footer} {this.props.footer}
</div>); </div>);
} }
if(!this.props.editable) {
style.cursor = 'not-allowed';
}
return ( return (
<div <div
className={'ascribe-settings-wrapper ' + this.getClassName()} className={'ascribe-settings-wrapper ' + this.getClassName()}
onClick={this.handleFocus} onClick={this.handleFocus}
onFocus={this.handleFocus} onFocus={this.handleFocus}
style={this.props.style}> style={style}>
<OverlayTrigger <OverlayTrigger
delay={500} delay={500}
placement="top" placement="top"
overlay={tooltip}> overlay={tooltip}>
<div className={'ascribe-settings-property ' + this.props.className}> <div className={'ascribe-settings-property ' + this.props.className}>
{this.state.errors} {this.state.errors}
<span>{ this.props.label}</span> <span>{this.props.label}</span>
{this.renderChildren()} {this.renderChildren(style)}
{footer} {footer}
</div> </div>
</OverlayTrigger> </OverlayTrigger>

View File

@ -3,11 +3,9 @@
import React from 'react'; import React from 'react';
import ReactAddons from 'react/addons'; import ReactAddons from 'react/addons';
import CollapsibleMixin from 'react-bootstrap/lib/CollapsibleMixin';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
import Panel from 'react-bootstrap/lib/Panel';
import classNames from 'classnames';
let PropertyCollapsile = React.createClass({ let PropertyCollapsile = React.createClass({
@ -17,22 +15,12 @@ let PropertyCollapsile = React.createClass({
tooltip: React.PropTypes.string tooltip: React.PropTypes.string
}, },
mixins: [CollapsibleMixin],
getInitialState() { getInitialState() {
return { return {
show: false show: false
}; };
}, },
getCollapsibleDOMNode(){
return React.findDOMNode(this.refs.panel);
},
getCollapsibleDimensionValue(){
return React.findDOMNode(this.refs.panel).scrollHeight;
},
handleFocus() { handleFocus() {
this.refs.checkboxCollapsible.getDOMNode().checked = !this.refs.checkboxCollapsible.getDOMNode().checked; this.refs.checkboxCollapsible.getDOMNode().checked = !this.refs.checkboxCollapsible.getDOMNode().checked;
this.setState({ 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() { render() {
let tooltip = <span/>; let tooltip = <span/>;
if (this.props.tooltip){ if (this.props.tooltip){
@ -85,11 +80,14 @@ let PropertyCollapsile = React.createClass({
<span className="checkbox"> {this.props.checkboxLabel}</span> <span className="checkbox"> {this.props.checkboxLabel}</span>
</div> </div>
</OverlayTrigger> </OverlayTrigger>
<div <Panel
className={classNames(this.getCollapsibleClassSet()) + ' ascribe-settings-property'} collapsible
ref="panel"> expanded={this.state.show}
className="bs-custom-panel">
<div className="ascribe-settings-property">
{this.renderChildren()} {this.renderChildren()}
</div> </div>
</Panel>
</div> </div>
); );
} }

View File

@ -28,12 +28,20 @@ let Other = React.createClass({
}, },
render() { 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 ( return (
<Panel className="media-other"> <Panel className="media-other">
<p className="text-center"> <p className="text-center">
.{ext} {preview}
</p> </p>
</Panel> </Panel>
); );
@ -200,7 +208,8 @@ let MediaPlayer = React.createClass({
<br />You can leave this page and check back on the status later.</em> <br />You can leave this page and check back on the status later.</em>
</p> </p>
<ProgressBar now={this.props.encodingStatus} <ProgressBar now={this.props.encodingStatus}
label='%(percent)s%' /> label="%(percent)s%"
className="ascribe-progress-bar" />
</div> </div>
); );
} else { } else {

View File

@ -7,9 +7,13 @@ import PasswordResetRequestForm from '../ascribe_forms/form_password_reset_reque
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getLangText } from '../../utils/lang_utils.js' import { getLangText } from '../../utils/lang_utils.js';
let PasswordResetRequestModal = React.createClass({ let PasswordResetRequestModal = React.createClass({
propTypes: {
button: React.PropTypes.element
},
handleResetSuccess(){ handleResetSuccess(){
let notificationText = getLangText('Request successfully sent, check your email'); let notificationText = getLangText('Request successfully sent, check your email');
let notification = new GlobalNotificationModel(notificationText, 'success', 50000); let notification = new GlobalNotificationModel(notificationText, 'success', 50000);
@ -18,10 +22,9 @@ let PasswordResetRequestModal = React.createClass({
render() { render() {
return ( return (
<ModalWrapper <ModalWrapper
button={this.props.button} trigger={this.props.button}
title={getLangText('Reset your password')} title={getLangText('Reset your password')}
handleSuccess={this.handleResetSuccess} handleSuccess={this.handleResetSuccess}>
tooltip={getLangText('Reset your password')}>
<PasswordResetRequestForm /> <PasswordResetRequestForm />
</ModalWrapper> </ModalWrapper>
); );

View File

@ -4,92 +4,74 @@ import React from 'react';
import ReactAddons from 'react/addons'; import ReactAddons from 'react/addons';
import Modal from 'react-bootstrap/lib/Modal'; 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({ let ModalWrapper = React.createClass({
propTypes: { propTypes: {
title: React.PropTypes.string.isRequired, trigger: React.PropTypes.element.isRequired,
onRequestHide: React.PropTypes.func, title: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element,
React.PropTypes.string
]).isRequired,
handleSuccess: React.PropTypes.func.isRequired, handleSuccess: React.PropTypes.func.isRequired,
button: React.PropTypes.object.isRequired, children: React.PropTypes.oneOfType([
children: React.PropTypes.object, React.PropTypes.arrayOf(React.PropTypes.element),
tooltip: React.PropTypes.string React.PropTypes.element
])
}, },
getModalTrigger() { getInitialState() {
return ( return {
<ModalTrigger modal={ showModal: false
<ModalBody };
title={this.props.title}
handleSuccess={this.props.handleSuccess}>
{this.props.children}
</ModalBody>
}>
{this.props.button}
</ModalTrigger>
);
}, },
render() { show() {
if(this.props.tooltip) { this.setState({
return ( showModal: true
<OverlayTrigger });
delay={500}
placement="left"
overlay={<Tooltip>{this.props.tooltip}</Tooltip>}>
{this.getModalTrigger()}
</OverlayTrigger>
);
} else {
return (
<span>
{/* This needs to be some kind of inline-block */}
{this.getModalTrigger()}
</span>
);
}
}
});
let ModalBody = React.createClass({
propTypes: {
onRequestHide: React.PropTypes.func,
handleSuccess: React.PropTypes.func,
children: React.PropTypes.object,
title: React.PropTypes.string.isRequired
}, },
mixins: [ModalMixin], hide() {
this.setState({
showModal: false
});
},
handleSuccess(response){ handleSuccess(response){
this.props.handleSuccess(response); this.props.handleSuccess(response);
this.props.onRequestHide(); this.hide();
}, },
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => { return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.addons.cloneWithProps(child, { return ReactAddons.addons.cloneWithProps(child, {
onRequestHide: this.props.onRequestHide,
handleSuccess: this.handleSuccess handleSuccess: this.handleSuccess
}); });
}); });
}, },
render() { 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 ( return (
<Modal {...this.props} title={this.props.title}> <span>
<div className="modal-body"> {trigger}
{this.renderChildren()} <Modal show={this.state.showModal} onHide={this.hide}>
</div> <Modal.Header closeButton>
</Modal> <Modal.Title>
{this.props.title}
</Modal.Title>
</Modal.Header>
<div className="modal-body">
{this.renderChildren()}
</div>
</Modal>
</span>
); );
} }
}); });
export default ModalWrapper; export default ModalWrapper;

View File

@ -1,15 +1,21 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import classnames from 'classnames';
let ActionPanel = React.createClass({ let ActionPanel = React.createClass({
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string,
content: React.PropTypes.string, content: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element
]),
buttons: React.PropTypes.element, buttons: React.PropTypes.element,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
ignoreFocus: React.PropTypes.bool ignoreFocus: React.PropTypes.bool,
leftColumnWidth: React.PropTypes.string,
rightColumnWidth: React.PropTypes.string
}, },
getInitialState() { getInitialState() {
@ -37,31 +43,25 @@ let ActionPanel = React.createClass({
}); });
}, },
getClassName() {
if(this.state.isFocused) {
return 'is-focused';
} else {
return '';
}
},
render() { render() {
let { leftColumnWidth, rightColumnWidth } = this.props;
return ( return (
<div <div className={classnames('ascribe-panel-wrapper', {'is-focused': this.state.isFocused})}>
className={'ascribe-panel-wrapper ' + this.getClassName()} <div
onClick={this.handleFocus} className="ascribe-panel-table"
onFocus={this.handleFocus}> style={{width: leftColumnWidth}}>
<div className='ascribe-panel-title'> <div className="ascribe-panel-content">
{this.props.title}
</div>
<div className='ascribe-panel-content-wrapper'>
<span className="ascribe-panel-content pull-left">
{this.props.content} {this.props.content}
</span> </div>
<span className='ascribe-panel-buttons pull-right'> </div>
<div
className="ascribe-panel-table"
style={{width: rightColumnWidth}}>
<div className="ascribe-panel-content">
{this.props.buttons} {this.props.buttons}
</span> </div>
</div> </div>
</div> </div>
); );

View File

@ -81,7 +81,7 @@ let PieceListBulkModal = React.createClass({
this.fetchSelectedPieceEditionList() this.fetchSelectedPieceEditionList()
.forEach((pieceId) => { .forEach((pieceId) => {
EditionListActions.refreshEditionList(pieceId); EditionListActions.refreshEditionList({pieceId, filterBy: {}});
}); });
EditionListActions.clearAllEditionSelections(); EditionListActions.clearAllEditionSelections();
}, },

View File

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget'; import PieceListToolbarFilterWidget from './piece_list_toolbar_filter_widget';
import PieceListToolbarOrderWidget from './piece_list_toolbar_order_widget';
import Input from 'react-bootstrap/lib/Input'; import Input from 'react-bootstrap/lib/Input';
import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import Glyphicon from 'react-bootstrap/lib/Glyphicon';
@ -13,8 +14,25 @@ let PieceListToolbar = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string, className: React.PropTypes.string,
searchFor: React.PropTypes.func, 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, filterBy: React.PropTypes.object,
applyFilterBy: React.PropTypes.func, applyFilterBy: React.PropTypes.func,
orderParams: React.PropTypes.array,
orderBy: React.PropTypes.string,
applyOrderBy: React.PropTypes.func,
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element React.PropTypes.element
@ -26,6 +44,29 @@ let PieceListToolbar = React.createClass({
this.props.searchFor(searchTerm); this.props.searchFor(searchTerm);
}, },
getFilterWidget(){
if (this.props.filterParams){
return (
<PieceListToolbarFilterWidget
filterParams={this.props.filterParams}
filterBy={this.props.filterBy}
applyFilterBy={this.props.applyFilterBy} />
);
}
return null;
},
getOrderWidget(){
if (this.props.orderParams){
return (
<PieceListToolbarOrderWidget
orderParams={this.props.orderParams}
orderBy={this.props.orderBy}
applyOrderBy={this.props.applyOrderBy}/>
);
}
return null;
},
render() { render() {
let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>; let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>;
@ -37,7 +78,7 @@ let PieceListToolbar = React.createClass({
<span className="pull-left"> <span className="pull-left">
{this.props.children} {this.props.children}
</span> </span>
<span className="pull-right search-bar"> <span className="pull-right search-bar ascribe-input-glyph">
<Input <Input
type='text' type='text'
ref="search" ref="search"
@ -46,13 +87,8 @@ let PieceListToolbar = React.createClass({
addonAfter={searchIcon} /> addonAfter={searchIcon} />
</span> </span>
<span className="pull-right"> <span className="pull-right">
<PieceListToolbarFilterWidget {this.getOrderWidget()}
filterParams={['acl_transfer', 'acl_consign', { {this.getFilterWidget()}
key: 'acl_create_editions',
label: 'create editions'
}]}
filterBy={this.props.filterBy}
applyFilterBy={this.props.applyFilterBy}/>
</span> </span>
</div> </div>
</div> </div>

View File

@ -3,20 +3,26 @@
import React from 'react'; import React from 'react';
import DropdownButton from 'react-bootstrap/lib/DropdownButton'; import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../utils/lang_utils.js';
let PieceListToolbarFilterWidgetFilter = React.createClass({ let PieceListToolbarFilterWidgetFilter = React.createClass({
propTypes: { propTypes: {
// An array of either strings (which represent acl enums) or objects of the form filterParams: React.PropTypes.arrayOf(
// React.PropTypes.shape({
// { label: React.PropTypes.string,
// key: <acl enum>, items: React.PropTypes.arrayOf(
// label: <a human readable string> React.PropTypes.oneOfType([
// } React.PropTypes.string,
// React.PropTypes.shape({
filterParams: React.PropTypes.arrayOf(React.PropTypes.any).isRequired, key: React.PropTypes.string,
label: React.PropTypes.string
})
])
)
})
).isRequired,
filterBy: React.PropTypes.object, filterBy: React.PropTypes.object,
applyFilterBy: React.PropTypes.func applyFilterBy: React.PropTypes.func
}, },
@ -79,35 +85,53 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({
<DropdownButton <DropdownButton
title={filterIcon} title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget"> className="ascribe-piece-list-toolbar-filter-widget">
<li style={{'textAlign': 'center'}}> {/* We iterate over filterParams, to receive the label and then for each
<em>{getLangText('Show works that')}:</em> label also iterate over its items, to get all filterable options */}
</li> {this.props.filterParams.map(({ label, items }, i) => {
{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];
}
return ( return (
<MenuItem <div>
key={i} <li
onClick={this.filterBy(param)} style={{'textAlign': 'center'}}
className="filter-widget-item"> key={i}>
<div className="checkbox-line"> <em>{label}:</em>
<span> </li>
{getLangText('I can') + ' ' + getLangText(label)} {items.map((param, j) => {
</span>
<input // As can be seen in the PropTypes, a param can either
readOnly // be a string or an object of the shape:
type="checkbox" //
checked={this.props.filterBy[param]} /> // {
</div> // key: <String>,
</MenuItem> // label: <String>
// }
//
// 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 (
<li
key={j}
onClick={this.filterBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(label)}
</span>
<input
readOnly
type="checkbox"
checked={this.props.filterBy[param]} />
</div>
</li>
);
})}
</div>
); );
})} })}
</DropdownButton> </DropdownButton>

View File

@ -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: <acl enum>,
// label: <a human readable string>
// }
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 = (
<span>
<span className="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span>
<span style={this.isOrderActive()}>*</span>
</span>
);
return (
<DropdownButton
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">
<li style={{'textAlign': 'center'}}>
<em>{getLangText('Sort by')}:</em>
</li>
{this.props.orderParams.map((param) => {
return (
<div>
<li
key={param}
onClick={this.orderBy(param)}
className="filter-widget-item">
<div className="checkbox-line">
<span>
{getLangText(param.replace('_', ' '))}
</span>
<input
readOnly
type="checkbox"
checked={param.indexOf(this.props.orderBy) > -1} />
</div>
</li>
</div>
);
})}
</DropdownButton>
);
}
});
export default PieceListToolbarOrderWidget;

View File

@ -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 (
<Table
responsive
className="ascribe-table"
columnList={this.getColumnList()}
itemList={this.state.prizeList}>
{this.state.prizeList.map((item, i) => {
return (
<TableItem
className="ascribe-table-item-selectable"
key={i}/>
);
})}
</Table>
);
}
});
export default PrizesDashboard;

View File

@ -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 = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let profile = null;
if (this.props.currentUser.username) {
content = (
<Form
url={ApiUrls.users_username}
handleSuccess={this.handleSuccess}>
<Property
name='username'
label={getLangText('Username')}>
<input
type="text"
defaultValue={this.props.currentUser.username}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<Property
name='email'
label={getLangText('Email')}
overrideForm={true}
editable={false}>
<input
type="text"
defaultValue={this.props.currentUser.email}
placeholder={getLangText('Enter your username')}
required/>
</Property>
<hr />
</Form>
);
profile = (
<AclProxy
aclObject={this.props.whitelabel}
aclName="acl_view_settings_account_hash">
<Form
url={ApiUrls.users_profile}
handleSuccess={this.handleSuccess}
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.props.currentUser.profile.hash_locally}>
<span>
{' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')}
</span>
</InputCheckbox>
</Property>
</Form>
</AclProxy>
);
}
return (
<CollapsibleParagraph
title={getLangText('Account')}
defaultExpanded={true}>
{content}
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
{profile}
</CollapsibleParagraph>
);
}
});
export default AccountSettings;

View File

@ -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 = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app, i) {
return (
<ActionPanel
name={app.name}
key={i}
content={
<div>
<div className='ascribe-panel-title'>
{app.name}
</div>
<div className="ascribe-panel-subtitle">
{'Bearer ' + app.bearer_token.token}
</div>
</div>
}
buttons={
<div className="pull-right">
<div className="pull-right">
<button
className="pull-right btn btn-default btn-sm"
onClick={this.handleTokenRefresh}
data-id={app.name}>
{getLangText('REFRESH')}
</button>
</div>
</div>
}/>
);
}, this);
}
return content;
},
render() {
return (
<CollapsibleParagraph
title={getLangText('API Integration')}
defaultExpanded={this.props.defaultExpanded}>
<Form
url={ApiUrls.applications}
handleSuccess={this.handleCreateSuccess}>
<Property
name='name'
label={getLangText('Application Name')}>
<input
type="text"
placeholder={getLangText('Enter the name of your app')}
required/>
</Property>
<hr />
</Form>
<pre>
Usage: curl &lt;url&gt; -H 'Authorization: Bearer &lt;token&gt;'
</pre>
{this.getApplications()}
</CollapsibleParagraph>
);
}
});
export default APISettings;

View File

@ -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 = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
if (this.state.walletSettings.btc_public_key) {
content = (
<Form >
<Property
name='btc_public_key'
label={getLangText('Bitcoin public key')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_public_key}</pre>
</Property>
<Property
name='btc_root_address'
label={getLangText('Root Address')}
editable={false}>
<pre className="ascribe-pre">{this.state.walletSettings.btc_root_address}</pre>
</Property>
<hr />
</Form>);
}
return (
<CollapsibleParagraph
title={getLangText('Crypto Wallet')}
defaultExpanded={this.props.defaultExpanded}>
{content}
</CollapsibleParagraph>
);
}
});
export default BitcoinWalletSettings;

View File

@ -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 = (
<CreateContractForm
isPublic={true}
fileClassToUpload={{
singular: 'new contract',
plural: 'new contracts'
}}/>
);
}
return (
<div className="settings-container">
<CollapsibleParagraph
title={getLangText('Contracts')}
defaultExpanded={true}>
<AclProxy
aclName="acl_edit_public_contract"
aclObject={this.state.currentUser.acl}>
<div>
{createPublicContractForm}
{publicContracts.map((contract, i) => {
return (
<ActionPanel
key={i}
title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
buttons={
<div className="pull-right">
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_public_contract">
<ContractSettingsUpdateButton contract={contract}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
href={contract.blob.url_safe}
target="_blank">
{getLangText('PREVIEW')}
</a>
<button
className="btn btn-danger btn-sm margin-left-2px"
onClick={this.removeContract(contract)}>
{getLangText('REMOVE')}
</button>
</div>
}
leftColumnWidth="40%"
rightColumnWidth="60%"/>
);
})}
</div>
</AclProxy>
<AclProxy
aclName="acl_edit_private_contract"
aclObject={this.state.currentUser.acl}>
<div>
<CreateContractForm
isPublic={false}
fileClassToUpload={{
singular: getLangText('new contract'),
plural: getLangText('new contracts')
}}/>
{privateContracts.map((contract, i) => {
return (
<ActionPanel
key={i}
title={contract.name}
content={truncateTextAtCharIndex(contract.name, 120, '(...).pdf')}
buttons={
<div className="pull-right">
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_update_private_contract">
<ContractSettingsUpdateButton contract={contract}/>
</AclProxy>
<a
className="btn btn-default btn-sm margin-left-2px"
href={contract.blob.url_safe}
target="_blank">
{getLangText('PREVIEW')}
</a>
<button
className="btn btn-danger btn-sm margin-left-2px"
onClick={this.removeContract(contract)}>
{getLangText('REMOVE')}
</button>
</div>
}
leftColumnWidth="60%"
rightColumnWidth="40%"/>
);
})}
</div>
</AclProxy>
</CollapsibleParagraph>
</div>
);
}
});
export default ContractSettings;

View File

@ -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 (
<ReactS3FineUploader
ref="fineuploader"
fileInputElement={UploadButton}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
fileClass: 'contract'
}}
createBlobRoutine={{
url: ApiUrls.blob_contracts
}}
validation={{
itemLimit: AppConstants.fineUploader.validation.registerWork.itemLimit,
sizeLimit: AppConstants.fineUploader.validation.additionalData.sizeLimit,
allowedExtensions: ['pdf']
}}
setIsUploadReady={() =>{/* 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;

View File

@ -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 (
<div className="settings-container">
<AccountSettings
currentUser={this.state.currentUser}
loadUser={this.loadUser}
whitelabel={this.state.whitelabel}/>
{this.props.children}
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_api">
<APISettings />
</AclProxy>
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_bitcoin">
<BitcoinWalletSettings />
</AclProxy>
</div>
);
}
return null;
}
});
export default SettingsContainer;

View File

@ -4,12 +4,21 @@ import React from 'react';
import Router from 'react-router'; import Router from 'react-router';
import ReactAddons from 'react/addons'; import ReactAddons from 'react/addons';
import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs';
let State = Router.State; let State = Router.State;
let Navigation = Router.Navigation; let Navigation = Router.Navigation;
let SlidesContainer = React.createClass({ let SlidesContainer = React.createClass({
propTypes: { 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], mixins: [State, Navigation],
@ -18,15 +27,25 @@ let SlidesContainer = React.createClass({
// handle queryParameters // handle queryParameters
let queryParams = this.getQuery(); let queryParams = this.getQuery();
let slideNum = -1; 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) { if(queryParams && 'slide_num' in queryParams) {
slideNum = parseInt(queryParams.slide_num, 10); slideNum = parseInt(queryParams.slide_num, 10);
} }
// if slide_num is not set, this will be done in componentDidMount // 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 { return {
slideNum,
startFrom,
containerWidth: 0, containerWidth: 0,
slideNum: slideNum,
historyLength: window.history.length historyLength: window.history.length
}; };
}, },
@ -34,6 +53,9 @@ let SlidesContainer = React.createClass({
componentDidMount() { componentDidMount() {
// check if slide_num was defined, and if not then default to 0 // check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery(); 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)) { if(!('slide_num' in queryParams)) {
// we're first requiring all the other possible queryParams and then set // 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); window.addEventListener('resize', this.handleContainerResize);
}, },
componentDidUpdate() { componentWillReceiveProps() {
// check if slide_num was defined, and if not then default to 0
let queryParams = this.getQuery(); 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); 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, // 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. // though only if the slideNum is actually in the range of our children-list.
setSlideNum(slideNum) { 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 // then we want to "replace" (in this case append) the current url with ?slide_num=0
if(isNaN(slideNum) && this.state.slideNum === -1) { if(isNaN(slideNum) && this.state.slideNum === -1) {
slideNum = 0; slideNum = 0;
queryParams.slide_num = slideNum; queryParams.slide_num = slideNum;
this.replaceWith(this.getPathname(), null, queryParams); 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 // if slideNum is within the range of slides and none of the previous cases
// where matched, we can actually do transitions // 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) { if(slideNum !== this.state.slideNum - 1) {
// Bootstrapping the component, getInitialState is called once to save // 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). // we push a new state on it ONCE (ever).
// Otherwise, we're able to use the browsers history.forward() method // Otherwise, we're able to use the browsers history.forward() method
// to keep the stack clean // to keep the stack clean
if(this.state.historyLength === window.history.length) {
if(this.props.forwardProcess) {
queryParams.slide_num = slideNum; queryParams.slide_num = slideNum;
this.transitionTo(this.getPathname(), null, queryParams); this.transitionTo(this.getPathname(), null, queryParams);
} else { } 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 (
<SlidesContainerBreadcrumbs
breadcrumbs={breadcrumbs}
slideNum={this.state.slideNum}
numOfSlides={breadcrumbs.length}
containerWidth={this.state.containerWidth}
glyphiconClassNames={this.props.glyphiconClassNames}/>
);
} else {
return null;
}
},
// Since we need to give the slides a width, we need to call ReactAddons.addons.cloneWithProps // Since we need to give the slides a width, we need to call ReactAddons.addons.cloneWithProps
// Also, a key is nice to have! // Also, a key is nice to have!
renderChildren() { renderChildren() {
return ReactAddons.Children.map(this.props.children, (child, i) => { return ReactAddons.Children.map(this.props.children, (child, i) => {
return ReactAddons.addons.cloneWithProps(child, {
className: 'ascribe-slide', // since the default parameter of startFrom is -1, we do not need to check
style: { // if its actually present in the url bar, as it will just not match
width: this.state.containerWidth if(child && i >= this.state.startFrom) {
}, return ReactAddons.addons.cloneWithProps(child, {
key: i className: 'ascribe-slide',
}); style: {
width: this.state.containerWidth
},
key: i
});
} else {
// Abortions are bad mkay
return null;
}
}); });
}, },
render() { 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 ( return (
<div <div
className="container ascribe-sliding-container-wrapper" className="container ascribe-sliding-container-wrapper"
ref="containerWrapper"> ref="containerWrapper">
{this.renderBreadcrumbs()}
<div <div
className="container ascribe-sliding-container" className="container ascribe-sliding-container"
style={{ style={{
width: this.state.containerWidth * React.Children.count(this.props.children), width: this.state.containerWidth * this.customChildrenCount(),
transform: 'translateX(' + (-1) * this.state.containerWidth * this.state.slideNum + 'px)' transform: translateXValue,
WebkitTransform: translateXValue,
MozTransform: translateXValue,
OTransform: translateXValue,
mstransform: translateXValue
}}> }}>
<div className="row"> <div className="row">
{this.renderChildren()} {this.renderChildren()}
</div> </div>
</div> </div>
</div> </div>
); );

View File

@ -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 (
<div className="row" style={{width: this.props.containerWidth}}>
<div className="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1 col-xs-12">
<div className="no-margin row ascribe-breadcrumb-container">
{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 (
<Col
className="no-padding"
sm={columnWidth}
key={i}>
<div className="ascribe-breadcrumb">
<a className={classnames({'active': this.props.slideNum === i})}>
{breadcrumb}
<span
className={classnames({
'invisible': i === numSlides - 1,
'pull-right': true,
[glyphiconClassName]: true
})}>
</span>
</a>
</div>
</Col>
);
})}
</div>
</div>
</div>
);
}
});
export default SlidesContainerBreadcrumbs;

View File

@ -6,15 +6,15 @@ import React from 'react';
let TableItemAclFiltered = React.createClass({ let TableItemAclFiltered = React.createClass({
propTypes: { propTypes: {
content: React.PropTypes.object, content: React.PropTypes.object,
requestAction: React.PropTypes.string notifications: React.PropTypes.string
}, },
render() { render() {
var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; 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 ( return (
<span> <span>
{this.props.requestAction + ' request pending'} {this.props.notifications[0].action_str}
</span> </span>
); );
} }

View File

@ -1,25 +1,19 @@
'use strict'; 'use strict';
import React from 'react'; 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 FileDragAndDropDialog from './file_drag_and_drop_dialog';
import FileDragAndDropPreviewIterator from './file_drag_and_drop_preview_iterator'; 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 // Taken from: https://github.com/fedosejev/react-file-drag-and-drop
let FileDragAndDrop = React.createClass({ let FileDragAndDrop = React.createClass({
propTypes: { propTypes: {
className: React.PropTypes.string,
onDragStart: React.PropTypes.func,
onDrop: React.PropTypes.func.isRequired, 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, onDragOver: React.PropTypes.func,
onDragEnd: React.PropTypes.func,
onInactive: React.PropTypes.func, onInactive: React.PropTypes.func,
filesToUpload: React.PropTypes.array, filesToUpload: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func, handleDeleteFile: React.PropTypes.func,
@ -37,37 +31,16 @@ let FileDragAndDrop = React.createClass({
hashingProgress: React.PropTypes.number, hashingProgress: React.PropTypes.number,
// sets the value of this.state.hashingProgress in reactfineuploader // sets the value of this.state.hashingProgress in reactfineuploader
// to -1 which is code for: aborted // to -1 which is code for: aborted
handleCancelHashing: React.PropTypes.func handleCancelHashing: React.PropTypes.func,
},
handleDragStart(event) { // A class of a file the user has to upload
if (typeof this.props.onDragStart === 'function') { // Needs to be defined both in singular as well as in plural
this.props.onDragStart(event); fileClassToUpload: React.PropTypes.shape({
} singular: React.PropTypes.string,
}, plural: React.PropTypes.string
}),
handleDrag(event) { allowedExtensions: React.PropTypes.string
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);
}
}, },
handleDragOver(event) { handleDragOver(event) {
@ -159,50 +132,64 @@ let FileDragAndDrop = React.createClass({
}, },
render: function () { 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 // 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 hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
let className = hasFiles ? 'has-files ' : ''; let updatedClassName = hasFiles ? 'has-files ' : '';
className += this.props.dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone'; updatedClassName += dropzoneInactive ? 'inactive-dropzone' : 'active-dropzone';
className += this.props.className ? ' ' + this.props.className : ''; updatedClassName += ' file-drag-and-drop';
// if !== -2: triggers a FileDragAndDrop-global spinner // if !== -2: triggers a FileDragAndDrop-global spinner
if(this.props.hashingProgress !== -2) { if(hashingProgress !== -2) {
return ( return (
<div className={className}> <div className={className}>
<p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p> <div className="file-drag-and-drop-hashing-dialog">
<p> <p>{getLangText('Computing hash(es)... This may take a few minutes.')}</p>
<span>{Math.ceil(this.props.hashingProgress)}%</span> <p>
<a onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</a> <a onClick={this.props.handleCancelHashing}> {getLangText('Cancel hashing')}</a>
</p> </p>
<ProgressBar completed={this.props.hashingProgress} color="#48DACB"/> <ProgressBar
now={Math.ceil(this.props.hashingProgress)}
label="%(percent)s%"
className="ascribe-progress-bar"/>
</div>
</div> </div>
); );
} else { } else {
return ( return (
<div <div
className={className} className={updatedClassName}
onDragStart={this.handleDragStart}
onDrag={this.handleDrop} onDrag={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver} onDragOver={this.handleDragOver}
onDrop={this.handleDrop} onDrop={this.handleDrop}>
onDragEnd={this.handleDragEnd}>
<FileDragAndDropDialog <FileDragAndDropDialog
multipleFiles={this.props.multiple} multipleFiles={multiple}
hasFiles={hasFiles} hasFiles={hasFiles}
onClick={this.handleOnClick} onClick={this.handleOnClick}
enableLocalHashing={this.props.enableLocalHashing}/> enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload}/>
<FileDragAndDropPreviewIterator <FileDragAndDropPreviewIterator
files={this.props.filesToUpload} files={filesToUpload}
handleDeleteFile={this.handleDeleteFile} handleDeleteFile={this.handleDeleteFile}
handleCancelFile={this.handleCancelFile} handleCancelFile={this.handleCancelFile}
handlePauseFile={this.handlePauseFile} handlePauseFile={this.handlePauseFile}
handleResumeFile={this.handleResumeFile} handleResumeFile={this.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/> areAssetsEditable={areAssetsEditable}/>
<input <input
multiple={this.props.multiple} multiple={multiple}
ref="fileinput" ref="fileinput"
type="file" type="file"
style={{ style={{
@ -210,7 +197,8 @@ let FileDragAndDrop = React.createClass({
height: 0, height: 0,
width: 0 width: 0
}} }}
onChange={this.handleDrop} /> onChange={this.handleDrop}
accept={allowedExtensions}/>
</div> </div>
); );
} }

View File

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import Router from 'react-router'; import Router from 'react-router';
import { getLangText } from '../../utils/lang_utils'; import { getLangText } from '../../../utils/lang_utils';
let Link = Router.Link; let Link = Router.Link;
@ -12,7 +12,14 @@ let FileDragAndDropDialog = React.createClass({
hasFiles: React.PropTypes.bool, hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool, multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func, 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], mixins: [Router.State],
@ -32,7 +39,7 @@ let FileDragAndDropDialog = React.createClass({
queryParamsUpload.method = 'upload'; queryParamsUpload.method = 'upload';
return ( return (
<span className="file-drag-and-drop-dialog present-options"> <div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p> <p>{getLangText('Would you rather')}</p>
<Link <Link
to={this.getPath()} to={this.getPath()}
@ -51,21 +58,27 @@ let FileDragAndDropDialog = React.createClass({
{getLangText('Upload and hash your work')} {getLangText('Upload and hash your work')}
</span> </span>
</Link> </Link>
</span> </div>
); );
} else { } else {
if(this.props.multipleFiles) { if(this.props.multipleFiles) {
return ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
{getLangText('Click or drag to add files')} <p>{getLangText('Drag %s here', this.props.fileClassToUpload.plural)}</p>
<p>{getLangText('or')}</p>
<span
className="btn btn-default"
onClick={this.props.onClick}>
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)}
</span>
</span> </span>
); );
} else { } 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 ( return (
<span className="file-drag-and-drop-dialog"> <span className="file-drag-and-drop-dialog">
<p>{getLangText('Drag a file here')}</p> <p>{getLangText('Drag a %s here', this.props.fileClassToUpload.singular)}</p>
<p>{getLangText('or')}</p> <p>{getLangText('or')}</p>
<span <span
className="btn btn-default" className="btn btn-default"

View File

@ -4,7 +4,9 @@ import React from 'react';
import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image'; import FileDragAndDropPreviewImage from './file_drag_and_drop_preview_image';
import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other'; import FileDragAndDropPreviewOther from './file_drag_and_drop_preview_other';
import { getLangText } from '../../utils/lang_utils.js';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreview = React.createClass({ let FileDragAndDropPreview = React.createClass({
@ -43,6 +45,7 @@ let FileDragAndDropPreview = React.createClass({
handleDownloadFile() { handleDownloadFile() {
if(this.props.file.s3Url) { if(this.props.file.s3Url) {
// This simply opens a new browser tab with the url provided
open(this.props.file.s3Url); open(this.props.file.s3Url);
} }
}, },
@ -72,7 +75,7 @@ let FileDragAndDropPreview = React.createClass({
if(this.props.areAssetsEditable) { if(this.props.areAssetsEditable) {
removeBtn = (<div className="delete-file"> removeBtn = (<div className="delete-file">
<span <span
className="glyphicon glyphicon-remove text-center" className="glyphicon glyphicon-remove text-center"
aria-hidden="true" aria-hidden="true"
title={getLangText('Remove file')} title={getLangText('Remove file')}

View File

@ -1,10 +1,10 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ProgressBar from 'react-progressbar'; import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewImage = React.createClass({ let FileDragAndDropPreviewImage = React.createClass({
propTypes: { propTypes: {
@ -60,7 +60,9 @@ let FileDragAndDropPreviewImage = React.createClass({
<div <div
className="file-drag-and-drop-preview-image" className="file-drag-and-drop-preview-image"
style={imageStyle}> style={imageStyle}>
<ProgressBar completed={this.props.progress} color="black"/> <ProgressBar
now={Math.ceil(this.props.progress)}
className="ascribe-progress-bar ascribe-progress-bar-xs"/>
{actionSymbol} {actionSymbol}
</div> </div>
); );

View File

@ -0,0 +1,62 @@
'use strict';
import React from 'react';
import FileDragAndDropPreview from './file_drag_and_drop_preview';
import FileDragAndDropPreviewProgress from './file_drag_and_drop_preview_progress';
import { displayValidFilesFilter } from '../react_s3_fine_uploader_utils';
let FileDragAndDropPreviewIterator = React.createClass({
propTypes: {
files: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
render() {
let {
files,
handleDeleteFile,
handleCancelFile,
handlePauseFile,
handleResumeFile,
areAssetsDownloadable,
areAssetsEditable
} = this.props;
files = files.filter(displayValidFilesFilter);
if(files && files.length > 0) {
return (
<div className="file-drag-and-drop-preview-iterator">
<div className="file-drag-and-drop-preview-iterator-spacing">
{files.map((file, i) => {
return (
<FileDragAndDropPreview
key={i}
file={file}
handleDeleteFile={handleDeleteFile}
handleCancelFile={handleCancelFile}
handlePauseFile={handlePauseFile}
handleResumeFile={handleResumeFile}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={areAssetsEditable}/>
);
})}
</div>
<FileDragAndDropPreviewProgress files={files} />
</div>
);
} else {
return null;
}
}
});
export default FileDragAndDropPreviewIterator;

View File

@ -1,10 +1,10 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import ProgressBar from 'react-progressbar'; import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js'; import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewOther = React.createClass({ let FileDragAndDropPreviewOther = React.createClass({
propTypes: { propTypes: {
@ -55,11 +55,13 @@ let FileDragAndDropPreviewOther = React.createClass({
return ( return (
<div <div
className="file-drag-and-drop-preview"> className="file-drag-and-drop-preview">
<ProgressBar completed={this.props.progress} color="black"/> <ProgressBar
now={Math.ceil(this.props.progress)}
className="ascribe-progress-bar ascribe-progress-bar-xs"/>
<div className="file-drag-and-drop-preview-table-wrapper"> <div className="file-drag-and-drop-preview-table-wrapper">
<div className="file-drag-and-drop-preview-other"> <div className="file-drag-and-drop-preview-other">
{actionSymbol} {actionSymbol}
<span>{'.' + this.props.type}</span> <p>{'.' + this.props.type}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,65 @@
'use strict';
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewProgress = React.createClass({
propTypes: {
files: React.PropTypes.array
},
calcOverallFileSize() {
let overallFileSize = 0;
let files = this.props.files.filter(displayValidProgressFilesFilter);
// We just sum up all files' sizes
for(let i = 0; i < files.length; i++) {
overallFileSize += files[i].size;
}
return overallFileSize;
},
calcOverallProgress() {
let overallProgress = 0;
let overallFileSize = this.calcOverallFileSize();
let files = this.props.files.filter(displayValidProgressFilesFilter);
// We calculate the overall progress by summing the individuals
// files' progresses in relation to their size
for(let i = 0; i < files.length; i++) {
overallProgress += files[i].size / overallFileSize * files[i].progress;
}
return overallProgress;
},
render() {
let overallProgress = this.calcOverallProgress();
let overallFileSize = this.calcOverallFileSize();
let style = {
visibility: 'hidden'
};
// only visible if overallProgress is over zero
// or the overallFileSize is greater than 10MB
if(overallProgress !== 0 && overallFileSize > 10000000) {
style.visibility = 'visible';
}
return (
<ProgressBar
now={Math.ceil(overallProgress)}
label={getLangText('Overall progress%s', ': %(percent)s%')}
className="ascribe-progress-bar"
style={style} />
);
}
});
export default FileDragAndDropPreviewProgress;

View File

@ -0,0 +1,103 @@
'use strict';
import React from 'react';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils';
let UploadButton = React.createClass({
propTypes: {
onDrop: React.PropTypes.func.isRequired,
filesToUpload: React.PropTypes.array,
multiple: React.PropTypes.bool,
// For simplification purposes we're just going to use this prop as a
// label for the upload button
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
}),
allowedExtensions: React.PropTypes.string
},
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
let files = event.target.files;
if(typeof this.props.onDrop === 'function' && files) {
this.props.onDrop(files);
}
},
getUploadingFiles() {
return this.props.filesToUpload.filter((file) => file.status === 'uploading');
},
handleOnClick() {
let uploadingFiles = this.getUploadingFiles();
// We only want the button to be clickable if there are no files currently uploading
if(uploadingFiles.length === 0) {
// Firefox only recognizes the simulated mouse click if bubbles is set to true,
// but since Google Chrome propagates the event much further than needed, we
// need to stop propagation as soon as the event is created
var evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
evt.stopPropagation();
this.refs.fileinput.getDOMNode().dispatchEvent(evt);
}
},
getButtonLabel() {
let { filesToUpload, fileClassToUpload } = this.props;
// filter invalid files that might have been deleted or canceled...
filesToUpload = filesToUpload.filter(displayValidProgressFilesFilter);
// Depending on wether there is an upload going on or not we
// display the progress
if(filesToUpload.length > 0) {
return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%';
} else {
return fileClassToUpload.singular;
}
},
render() {
let {
multiple,
fileClassToUpload,
allowedExtensions
} = this.props;
return (
<button
onClick={this.handleOnClick}
className="btn btn-default btn-sm margin-left-2px"
disabled={this.getUploadingFiles().length !== 0}>
{this.getButtonLabel()}
<input
multiple={multiple}
ref="fileinput"
type="file"
style={{
display: 'none',
height: 0,
width: 0
}}
onChange={this.handleDrop}
accept={allowedExtensions}/>
</button>
);
}
});
export default UploadButton;

View File

@ -1,47 +0,0 @@
'use strict';
import React from 'react';
import FileDragAndDropPreview from './file_drag_and_drop_preview';
let FileDragAndDropPreviewIterator = React.createClass({
propTypes: {
files: React.PropTypes.array,
handleDeleteFile: React.PropTypes.func,
handleCancelFile: React.PropTypes.func,
handlePauseFile: React.PropTypes.func,
handleResumeFile: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsEditable: React.PropTypes.bool
},
render() {
if(this.props.files) {
return (
<div>
{this.props.files.map((file, i) => {
if(file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1) {
return (
<FileDragAndDropPreview
key={i}
file={file}
handleDeleteFile={this.props.handleDeleteFile}
handleCancelFile={this.props.handleCancelFile}
handlePauseFile={this.props.handlePauseFile}
handleResumeFile={this.props.handleResumeFile}
areAssetsDownloadable={this.props.areAssetsDownloadable}
areAssetsEditable={this.props.areAssetsEditable}/>
);
} else {
return null;
}
})}
</div>
);
} else {
return null;
}
}
});
export default FileDragAndDropPreviewIterator;

View File

@ -1,16 +1,13 @@
'use strict'; 'use strict';
import React from 'react/addons'; import React from 'react/addons';
import fineUploader from 'fineUploader';
import Router from 'react-router'; import Router from 'react-router';
import Q from 'q'; import Q from 'q';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
import S3Fetcher from '../../fetchers/s3_fetcher'; import S3Fetcher from '../../fetchers/s3_fetcher';
import fineUploader from 'fineUploader'; import FileDragAndDrop from './ascribe_file_drag_and_drop/file_drag_and_drop';
import FileDragAndDrop from './file_drag_and_drop';
import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions'; import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -18,9 +15,12 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AppConstants from '../../constants/application_constants'; import AppConstants from '../../constants/application_constants';
import { computeHashOfFile } from '../../utils/file_utils'; import { computeHashOfFile } from '../../utils/file_utils';
import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils';
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
var ReactS3FineUploader = React.createClass({
let ReactS3FineUploader = React.createClass({
propTypes: { propTypes: {
keyRoutine: React.PropTypes.shape({ keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string, url: React.PropTypes.string,
@ -37,7 +37,7 @@ var ReactS3FineUploader = React.createClass({
React.PropTypes.number React.PropTypes.number
]) ])
}), }),
submitKey: React.PropTypes.func, submitFile: React.PropTypes.func,
autoUpload: React.PropTypes.bool, autoUpload: React.PropTypes.bool,
debug: React.PropTypes.bool, debug: React.PropTypes.bool,
objectProperties: React.PropTypes.shape({ objectProperties: React.PropTypes.shape({
@ -84,7 +84,8 @@ var ReactS3FineUploader = React.createClass({
}), }),
validation: React.PropTypes.shape({ validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number, itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string sizeLimit: React.PropTypes.string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
}), }),
messages: React.PropTypes.shape({ messages: React.PropTypes.shape({
unsupportedBrowser: React.PropTypes.string unsupportedBrowser: React.PropTypes.string
@ -94,6 +95,7 @@ var ReactS3FineUploader = React.createClass({
retry: React.PropTypes.shape({ retry: React.PropTypes.shape({
enableAuto: React.PropTypes.bool enableAuto: React.PropTypes.bool
}), }),
uploadStarted: React.PropTypes.func,
setIsUploadReady: React.PropTypes.func, setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func, isReadyForFormSubmission: React.PropTypes.func,
areAssetsDownloadable: React.PropTypes.bool, areAssetsDownloadable: React.PropTypes.bool,
@ -110,7 +112,22 @@ var ReactS3FineUploader = React.createClass({
enableLocalHashing: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool,
// automatically injected by React-Router // automatically injected by React-Router
query: React.PropTypes.object query: React.PropTypes.object,
// 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
}),
// Uploading functionality of react fineuploader is disconnected from its UI
// layer, which means that literally every (properly adjusted) react element
// can handle the UI handling.
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
])
}, },
mixins: [Router.State], mixins: [Router.State],
@ -124,6 +141,7 @@ var ReactS3FineUploader = React.createClass({
bucket: 'ascribe0' bucket: 'ascribe0'
}, },
request: { request: {
//endpoint: 'https://www.ascribe.io.global.prod.fastly.net',
endpoint: 'https://ascribe0.s3.amazonaws.com', endpoint: 'https://ascribe0.s3.amazonaws.com',
accessKey: 'AKIAIVCZJ33WSCBQ3QDA' accessKey: 'AKIAIVCZJ33WSCBQ3QDA'
}, },
@ -161,7 +179,12 @@ var ReactS3FineUploader = React.createClass({
return name; return name;
}, },
multiple: false, multiple: false,
defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.') defaultErrorMessage: getLangText('Unexpected error. Please contact us if this happens repeatedly.'),
fileClassToUpload: {
singular: getLangText('file'),
plural: getLangText('files')
},
fileInputElement: FileDragAndDrop
}; };
}, },
@ -228,13 +251,27 @@ var ReactS3FineUploader = React.createClass({
onDeleteComplete: this.onDeleteComplete, onDeleteComplete: this.onDeleteComplete,
onSessionRequestComplete: this.onSessionRequestComplete, onSessionRequestComplete: this.onSessionRequestComplete,
onError: this.onError, onError: this.onError,
onValidate: this.onValidate,
onUploadChunk: this.onUploadChunk, onUploadChunk: this.onUploadChunk,
onUploadChunkSuccess: this.onUploadChunkSuccess onUploadChunkSuccess: this.onUploadChunkSuccess
} }
}; };
}, },
// Resets the whole react fineuploader component to its initial state
reset() {
// Cancel all currently ongoing uploads
this.state.uploader.cancelAll();
// and reset component in general
this.state.uploader.reset();
// proclaim that upload is not ready
this.props.setIsUploadReady(false);
// reset internal data structures of component
this.setState(this.getInitialState());
},
requestKey(fileId) { requestKey(fileId) {
let filename = this.state.uploader.getName(fileId); let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId); let uuid = this.state.uploader.getUuid(fileId);
@ -297,6 +334,9 @@ var ReactS3FineUploader = React.createClass({
} else if(res.digitalwork) { } else if(res.digitalwork) {
file.s3Url = res.digitalwork.url_safe; file.s3Url = res.digitalwork.url_safe;
file.s3UrlSafe = res.digitalwork.url_safe; file.s3UrlSafe = res.digitalwork.url_safe;
} else if(res.contractblob) {
file.s3Url = res.contractblob.url_safe;
file.s3UrlSafe = res.contractblob.url_safe;
} else { } else {
throw new Error(getLangText('Could not find a url to download.')); throw new Error(getLangText('Could not find a url to download.'));
} }
@ -325,11 +365,9 @@ var ReactS3FineUploader = React.createClass({
completed: false completed: false
}; };
let newState = React.addons.update(this.state, { let startedChunks = React.addons.update(this.state.startedChunks, { $set: chunks });
startedChunks: { $set: chunks }
});
this.setState(newState); this.setState({ startedChunks });
}, },
onUploadChunkSuccess(id, chunkData, responseJson, xhr) { onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
@ -342,75 +380,65 @@ var ReactS3FineUploader = React.createClass({
chunks[chunkKey].responseJson = responseJson; chunks[chunkKey].responseJson = responseJson;
chunks[chunkKey].xhr = xhr; chunks[chunkKey].xhr = xhr;
let newState = React.addons.update(this.state, { let startedChunks = React.addons.update(this.state.startedChunks, { $set: chunks });
startedChunks: { $set: chunks }
});
this.setState(newState); this.setState({ startedChunks });
} }
}, },
onComplete(id, name, res, xhr) { onComplete(id, name, res, xhr) {
// there has been an issue with the server's connection // there has been an issue with the server's connection
if(xhr.status === 0) { if((xhr && xhr.status === 0) || res.error) {
console.logGlobal(new Error(res.error || 'Complete was called but there wasn\t a success'), false, {
console.logGlobal(new Error('Complete was called but there wasn\t a success'), false, {
files: this.state.filesToUpload, files: this.state.filesToUpload,
chunks: this.state.chunks chunks: this.state.chunks
}); });
} else {
let files = this.state.filesToUpload;
return; // Set the state of the completed file to 'upload successful' in order to
} // remove it from the GUI
files[id].status = 'upload successful';
files[id].key = this.state.uploader.getKey(id);
let files = this.state.filesToUpload; let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: files });
this.setState({ filesToUpload });
// Set the state of the completed file to 'upload successful' in order to // Only after the blob has been created server-side, we can make the form submittable.
// remove it from the GUI this.createBlob(files[id])
files[id].status = 'upload successful'; .then(() => {
files[id].key = this.state.uploader.getKey(id); // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined
let newState = React.addons.update(this.state, { if(this.props.submitFile) {
filesToUpload: { $set: files } this.props.submitFile(files[id]);
});
this.setState(newState);
// Only after the blob has been created server-side, we can make the form submittable.
this.createBlob(files[id])
.then(() => {
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey
// are optional, we'll only trigger them when they're actually defined
if(this.props.submitKey) {
this.props.submitKey(files[id].key);
} else {
console.warn('You didn\'t define submitKey in as a prop in react-s3-fine-uploader');
}
// for explanation, check comment of if statement above
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload,
// the form is ready for submission or not
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
// if so, set uploadstatus to true
this.props.setIsUploadReady(true);
} else { } else {
this.props.setIsUploadReady(false); console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader');
} }
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); // for explanation, check comment of if statement above
} if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
}) // also, lets check if after the completion of this upload,
.catch((err) => { // the form is ready for submission or not
console.logGlobal(err, false, { if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
files: this.state.filesToUpload, // if so, set uploadstatus to true
chunks: this.state.chunks this.props.setIsUploadReady(true);
} else {
this.props.setIsUploadReady(false);
}
} else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
}
})
.catch((err) => {
console.logGlobal(err, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
});
let notification = new GlobalNotificationModel(err.message, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
}); });
let notification = new GlobalNotificationModel(err.message, 'danger', 5000); }
GlobalNotificationActions.appendGlobalNotification(notification);
});
}, },
onError(id, name, errorReason) { onError(id, name, errorReason) {
@ -420,27 +448,32 @@ var ReactS3FineUploader = React.createClass({
}); });
this.state.uploader.cancelAll(); this.state.uploader.cancelAll();
let notification = new GlobalNotificationModel(this.props.defaultErrorMessage, 'danger', 5000); let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
}, },
onValidate(data) { isFileValid(file) {
if(data.size > this.props.validation.sizeLimit) { if(file.size > this.props.validation.sizeLimit) {
this.state.uploader.cancelAll();
let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; let fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000;
let notification = new GlobalNotificationModel(getLangText('Your file is bigger than %d MB', fileSizeInMegaBytes), 'danger', 5000);
let notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
return false;
} else {
return true;
} }
}, },
onCancel(id) { onCancel(id) {
this.removeFileWithIdFromFilesToUpload(id); // when a upload is canceled, we need to update this components file array
this.setStatusOfFile(id, 'canceled');
let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000); let notification = new GlobalNotificationModel(getLangText('File upload canceled'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined // are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) { if(this.props.isReadyForFormSubmission(this.state.filesToUpload)) {
@ -452,15 +485,17 @@ var ReactS3FineUploader = React.createClass({
} else { } else {
console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader'); console.warn('You didn\'t define the functions isReadyForFormSubmission and/or setIsUploadReady in as a prop in react-s3-fine-uploader');
} }
return true;
}, },
onProgress(id, name, uploadedBytes, totalBytes) { onProgress(id, name, uploadedBytes, totalBytes) {
let newState = React.addons.update(this.state, { let filesToUpload = React.addons.update(this.state.filesToUpload, {
filesToUpload: { [id]: { [id]: {
progress: { $set: (uploadedBytes / totalBytes) * 100} } progress: { $set: (uploadedBytes / totalBytes) * 100}
} }
}); });
this.setState(newState); this.setState({ filesToUpload });
}, },
onSessionRequestComplete(response, success) { onSessionRequestComplete(response, success) {
@ -482,8 +517,9 @@ var ReactS3FineUploader = React.createClass({
return file; return file;
}); });
let newState = React.addons.update(this.state, {filesToUpload: {$set: updatedFilesToUpload}}); let filesToUpload = React.addons.update(this.state.filesToUpload, {$set: updatedFilesToUpload});
this.setState(newState);
this.setState({filesToUpload });
} else { } else {
// server has to respond with 204 // server has to respond with 204
//let notification = new GlobalNotificationModel('Could not load attached files (Further data)', 'danger', 10000); //let notification = new GlobalNotificationModel('Could not load attached files (Further data)', 'danger', 10000);
@ -495,16 +531,16 @@ var ReactS3FineUploader = React.createClass({
onDeleteComplete(id, xhr, isError) { onDeleteComplete(id, xhr, isError) {
if(isError) { if(isError) {
let notification = new GlobalNotificationModel(getLangText('Couldn\'t delete file'), 'danger', 10000); this.setStatusOfFile(id, 'online');
let notification = new GlobalNotificationModel(getLangText('There was an error deleting your file.'), 'danger', 10000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} else { } else {
this.removeFileWithIdFromFilesToUpload(id);
let notification = new GlobalNotificationModel(getLangText('File deleted'), 'success', 5000); let notification = new GlobalNotificationModel(getLangText('File deleted'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification); GlobalNotificationActions.appendGlobalNotification(notification);
} }
// since the form validation props isReadyForFormSubmission, setIsUploadReady and submitKey // since the form validation props isReadyForFormSubmission, setIsUploadReady and submitFile
// are optional, we'll only trigger them when they're actually defined // are optional, we'll only trigger them when they're actually defined
if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) { if(this.props.isReadyForFormSubmission && this.props.setIsUploadReady) {
// also, lets check if after the completion of this upload, // also, lets check if after the completion of this upload,
@ -521,7 +557,15 @@ var ReactS3FineUploader = React.createClass({
}, },
handleDeleteFile(fileId) { handleDeleteFile(fileId) {
// In some instances (when the file was already uploaded and is just displayed to the user) // We set the files state to 'deleted' immediately, so that the user is not confused with
// the unresponsiveness of the UI
//
// If there is an error during the deletion, we will just change the status back to 'online'
// and display an error message
this.setStatusOfFile(fileId, 'deleted');
// In some instances (when the file was already uploaded and is just displayed to the user
// - for example in the contract or additional files dialog)
// fineuploader does not register an id on the file (we do, don't be confused by this!). // fineuploader does not register an id on the file (we do, don't be confused by this!).
// Since you can only delete a file by its id, we have to implement this method ourselves // Since you can only delete a file by its id, we have to implement this method ourselves
// //
@ -532,13 +576,11 @@ var ReactS3FineUploader = React.createClass({
if(this.state.filesToUpload[fileId].status !== 'online') { if(this.state.filesToUpload[fileId].status !== 'online') {
// delete file from server // delete file from server
this.state.uploader.deleteFile(fileId); this.state.uploader.deleteFile(fileId);
// this is being continues in onDeleteFile, as // this is being continued in onDeleteFile, as
// fineuploaders deleteFile does not return a correct callback or // fineuploaders deleteFile does not return a correct callback or
// promise // promise
} else { } else {
let fileToDelete = this.state.filesToUpload[fileId]; let fileToDelete = this.state.filesToUpload[fileId];
fileToDelete.status = 'deleted';
S3Fetcher S3Fetcher
.deleteFile(fileToDelete.s3Key, fileToDelete.s3Bucket) .deleteFile(fileToDelete.s3Key, fileToDelete.s3Bucket)
.then(() => this.onDeleteComplete(fileToDelete.id, null, false)) .then(() => this.onDeleteComplete(fileToDelete.id, null, false))
@ -570,10 +612,25 @@ var ReactS3FineUploader = React.createClass({
handleUploadFile(files) { handleUploadFile(files) {
// If multiple set and user already uploaded its work, // If multiple set and user already uploaded its work,
// cancel upload // cancel upload
if(!this.props.multiple && this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled').length > 0) { if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) {
return; return;
} }
// validate each submitted file if it fits the file size
let validFiles = [];
for(let i = 0; i < files.length; i++) {
if(this.isFileValid(files[i])) {
validFiles.push(files[i]);
}
}
// override standard files list with only valid files
files = validFiles;
// Call this method to signal the outside component that an upload is in progress
if(typeof this.props.uploadStarted === 'function' && files.length > 0) {
this.props.uploadStarted();
}
// if multiple is set to false and user drops multiple files into the dropzone, // if multiple is set to false and user drops multiple files into the dropzone,
// take the first one and notify user that only one file can be submitted // take the first one and notify user that only one file can be submitted
if(!this.props.multiple && files.length > 1) { if(!this.props.multiple && files.length > 1) {
@ -684,8 +741,10 @@ var ReactS3FineUploader = React.createClass({
// if we're not hashing the files locally, we're just going to hand them over to fineuploader // if we're not hashing the files locally, we're just going to hand them over to fineuploader
// to upload them to the server // to upload them to the server
} else { } else {
this.state.uploader.addFiles(files); if(files.length > 0) {
this.synchronizeFileLists(files); this.state.uploader.addFiles(files);
this.synchronizeFileLists(files);
}
} }
}, },
@ -709,6 +768,7 @@ var ReactS3FineUploader = React.createClass({
synchronizeFileLists(files) { synchronizeFileLists(files) {
let oldFiles = this.state.filesToUpload; let oldFiles = this.state.filesToUpload;
let oldAndNewFiles = this.state.uploader.getUploads(); let oldAndNewFiles = this.state.uploader.getUploads();
// Add fineuploader specific information to new files // Add fineuploader specific information to new files
for(let i = 0; i < oldAndNewFiles.length; i++) { for(let i = 0; i < oldAndNewFiles.length; i++) {
for(let j = 0; j < files.length; j++) { for(let j = 0; j < files.length; j++) {
@ -723,6 +783,22 @@ var ReactS3FineUploader = React.createClass({
// and re-add fineuploader specific information for old files as well // and re-add fineuploader specific information for old files as well
for(let i = 0; i < oldAndNewFiles.length; i++) { for(let i = 0; i < oldAndNewFiles.length; i++) {
for(let j = 0; j < oldFiles.length; j++) { for(let j = 0; j < oldFiles.length; j++) {
// EXCEPTION:
//
// Files do not necessarily come from the user's hard drive but can also be fetched
// from Amazon S3. This is handled in onSessionRequestComplete.
//
// If the user deletes one of those files, then fineuploader will still keep it in his
// files array but with key, progress undefined and size === -1 but
// status === 'upload successful'.
// This poses a problem as we depend on the amount of files that have
// status === 'upload successful', therefore once the file is synced,
// we need to tag its status as 'deleted' (which basically happens here)
if(oldAndNewFiles[i].size === -1 && (!oldAndNewFiles[i].progress || oldAndNewFiles[i].progress === 0)) {
oldAndNewFiles[i].status = 'deleted';
}
if(oldAndNewFiles[i].originalName === oldFiles[j].name) { if(oldAndNewFiles[i].originalName === oldFiles[j].name) {
oldAndNewFiles[i].progress = oldFiles[j].progress; oldAndNewFiles[i].progress = oldFiles[j].progress;
oldAndNewFiles[i].type = oldFiles[j].type; oldAndNewFiles[i].type = oldFiles[j].type;
@ -733,38 +809,23 @@ var ReactS3FineUploader = React.createClass({
} }
// set the new file array // set the new file array
let newState = React.addons.update(this.state, { let filesToUpload = React.addons.update(this.state.filesToUpload, { $set: oldAndNewFiles });
filesToUpload: { $set: oldAndNewFiles }
});
this.setState(newState);
},
removeFileWithIdFromFilesToUpload(fileId) { this.setState({ filesToUpload });
// also, sync files from state with the ones from fineuploader
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can
filesToUpload.splice(fileId, 1);
// set state
let newState = React.addons.update(this.state, {
filesToUpload: { $set: filesToUpload }
});
this.setState(newState);
}, },
setStatusOfFile(fileId, status) { setStatusOfFile(fileId, status) {
// also, sync files from state with the ones from fineuploader let changeSet = {};
let filesToUpload = JSON.parse(JSON.stringify(this.state.filesToUpload));
// splice because I can if(status === 'deleted' || status === 'canceled') {
filesToUpload[fileId].status = status; changeSet.progress = { $set: 0 };
}
// set state changeSet.status = { $set: status };
let newState = React.addons.update(this.state, {
filesToUpload: { $set: filesToUpload } let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
});
this.setState(newState); this.setState({ filesToUpload });
}, },
isDropzoneInactive() { isDropzoneInactive() {
@ -779,27 +840,48 @@ var ReactS3FineUploader = React.createClass({
}, },
getAllowedExtensions() {
let { validation } = this.props;
if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) {
return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions);
} else {
return null;
}
},
render() { render() {
return ( let {
<div> multiple,
<FileDragAndDrop areAssetsDownloadable,
className="file-drag-and-drop" areAssetsEditable,
onDrop={this.handleUploadFile} onInactive,
filesToUpload={this.state.filesToUpload} enableLocalHashing,
handleDeleteFile={this.handleDeleteFile} fileClassToUpload,
handleCancelFile={this.handleCancelFile} validation,
handlePauseFile={this.handlePauseFile} fileInputElement
handleResumeFile={this.handleResumeFile} } = this.props;
handleCancelHashing={this.handleCancelHashing}
multiple={this.props.multiple} // Here we initialize the template that has been either provided from the outside
areAssetsDownloadable={this.props.areAssetsDownloadable} // or the default input that is FileDragAndDrop.
areAssetsEditable={this.props.areAssetsEditable} return React.createElement(fileInputElement, {
onInactive={this.props.onInactive} onDrop: this.handleUploadFile,
dropzoneInactive={this.isDropzoneInactive()} filesToUpload: this.state.filesToUpload,
hashingProgress={this.state.hashingProgress} handleDeleteFile: this.handleDeleteFile,
enableLocalHashing={this.props.enableLocalHashing} /> handleCancelFile: this.handleCancelFile,
</div> handlePauseFile: this.handlePauseFile,
); handleResumeFile: this.handleResumeFile,
handleCancelHashing: this.handleCancelHashing,
multiple: multiple,
areAssetsDownloadable: areAssetsDownloadable,
areAssetsEditable: areAssetsEditable,
onInactive: onInactive,
dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress,
enableLocalHashing: enableLocalHashing,
fileClassToUpload: fileClassToUpload,
allowedExtensions: this.getAllowedExtensions()
});
} }
}); });

View File

@ -0,0 +1,73 @@
'use strict';
export const formSubmissionValidation = {
/**
* Returns a boolean if there has been at least one file uploaded
* successfully without it being deleted or canceled.
* @param {array of files} files provided by react fine uploader
* @return {boolean}
*/
atLeastOneUploadedFile(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;
}
},
/**
* File submission for the form is optional, but if the user decides to submit a file
* the form is not ready until there are no more files currently uploading.
* @param {array of files} files files provided by react fine uploader
* @return {boolean} [description]
*/
fileOptional(files) {
let uploadingFiles = files.filter((file) => file.status === 'submitting');
if (uploadingFiles.length === 0) {
return true;
} else {
return false;
}
}
};
/**
* Filter function for filtering all deleted and canceled files
* @param {object} file A file from filesToUpload that has status as a prop.
* @return {boolean}
*/
export function displayValidFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled';
}
/**
* Filter function for which files to integrate in the progress process
* @param {object} file A file from filesToUpload, that has a status as a prop.
* @return {boolean}
*/
export function displayValidProgressFilesFilter(file) {
return file.status !== 'deleted' && file.status !== 'canceled' && file.status !== 'online';
}
/**
* Fineuploader allows to specify the file extensions that are allowed to upload.
* For our self defined input, we can reuse those declarations to restrict which files
* the user can pick from his hard drive.
*
* Takes an array of file extensions (['pdf', 'png', ...]) and transforms them into a string
* that can be passed into an html5 input via its 'accept' prop.
* @param {array} allowedExtensions Array of strings without a dot prefixed
* @return {string} Joined string (comma-separated) of the passed-in array
*/
export function transformAllowedExtensionsToInputAcceptProp(allowedExtensions) {
// add a dot in front of the extension
let prefixedAllowedExtensions = allowedExtensions.map((ext) => '.' + ext);
// generate a comma separated list to add them to the DOM element
// See: http://stackoverflow.com/questions/4328947/limit-file-format-when-using-input-type-file
return prefixedAllowedExtensions.join(', ');
}

View File

@ -3,7 +3,7 @@
* *
* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com * Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com
* *
* Version: 5.2.2 * Version: 5.3.0
* *
* Homepage: http://fineuploader.com * Homepage: http://fineuploader.com
* *
@ -894,7 +894,7 @@ var qq = function(element) {
}()); }());
/*global qq */ /*global qq */
qq.version = "5.2.2"; qq.version = "5.3.0";
/* globals qq */ /* globals qq */
qq.supportedFeatures = (function() { qq.supportedFeatures = (function() {
@ -1928,6 +1928,10 @@ qq.status = {
this._endpointStore.set(endpoint, id); this._endpointStore.set(endpoint, id);
}, },
setForm: function(elementOrId) {
this._updateFormSupportAndParams(elementOrId);
},
setItemLimit: function(newItemLimit) { setItemLimit: function(newItemLimit) {
this._currentItemLimit = newItemLimit; this._currentItemLimit = newItemLimit;
}, },
@ -1945,16 +1949,11 @@ qq.status = {
}, },
uploadStoredFiles: function() { uploadStoredFiles: function() {
var idToUpload;
if (this._storedIds.length === 0) { if (this._storedIds.length === 0) {
this._itemError("noFilesError"); this._itemError("noFilesError");
} }
else { else {
while (this._storedIds.length) { this._uploadStoredFiles();
idToUpload = this._storedIds.shift();
this._uploadFile(idToUpload);
}
} }
} }
}; };
@ -2038,10 +2037,11 @@ qq.status = {
}); });
}, },
_createStore: function(initialValue, readOnlyValues) { _createStore: function(initialValue, _readOnlyValues_) {
var store = {}, var store = {},
catchall = initialValue, catchall = initialValue,
perIdReadOnlyValues = {}, perIdReadOnlyValues = {},
readOnlyValues = _readOnlyValues_,
copy = function(orig) { copy = function(orig) {
if (qq.isObject(orig)) { if (qq.isObject(orig)) {
return qq.extend({}, orig); return qq.extend({}, orig);
@ -2095,8 +2095,20 @@ qq.status = {
addReadOnly: function(id, values) { addReadOnly: function(id, values) {
// Only applicable to Object stores // Only applicable to Object stores
if (qq.isObject(store)) { if (qq.isObject(store)) {
perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {}; // If null ID, apply readonly values to all files
qq.extend(perIdReadOnlyValues[id], values); if (id === null) {
if (qq.isFunction(values)) {
readOnlyValues = values;
}
else {
readOnlyValues = readOnlyValues || {};
qq.extend(readOnlyValues, values);
}
}
else {
perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {};
qq.extend(perIdReadOnlyValues[id], values);
}
} }
}, },
@ -2882,7 +2894,7 @@ qq.status = {
_onBeforeManualRetry: function(id) { _onBeforeManualRetry: function(id) {
var itemLimit = this._currentItemLimit, var itemLimit = this._currentItemLimit,
fileName; fileName;
console.log(this._handler.isValid(id));
if (this._preventRetries[id]) { if (this._preventRetries[id]) {
this.log("Retries are forbidden for id " + id, "warn"); this.log("Retries are forbidden for id " + id, "warn");
return false; return false;
@ -3005,13 +3017,14 @@ qq.status = {
this._onSubmit.apply(this, arguments); this._onSubmit.apply(this, arguments);
this._uploadData.setStatus(id, qq.status.SUBMITTED); this._uploadData.setStatus(id, qq.status.SUBMITTED);
this._onSubmitted.apply(this, arguments); this._onSubmitted.apply(this, arguments);
this._options.callbacks.onSubmitted.apply(this, arguments);
if (this._options.autoUpload) { if (this._options.autoUpload) {
this._options.callbacks.onSubmitted.apply(this, arguments);
this._uploadFile(id); this._uploadFile(id);
} }
else { else {
this._storeForLater(id); this._storeForLater(id);
this._options.callbacks.onSubmitted.apply(this, arguments);
} }
}, },
@ -3238,6 +3251,23 @@ qq.status = {
} }
}, },
_updateFormSupportAndParams: function(formElementOrId) {
this._options.form.element = formElementOrId;
this._formSupport = qq.FormSupport && new qq.FormSupport(
this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this)
);
if (this._formSupport && this._formSupport.attachedToForm) {
this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject);
this._options.autoUpload = this._formSupport.newAutoUpload;
if (this._formSupport.newEndpoint) {
this.setEndpoint(this._formSupport.newEndpoint);
}
}
},
_upload: function(id, params, endpoint) { _upload: function(id, params, endpoint) {
var name = this.getName(id); var name = this.getName(id);
@ -3264,6 +3294,25 @@ qq.status = {
} }
}, },
_uploadStoredFiles: function() {
var idToUpload, stillSubmitting,
self = this;
while (this._storedIds.length) {
idToUpload = this._storedIds.shift();
this._uploadFile(idToUpload);
}
// If we are still waiting for some files to clear validation, attempt to upload these again in a bit
stillSubmitting = this.getUploads({status: qq.status.SUBMITTING}).length;
if (stillSubmitting) {
qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly.");
setTimeout(function() {
self._uploadStoredFiles();
}, 1000);
}
},
/** /**
* Performs some internal validation checks on an item, defined in the `validation` option. * Performs some internal validation checks on an item, defined in the `validation` option.
* *
@ -5271,6 +5320,7 @@ qq.XhrUploadHandler = function(spec) {
*/ */
getResumableFilesData: function() { getResumableFilesData: function() {
var resumableFilesData = []; var resumableFilesData = [];
handler._iterateResumeRecords(function(key, uploadData) { handler._iterateResumeRecords(function(key, uploadData) {
handler.moveInProgressToRemaining(null, uploadData.chunking.inProgress, uploadData.chunking.remaining); handler.moveInProgressToRemaining(null, uploadData.chunking.inProgress, uploadData.chunking.remaining);
@ -5461,7 +5511,7 @@ qq.XhrUploadHandler = function(spec) {
_iterateResumeRecords: function(callback) { _iterateResumeRecords: function(callback) {
if (resumeEnabled) { if (resumeEnabled) {
qq.each(localStorage, function(key, item) { qq.each(localStorage, function(key, item) {
if (key.indexOf(qq.format("qq{}resume-", namespace)) === 0) { if (key.indexOf(qq.format("qq{}resume", namespace)) === 0) {
var uploadData = JSON.parse(item); var uploadData = JSON.parse(item);
callback(key, uploadData); callback(key, uploadData);
} }
@ -5728,7 +5778,9 @@ qq.WindowReceiveMessage = function(o) {
}, },
getItemByFileId: function(id) { getItemByFileId: function(id) {
return this._templating.getFileContainer(id); if (!this._templating.isHiddenForever(id)) {
return this._templating.getFileContainer(id);
}
}, },
reset: function() { reset: function() {
@ -6238,11 +6290,6 @@ qq.WindowReceiveMessage = function(o) {
dontDisplay = this._handler.isProxied(id) && this._options.scaling.hideScaled, dontDisplay = this._handler.isProxied(id) && this._options.scaling.hideScaled,
record; record;
// If we don't want this file to appear in the UI, skip all of this UI-related logic.
if (dontDisplay) {
return;
}
if (this._options.display.prependFiles) { if (this._options.display.prependFiles) {
if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) { if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
prependIndex = this._filesInBatchAddedToUi - 1; prependIndex = this._filesInBatchAddedToUi - 1;
@ -6274,7 +6321,7 @@ qq.WindowReceiveMessage = function(o) {
} }
} }
this._templating.addFile(id, this._options.formatFileName(name), prependData); this._templating.addFile(id, this._options.formatFileName(name), prependData, dontDisplay);
if (canned) { if (canned) {
this._thumbnailUrls[id] && this._templating.updateThumbnail(id, this._thumbnailUrls[id], true); this._thumbnailUrls[id] && this._templating.updateThumbnail(id, this._thumbnailUrls[id], true);
@ -6638,6 +6685,7 @@ qq.Templating = function(spec) {
HIDE_DROPZONE_ATTR = "qq-hide-dropzone", HIDE_DROPZONE_ATTR = "qq-hide-dropzone",
DROPZPONE_TEXT_ATTR = "qq-drop-area-text", DROPZPONE_TEXT_ATTR = "qq-drop-area-text",
IN_PROGRESS_CLASS = "qq-in-progress", IN_PROGRESS_CLASS = "qq-in-progress",
HIDDEN_FOREVER_CLASS = "qq-hidden-forever",
isCancelDisabled = false, isCancelDisabled = false,
generatedThumbnails = 0, generatedThumbnails = 0,
thumbnailQueueMonitorRunning = false, thumbnailQueueMonitorRunning = false,
@ -7273,7 +7321,7 @@ qq.Templating = function(spec) {
isCancelDisabled = true; isCancelDisabled = true;
}, },
addFile: function(id, name, prependInfo) { addFile: function(id, name, prependInfo, hideForever) {
var fileEl = qq.toElement(templateHtml.fileTemplate), var fileEl = qq.toElement(templateHtml.fileTemplate),
fileNameEl = getTemplateEl(fileEl, selectorClasses.file), fileNameEl = getTemplateEl(fileEl, selectorClasses.file),
uploaderEl = getTemplateEl(container, selectorClasses.uploader), uploaderEl = getTemplateEl(container, selectorClasses.uploader),
@ -7296,30 +7344,36 @@ qq.Templating = function(spec) {
fileList.appendChild(fileEl); fileList.appendChild(fileEl);
} }
hide(getProgress(id)); if (hideForever) {
hide(getSize(id)); fileEl.style.display = "none";
hide(getDelete(id)); qq(fileEl).addClass(HIDDEN_FOREVER_CLASS);
hide(getRetry(id));
hide(getPause(id));
hide(getContinue(id));
if (isCancelDisabled) {
this.hideCancel(id);
} }
else {
hide(getProgress(id));
hide(getSize(id));
hide(getDelete(id));
hide(getRetry(id));
hide(getPause(id));
hide(getContinue(id));
thumb = getThumbnail(id); if (isCancelDisabled) {
if (thumb && !thumb.src) { this.hideCancel(id);
cachedWaitingForThumbnailImg.then(function(waitingImg) { }
thumb.src = waitingImg.src;
if (waitingImg.style.maxHeight && waitingImg.style.maxWidth) {
qq(thumb).css({
maxHeight: waitingImg.style.maxHeight,
maxWidth: waitingImg.style.maxWidth
});
}
show(thumb); thumb = getThumbnail(id);
}); if (thumb && !thumb.src) {
cachedWaitingForThumbnailImg.then(function(waitingImg) {
thumb.src = waitingImg.src;
if (waitingImg.style.maxHeight && waitingImg.style.maxWidth) {
qq(thumb).css({
maxHeight: waitingImg.style.maxHeight,
maxWidth: waitingImg.style.maxWidth
});
}
show(thumb);
});
}
} }
}, },
@ -7413,6 +7467,10 @@ qq.Templating = function(spec) {
icon && qq(icon).addClass(options.classes.editable); icon && qq(icon).addClass(options.classes.editable);
}, },
isHiddenForever: function(id) {
return qq(getFile(id)).hasClass(HIDDEN_FOREVER_CLASS);
},
hideEditIcon: function(id) { hideEditIcon: function(id) {
var icon = getEditIcon(id); var icon = getEditIcon(id);
@ -7572,13 +7630,17 @@ qq.Templating = function(spec) {
}, },
generatePreview: function(id, optFileOrBlob) { generatePreview: function(id, optFileOrBlob) {
thumbGenerationQueue.push({id: id, optFileOrBlob: optFileOrBlob}); if (!this.isHiddenForever(id)) {
!thumbnailQueueMonitorRunning && generateNextQueuedPreview(); thumbGenerationQueue.push({id: id, optFileOrBlob: optFileOrBlob});
!thumbnailQueueMonitorRunning && generateNextQueuedPreview();
}
}, },
updateThumbnail: function(id, thumbnailUrl, showWaitingImg) { updateThumbnail: function(id, thumbnailUrl, showWaitingImg) {
thumbGenerationQueue.push({update: true, id: id, thumbnailUrl: thumbnailUrl, showWaitingImg: showWaitingImg}); if (!this.isHiddenForever(id)) {
!thumbnailQueueMonitorRunning && generateNextQueuedPreview(); thumbGenerationQueue.push({update: true, id: id, thumbnailUrl: thumbnailUrl, showWaitingImg: showWaitingImg});
!thumbnailQueueMonitorRunning && generateNextQueuedPreview();
}
}, },
hasDialog: function(type) { hasDialog: function(type) {
@ -9489,12 +9551,6 @@ qq.s3.XhrUploadHandler = function(spec, proxy) {
result.success, result.success,
function failure(reason, xhr) { function failure(reason, xhr) {
console.logGlobal(reason + 'in chunked.combine', false, {
uploadId,
etagMap,
result
});
result.failure(upload.done(id, xhr).response, xhr); result.failure(upload.done(id, xhr).response, xhr);
} }
); );
@ -12335,7 +12391,7 @@ qq.Scaler = function(spec, log) {
"use strict"; "use strict";
var self = this, var self = this,
includeReference = spec.sendOriginal, includeOriginal = spec.sendOriginal,
orient = spec.orient, orient = spec.orient,
defaultType = spec.defaultType, defaultType = spec.defaultType,
defaultQuality = spec.defaultQuality / 100, defaultQuality = spec.defaultQuality / 100,
@ -12385,16 +12441,18 @@ qq.Scaler = function(spec, log) {
}); });
}); });
includeReference && records.push({ records.push({
uuid: originalFileUuid, uuid: originalFileUuid,
name: originalFileName, name: originalFileName,
blob: originalBlob size: originalBlob.size,
blob: includeOriginal ? originalBlob : null
}); });
} }
else { else {
records.push({ records.push({
uuid: originalFileUuid, uuid: originalFileUuid,
name: originalFileName, name: originalFileName,
size: originalBlob.size,
blob: originalBlob blob: originalBlob
}); });
} }
@ -12413,19 +12471,17 @@ qq.Scaler = function(spec, log) {
proxyGroupId = qq.getUniqueId(); proxyGroupId = qq.getUniqueId();
qq.each(self.getFileRecords(uuid, name, file), function(idx, record) { qq.each(self.getFileRecords(uuid, name, file), function(idx, record) {
var relatedBlob = file, var blobSize = record.size,
relatedSize = size,
id; id;
if (record.blob instanceof qq.BlobProxy) { if (record.blob instanceof qq.BlobProxy) {
relatedBlob = record.blob; blobSize = -1;
relatedSize = -1;
} }
id = uploadData.addFile({ id = uploadData.addFile({
uuid: record.uuid, uuid: record.uuid,
name: record.name, name: record.name,
size: relatedSize, size: blobSize,
batchId: batchId, batchId: batchId,
proxyGroupId: proxyGroupId proxyGroupId: proxyGroupId
}); });
@ -12437,10 +12493,13 @@ qq.Scaler = function(spec, log) {
originalId = id; originalId = id;
} }
addFileToHandler(id, relatedBlob); if (record.blob) {
addFileToHandler(id, record.blob);
fileList.push({id: id, file: relatedBlob}); fileList.push({id: id, file: record.blob});
}
else {
uploadData.setStatus(id, qq.status.REJECTED);
}
}); });
// If we are potentially uploading an original file and some scaled versions, // If we are potentially uploading an original file and some scaled versions,
@ -12453,8 +12512,8 @@ qq.Scaler = function(spec, log) {
qqparentsize: uploadData.retrieve({id: originalId}).size qqparentsize: uploadData.retrieve({id: originalId}).size
}; };
// Make SURE the UUID for each scaled image is sent with the upload request, // Make sure the UUID for each scaled image is sent with the upload request,
// to be consistent (since we need to ensure it is sent for the original file as well). // to be consistent (since we may need to ensure it is sent for the original file as well).
params[uuidParamName] = uploadData.retrieve({id: scaledId}).uuid; params[uuidParamName] = uploadData.retrieve({id: scaledId}).uuid;
uploadData.setParentId(scaledId, originalId); uploadData.setParentId(scaledId, originalId);
@ -14411,4 +14470,4 @@ code.google.com/p/crypto-js/wiki/License
C.HmacSHA1 = Hasher._createHmacHelper(SHA1); C.HmacSHA1 = Hasher._createHmacHelper(SHA1);
}()); }());
/*! 2015-06-09 */ /*! 2015-08-26 */

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property'; import Property from './ascribe_forms/property';
import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable'; import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import apiUrls from '../constants/api_urls'; import ApiUrls from '../constants/api_urls';
import { getLangText } from '../utils/lang_utils'; import { getLangText } from '../utils/lang_utils';
@ -59,7 +59,7 @@ let CoaVerifyForm = React.createClass({
return ( return (
<div> <div>
<Form <Form
url={apiUrls.coa_verify} url={ApiUrls.coa_verify}
handleSuccess={this.handleSuccess} handleSuccess={this.handleSuccess}
buttons={ buttons={
<button <button
@ -84,10 +84,11 @@ let CoaVerifyForm = React.createClass({
</Property> </Property>
<Property <Property
name='signature' name='signature'
label="Signature"> label="Signature"
editable={true}
overrideForm={true}>
<InputTextAreaToggable <InputTextAreaToggable
rows={3} rows={3}
editable={true}
placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')} placeholder={getLangText('Copy paste the signature on the bottom of your Certificate of Authenticity')}
required/> required/>
</Property> </Property>

Some files were not shown because too many files have changed in this diff Show More