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:
commit
1c48b8b828
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
224
.scss-lint.yml
Normal 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
|
22
README.md
22
README.md
|
@ -24,13 +24,16 @@ gulp serve
|
||||||
Additionally, to work on the white labeling functionality, you need to edit your `/etc/hosts` file and add:
|
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
63
docs/feature_list.md
Normal 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))
|
|
@ -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
|
||||||
|
|
28
gulpfile.js
28
gulpfile.js
|
@ -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'))
|
||||||
|
|
113
js/actions/contract_agreement_list_actions.js
Normal file
113
js/actions/contract_agreement_list_actions.js
Normal 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);
|
58
js/actions/contract_list_actions.js
Normal file
58
js/actions/contract_list_actions.js
Normal 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);
|
|
@ -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);
|
|
68
js/actions/notification_actions.js
Normal file
68
js/actions/notification_actions.js
Normal 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);
|
38
js/actions/ownership_actions.js
Normal file
38
js/actions/ownership_actions.js
Normal 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);
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
10
js/app.js
10
js/app.js
|
@ -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')
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
|
@ -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'),
|
||||||
|
|
|
@ -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;
|
|
@ -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 />
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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')},
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
97
js/components/ascribe_detail/further_details_fileuploader.js
Normal file
97
js/components/ascribe_detail/further_details_fileuploader.js
Normal 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;
|
33
js/components/ascribe_detail/history_iterator.js
Normal file
33
js/components/ascribe_detail/history_iterator.js
Normal 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;
|
31
js/components/ascribe_detail/license_detail.js
Normal file
31
js/components/ascribe_detail/license_detail.js
Normal 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;
|
|
@ -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>
|
||||||
}/>
|
}/>
|
||||||
|
|
65
js/components/ascribe_detail/note.js
Normal file
65
js/components/ascribe_detail/note.js
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
149
js/components/ascribe_forms/form_contract_agreement.js
Normal file
149
js/components/ascribe_forms/form_contract_agreement.js
Normal 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;
|
80
js/components/ascribe_forms/form_copyright_association.js
Normal file
80
js/components/ascribe_forms/form_copyright_association.js
Normal 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;
|
111
js/components/ascribe_forms/form_create_contract.js
Normal file
111
js/components/ascribe_forms/form_create_contract.js
Normal 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;
|
|
@ -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')}?</p>
|
<p>{getLangText('Are you sure you would like to permanently delete this edition')}?</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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')}?</p>
|
<p>{getLangText('Are you sure you would like to permanently delete this piece')}?</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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')}
|
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')}
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
79
js/components/ascribe_forms/form_loan_request_answer.js
Normal file
79
js/components/ascribe_forms/form_loan_request_answer.js
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
||||||
|
|
|
@ -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')}?</p>
|
<p>{getLangText('Are you sure you would like to remove these editions from your collection')}?</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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')}?</p>
|
<p>{getLangText('Are you sure you would like to remove this piece from your collection')}?</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', '.')} </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()}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>)
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
114
js/components/ascribe_forms/input_fineuploader.js
Normal file
114
js/components/ascribe_forms/input_fineuploader.js
Normal 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;
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
36
js/components/ascribe_forms/list_form_request_actions.js
Normal file
36
js/components/ascribe_forms/list_form_request_actions.js
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
106
js/components/ascribe_settings/account_settings.js
Normal file
106
js/components/ascribe_settings/account_settings.js
Normal 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;
|
123
js/components/ascribe_settings/api_settings.js
Normal file
123
js/components/ascribe_settings/api_settings.js
Normal 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 <url> -H 'Authorization: Bearer <token>'
|
||||||
|
</pre>
|
||||||
|
{this.getApplications()}
|
||||||
|
</CollapsibleParagraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default APISettings;
|
71
js/components/ascribe_settings/bitcoin_wallet_settings.js
Normal file
71
js/components/ascribe_settings/bitcoin_wallet_settings.js
Normal 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;
|
186
js/components/ascribe_settings/contract_settings.js
Normal file
186
js/components/ascribe_settings/contract_settings.js
Normal 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;
|
|
@ -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;
|
84
js/components/ascribe_settings/settings_container.js
Normal file
84
js/components/ascribe_settings/settings_container.js
Normal 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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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"
|
|
@ -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')}
|
|
@ -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>
|
||||||
);
|
);
|
|
@ -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;
|
|
@ -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>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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(', ');
|
||||||
|
}
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue
Block a user