1
0
mirror of https://github.com/ascribe/onion.git synced 2024-06-28 08:37:59 +02:00

Merge branch 'master' into AD-1177-display-404-on-pieces-and-editio

Conflicts:
	js/components/ascribe_detail/edition_container.js
	js/components/ascribe_detail/piece_container.js
This commit is contained in:
Tim Daubenschütz 2015-11-30 15:00:39 +01:00
commit 5a6c827f0b
220 changed files with 6456 additions and 2454 deletions

View File

@ -22,7 +22,7 @@
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-mount-set-state": [1, "allow-in-func"],
"react/no-did-update-set-state": 1,
"react/no-multi-comp": 0,
"react/no-unknown-property": 1,
@ -58,4 +58,4 @@
"superInFunctions": 1,
"templateStrings": 1
}
}
}

View File

@ -14,7 +14,7 @@ Install some nice extension for Chrom(e|ium):
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
```bash
git clone git@bitbucket.org:ascribe/onion.git
git clone git@github.com:ascribe/onion.git
cd onion
npm install
sudo npm install -g gulp
@ -29,6 +29,8 @@ Additionally, to work on the white labeling functionality, you need to edit your
127.0.0.1 cyland.localhost.com
127.0.0.1 ikonotv.localhost.com
127.0.0.1 sluice.localhost.com
127.0.0.1 lumenus.localhost.com
127.0.0.1 portfolioreview.localhost.com
```
@ -41,7 +43,25 @@ For this project, we're using:
* We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding))
* 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 don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways
Branch names
=====================
Since we moved to Github, we cannot create branch names automatically with JIRA anymore.
To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion.
```
AD-<JIRA-ticket-id>-brief-and-sane-description-of-the-ticket
```
where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title.
This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind.
Example
-------------
**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load <more useless information>`
**Github branch name:** `AD-1242-caching-solution-for-stores`
SCSS Code Conventions
=====================

View File

@ -8,6 +8,9 @@
queryParams of the piece_list_store should all be reflected in the url and not a single component each should manipulate the URL bar (refactor pagination, use actions and state)
- Refactor string-templating for api_urls
- Use classNames plugin instead of if-conditional-classes
- Instead of using `currentUser && currentUser.email` in an validation that checks whether we user is logged in or now, in the `UserStore` on login we set a boolean property called `isLoggedIn` that can then be used instead of `email`
- Refactor AclProxy to be a generic hide/show element component. Have it take data input and a validation function to assess whether it should show or hide child elements. Move current Acl checks to another place, eg. acl_utils.js.
- Convert all fetchers to [alt.js's sources](http://alt.js.org/docs/async/)
# Refactor DONE
- Refactor forms to generic-declarative form component ✓

BIN
fonts/ascribe-logo.eot Normal file

Binary file not shown.

19
fonts/ascribe-logo.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="ascribe-logo" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe800;" glyph-name="add" d="M1024 482.684h-477.316v477.316h-69.368v-477.316h-477.316v-69.368h477.316v-477.316h69.368v477.316h477.316z" />
<glyph unicode="&#xe801;" glyph-name="sort" d="M429.733 809.882l-139.567 150.118-139.6-150.118 25-23.283 97.5 104.887v-955.486h34.133v955.553l97.533-104.954zM848.433 109.401l-97.5-104.887v955.486h-34.133v-955.553l-97.533 104.954-25-23.283 139.567-150.118 139.6 150.118z" />
<glyph unicode="&#xe802;" glyph-name="search" d="M1021.1-36.711l-305.914 305.583c67.615 73.406 108.95 171.391 108.95 279.060 0 227.579-184.49 412.068-412.068 412.068s-412.068-184.489-412.068-412.068c0-227.578 184.489-412.068 412.068-412.068 107.625 0 205.576 41.3 278.972 108.866l305.927-305.597 24.133 24.156zM412.068 169.997c-208.394 0-377.935 169.541-377.935 377.935s169.541 377.935 377.935 377.935 377.935-169.54 377.935-377.935c0-208.394-169.541-377.935-377.935-377.935z" />
<glyph unicode="&#xe803;" glyph-name="filter" d="M0 960l384.89-534.756 8.722-489.244 184.119 174.521 0.324 314.724 445.947 534.756h-1024zM551.839 447.106c-5.109-6.126-7.91-13.849-7.919-21.826l-0.308-300.068-117.253-111.141-7.341 411.782c-0.124 6.948-2.365 13.692-6.424 19.331l-345.97 480.683h884.467l-399.251-478.761z" />
<glyph unicode="&#xe804;" glyph-name="add-white" d="M510.103 923.822c263.415 0 477.719-214.304 477.719-477.719s-214.303-477.718-477.719-477.718c-263.415 0-477.718 214.304-477.718 477.718s214.303 477.719 477.718 477.719zM510.103 957.955c-282.688 0-511.851-229.164-511.851-511.852s229.163-511.851 511.851-511.851 511.852 229.164 511.852 511.851-229.164 511.852-511.852 511.852v0zM796.444 459.378h-273.067v273.067h-34.133v-273.067h-261.689v-34.133h261.689v-261.689h34.133v261.689h273.067z" />
<glyph unicode="&#xe805;" d="M512.148 959.852c-282.688 0-511.851-229.164-511.851-511.852s229.164-511.851 511.851-511.851 511.852 229.163 511.852 511.851-229.164 511.852-511.852 511.852z" />
<glyph unicode="&#xe806;" d="M796.444 459.378h-273.067v273.067h-34.133v-273.067h-261.689v-34.133h261.689v-261.689h34.133v261.689h273.067z" />
<glyph unicode="&#xe807;" glyph-name="icon" d="M550.306 782.458h-75.373l-249.184-613.64h90.453l62.951 159.627h262.477l62.974-159.627h95.755l-250.053 613.64zM403.098 400.255l107.305 274.897 107.28-274.897h-214.586zM1024 448c0 286.204-225.796 512-511.999 512s-512.001-225.796-512.001-512c0-286.204 225.797-512 512.001-512s511.999 225.797 511.999 512v0zM962.165 448c0-245.94-204.249-450.164-450.164-450.164-245.941 0-450.161 204.224-450.161 450.164s204.221 450.164 450.161 450.164c245.915 0 450.164-204.224 450.164-450.164v0z" />
<glyph unicode="&#xe808;" glyph-name="logo" horiz-adv-x="4195" d="M499.718 326.19c0 109.528-24.641 157.448-61.607 198.517-38.336 41.077-95.832 71.191-191.673 71.191-95.832 0-171.135-36.957-212.212-64.34l27.382-52.031c13.695 10.954 88.998 54.764 187.571 54.764 99.943 0 177.978-57.505 177.978-173.876v-34.225l-173.876-6.843c-171.135-6.852-253.281-82.146-253.281-191.673s88.989-191.674 212.212-191.674c123.214 0 191.674 75.294 214.944 102.676v-88.989h72.562v376.503zM427.156 113.978c-30.114-47.92-98.573-116.371-201.258-116.371-102.676 0-154.707 61.607-154.707 130.066 0 68.45 42.448 125.955 175.246 132.798l180.719 10.955v-157.448zM1063.784 123.562c0 120.482-119.12 161.551-198.525 188.933-78.035 27.382-146.494 56.134-146.494 121.853s50.661 101.314 121.853 101.314c71.191 0 115.001-24.641 158.819-72.561l43.809 43.809c-49.291 56.134-106.795 88.989-199.887 88.989-93.1 0-193.044-54.764-193.044-169.765s119.112-156.078 173.876-175.246c54.764-19.168 168.394-47.92 168.394-130.066s-69.821-120.482-149.226-120.482c-79.413 0-142.391 39.707-183.46 99.952l-50.661-41.077c39.707-65.718 113.639-123.215 231.38-123.215s223.166 68.451 223.166 187.562v0zM1679.873 93.44c0 0-68.451-93.1-212.212-93.1-143.753 0-242.326 113.639-242.326 271.087s112.268 260.132 239.594 260.132c125.955 0 180.719-61.616 208.101-93.1l45.18 47.912c-13.687 17.798-82.146 109.528-250.548 109.528-167.024 0-317.628-132.798-317.628-328.583 0-194.406 139.65-331.315 310.785-331.315s236.853 82.146 260.133 109.528l-41.077 47.912zM2142.607 586.323c0 0-27.382 9.576-68.45 9.576-68.459 0-138.28-43.81-161.551-130.058v119.112h-71.2v-635.266h71.2v342.278c0 109.528 34.225 162.921 45.18 177.978 10.946 15.066 47.912 57.505 109.528 57.505 34.225 0 53.394-5.473 68.451-10.955l6.843 69.83zM2353.432 810.851c0 32.855-26.012 58.875-58.867 58.875-32.863 0-58.875-26.020-58.875-58.875s26.011-58.875 58.875-58.875c32.855 0 58.867 26.020 58.867 58.875v0zM2330.161 584.953h-71.191v-635.266h71.191v635.266zM3144.767 267.315c0 188.941-123.223 328.583-312.155 328.583-91.73 0-177.987-36.957-239.594-132.798v431.267h-71.191v-944.68h71.191v120.482c53.394-93.1 141.012-134.169 236.853-134.169 191.673 0 314.896 143.753 314.896 331.315v0zM3069.464 267.315c0-146.494-86.257-266.976-239.594-266.976-154.707 0-239.594 120.482-239.594 266.976 0 147.864 84.887 266.976 239.594 266.976 153.337 0 239.594-119.112 239.594-266.976v0zM3836.158 298.808c0 171.135-120.482 297.090-287.514 297.090-168.402 0-308.044-132.798-308.044-328.583 0-194.406 119.112-331.315 303.933-331.315 184.83 0 264.244 95.833 264.244 95.833l-34.234 50.661c0 0-79.405-82.154-223.158-82.154-143.761 0-228.64 99.952-235.491 250.548h516.154c0 0 4.111 27.382 4.111 47.92v0zM3318.633 312.495c4.111 88.998 68.459 221.796 225.899 221.796 157.449 0 219.065-139.65 219.065-221.796h-444.963zM4027.755 897.194h-26.663l-88.15-217.077h31.998l22.269 56.468h92.852l22.277-56.468h33.874l-88.457 217.077zM3975.68 761.989l37.96 97.245 37.951-97.245h-75.91zM4195.326 778.878c0 101.246-79.876 181.122-181.121 181.122s-181.122-79.876-181.122-181.122c0-101.245 79.876-181.122 181.122-181.122s181.121 79.876 181.121 181.122v0zM4173.452 778.878c0-87.002-72.254-159.247-159.247-159.247-87.002 0-159.246 72.245-159.246 159.247s72.244 159.247 159.246 159.247c86.993 0 159.247-72.245 159.247-159.247v0z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
fonts/ascribe-logo.ttf Normal file

Binary file not shown.

BIN
fonts/ascribe-logo.woff Normal file

Binary file not shown.

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="595.28px" height="419.53px" viewBox="0 0 595.28 419.53" enable-background="new 0 0 595.28 419.53" xml:space="preserve">
<g>
<path d="M362.878,195.61c-1.647-6.692-4.142-13.088-7.412-19.008c-3.401-6.166-7.549-11.797-12.319-16.725
c-4.951-5.127-10.511-9.553-16.519-13.149c-6.202-3.728-12.833-6.607-19.722-8.564c-7.088-2.003-14.352-3.075-21.602-3.188
c-7.417-0.083-14.843,0.769-22.059,2.592c-4.731,1.212-9.302,2.843-13.703,4.791l7.269,14.875
c3.387-1.463,6.895-2.699,10.517-3.627c5.788-1.463,11.74-2.16,17.735-2.082c5.8,0.09,11.635,0.954,17.334,2.562
c5.471,1.554,10.754,3.852,15.717,6.833c4.77,2.856,9.184,6.372,13.131,10.46c3.762,3.886,7.035,8.333,9.73,13.219
c2.57,4.651,4.53,9.687,5.832,14.971c1.237,5.021,1.809,10.269,1.693,15.6c-0.105,5.053-0.9,10.154-2.362,15.145
c-1.409,4.783-3.46,9.395-6.094,13.703c-2.521,4.121-5.625,7.939-9.224,11.348c-3.437,3.256-7.326,6.064-11.562,8.352
c-4.053,2.188-8.428,3.838-13.016,4.914c-4.344,1.023-8.87,1.465-13.477,1.32c-4.341-0.141-8.718-0.875-12.994-2.176
c-4.068-1.244-7.992-3.043-11.661-5.348c-3.473-2.182-6.704-4.871-9.592-7.98c-2.712-2.932-5.052-6.266-6.955-9.906
c-1.799-3.445-3.146-7.162-4.008-11.059c-0.815-3.658-1.137-7.471-0.957-11.338c0.167-3.615,0.837-7.264,1.997-10.867
c1.049-3.288,2.634-6.606,4.587-9.602c1.839-2.828,4.165-5.532,6.732-7.829c2.425-2.172,5.2-4.05,8.23-5.574
c2.81-1.405,5.947-2.48,9.076-3.11c2.926-0.591,6.12-0.8,9.219-0.598c2.85,0.188,5.858,0.804,8.694,1.777
c2.586,0.892,5.217,2.22,7.612,3.845c2.159,1.464,4.247,3.347,6.052,5.468c1.635,1.903,3.084,4.162,4.205,6.548
c1.023,2.166,1.791,4.617,2.225,7.114c0.392,2.204,0.48,4.645,0.258,7.045c-0.201,2.135-0.735,4.404-1.526,6.51
c-0.75,1.959-1.806,3.893-3.056,5.598c-1.149,1.572-2.592,3.064-4.174,4.32c-1.398,1.107-3.125,2.129-4.843,2.861
c-1.534,0.648-3.352,1.137-5.082,1.369c-1.563,0.199-3.315,0.184-4.947-0.047c-1.428-0.201-2.979-0.645-4.357-1.248
c-1.193-0.523-2.489-1.326-3.561-2.209c-0.942-0.771-1.886-1.797-2.605-2.832c-0.609-0.871-1.173-1.992-1.563-3.123
c-0.299-0.855-0.512-1.979-0.568-3.033c-0.045-0.795,0.052-1.85,0.234-2.666c0.16-0.684,0.506-1.557,0.893-2.246
c0.262-0.471,0.75-1.094,1.267-1.594c0.28-0.279,0.826-0.666,1.228-0.875c0.257-0.135,0.845-0.338,1.163-0.402
c0.182-0.035,0.657-0.037,0.931-0.008c4.538,0.547,8.644-2.734,9.167-7.275c0.521-4.541-2.736-8.646-7.277-9.168
c-0.898-0.103-3.262-0.293-5.914,0.191c-1.95,0.389-4.019,1.104-5.693,1.97c-1.114,0.58-3.28,1.848-5.187,3.743
c-1.614,1.559-3.085,3.463-4.134,5.345c-1.135,2.027-2.045,4.35-2.577,6.625c-0.535,2.393-0.756,4.984-0.627,7.271
c0.099,1.828,0.434,4.629,1.461,7.555c0.879,2.555,2.137,5.037,3.623,7.16c1.583,2.275,3.546,4.402,5.667,6.146
c2.256,1.855,4.823,3.439,7.435,4.586c2.768,1.209,5.775,2.064,8.678,2.473c3.095,0.443,6.339,0.465,9.418,0.068
c3.234-0.432,6.479-1.309,9.398-2.545c3.105-1.326,6.101-3.1,8.656-5.127c2.727-2.164,5.233-4.762,7.242-7.508
c2.105-2.873,3.891-6.145,5.178-9.51c1.316-3.506,2.19-7.246,2.53-10.838c0.363-3.928,0.214-7.788-0.437-11.451
c-0.68-3.911-1.91-7.825-3.553-11.298c-1.761-3.754-3.986-7.211-6.595-10.25c-2.778-3.264-5.925-6.097-9.351-8.415
c-3.629-2.464-7.502-4.413-11.51-5.795c-4.27-1.468-8.64-2.356-13.008-2.646c-4.605-0.289-9.173,0.005-13.562,0.892
c-4.606,0.927-9.049,2.45-13.225,4.54c-4.354,2.188-8.342,4.896-11.849,8.036c-3.687,3.298-6.906,7.043-9.564,11.13
c-2.787,4.276-4.968,8.853-6.477,13.58c-1.604,4.977-2.535,10.077-2.771,15.155c-0.247,5.332,0.203,10.613,1.333,15.689
c1.174,5.307,3.022,10.398,5.496,15.137c2.583,4.941,5.772,9.477,9.487,13.492c3.88,4.18,8.228,7.793,12.928,10.746
c4.905,3.08,10.16,5.488,15.634,7.164c5.678,1.727,11.493,2.695,17.286,2.887c0.755,0.023,1.51,0.035,2.263,0.035
c5.287,0,10.509-0.602,15.541-1.789c5.996-1.406,11.744-3.578,17.084-6.457c5.519-2.977,10.595-6.646,15.089-10.902
c4.653-4.41,8.678-9.365,11.959-14.727c3.39-5.545,6.03-11.486,7.856-17.676c1.873-6.408,2.892-12.957,3.026-19.457
C365.195,208.741,364.465,202.041,362.878,195.61"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -111,7 +111,11 @@ gulp.task('sass:build', function () {
]
}).on('error', sass.logError))
.pipe(gulpif(!argv.production, sourcemaps.write('./maps')))
.pipe(gulpif(argv.production, minifyCss()))
// We need to set `advanced` to false, as it merges
// some of the styles wrongly
.pipe(gulpif(argv.production, minifyCss({
advanced: false
})))
.pipe(gulp.dest('./build/css'))
.pipe(browserSync.stream());
});

View File

@ -2,33 +2,14 @@
<html>
<head>
<link rel="apple-touch-icon" sizes="57x57" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= BASE_URL %>static/img/hq-favicons/apple-touch-icon-152x152.png">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/img/hq-favicons/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="<%= BASE_URL %>static/img/hq-favicons/manifest.json">
<link rel="shortcut icon" href="<%= BASE_URL %>static/img/hq-favicons/favicon.ico">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="msapplication-TileImage" content="<%= BASE_URL %>static/img/hq-favicons/mstile-144x144.png">
<meta name="msapplication-config" content="<%= BASE_URL %>static/img/hq-favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#48DACB">
<meta name="theme-color" content="#D3DEE4">
<title>ascribe</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//brick.a.ssl.fastly.net/Source+Sans+Pro:400,600,700,900">
<link rel="stylesheet" href="<%= BASE_URL %>static/css/main.css">
<% DEBUG && print('<link rel="stylesheet" href="' + BASE_URL + 'static/css/maps/main.css.map">') %>
@ -41,6 +22,12 @@
<% DEBUG && print('window.DEBUG = true'); %>
<% DEBUG && print('window.CREDENTIALS = \'' + CREDENTIALS + '\''); %>
</script>
<!-- Typekit gibson font -->
<script src="https://use.typekit.net/gma2yhj.js"></script>
<script>
try {Typekit.load({ async: true });}
catch(e){}
</script>
</head>
<body>
<div id="main"></div>

View File

@ -1,37 +1,18 @@
'use strict';
import { altUser } from '../alt';
import UserFetcher from '../fetchers/user_fetcher';
class UserActions {
constructor() {
this.generateActions(
'updateCurrentUser',
'deleteCurrentUser'
'fetchCurrentUser',
'successFetchCurrentUser',
'logoutCurrentUser',
'successLogoutCurrentUser',
'errorCurrentUser'
);
}
fetchCurrentUser() {
UserFetcher.fetchOne()
.then((res) => {
this.actions.updateCurrentUser(res.users[0]);
})
.catch((err) => {
console.logGlobal(err);
this.actions.updateCurrentUser({});
});
}
logoutCurrentUser() {
UserFetcher.logout()
.then(() => {
this.actions.deleteCurrentUser();
})
.catch((err) => {
console.logGlobal(err);
});
}
}
export default altUser.createActions(UserActions);

View File

@ -0,0 +1,19 @@
'use strict';
import { alt } from '../alt';
class WebhookActions {
constructor() {
this.generateActions(
'fetchWebhooks',
'successFetchWebhooks',
'fetchWebhookEvents',
'successFetchWebhookEvents',
'removeWebhook',
'successRemoveWebhook'
);
}
}
export default alt.createActions(WebhookActions);

View File

@ -1,29 +1,16 @@
'use strict';
import { altWhitelabel } from '../alt';
import WhitelabelFetcher from '../fetchers/whitelabel_fetcher';
class WhitelabelActions {
constructor() {
this.generateActions(
'updateWhitelabel'
'fetchWhitelabel',
'successFetchWhitelabel',
'errorWhitelabel'
);
}
fetchWhitelabel() {
WhitelabelFetcher.fetch()
.then((res) => {
if(res && res.whitelabel) {
this.actions.updateWhitelabel(res.whitelabel);
} else {
this.actions.updateWhitelabel({});
}
})
.catch((err) => {
console.logGlobal(err);
});
}
}
export default altWhitelabel.createActions(WhitelabelActions);

View File

@ -30,6 +30,7 @@ import GoogleAnalyticsHandler from './third_party/ga';
import RavenHandler from './third_party/raven';
import IntercomHandler from './third_party/intercom';
import NotificationsHandler from './third_party/notifications';
import FacebookHandler from './third_party/facebook';
/* eslint-enable */
initLogging();

View File

@ -35,7 +35,7 @@ let AccordionList = React.createClass({
{getLangText('We could not find any works related to you...')}
</p>
<p className="text-center">
{getLangText('To register one, click')}
{getLangText('To register one, click')}&nbsp;
<a href="register_piece">{getLangText('here')}</a>!
</p>
</div>

View File

@ -1,6 +1,7 @@
'use strict';
import React from 'react';
import { Link } from 'react-router';
let AccordionListItem = React.createClass({
@ -12,6 +13,7 @@ let AccordionListItem = React.createClass({
subheading: React.PropTypes.object,
subsubheading: React.PropTypes.object,
buttons: React.PropTypes.object,
linkData: React.PropTypes.string,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
@ -19,29 +21,49 @@ let AccordionListItem = React.createClass({
},
render() {
const { linkData,
className,
thumbnail,
heading,
subheading,
subsubheading,
buttons,
badge,
children } = this.props;
return (
<div className="row">
<div className={this.props.className}>
<div className={className}>
<div className="wrapper">
<div className="col-xs-4 col-sm-3 col-md-2 col-lg-2 clear-paddings">
<div className="thumbnail-wrapper">
{this.props.thumbnail}
<div className="pull-left">
<Link to={linkData}>
<div className="thumbnail-wrapper">
{thumbnail}
</div>
</Link>
</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">
{this.props.heading}
{this.props.subheading}
{this.props.subsubheading}
{this.props.buttons}
</div>
<div className="accordion-list-item-header">
<Link to={linkData}>
{heading}
</Link>
<Link to={linkData}>
{subheading}
{subsubheading}
</Link>
<div className="accordion-list-item-buttons">
{buttons}
</div>
</div>
<span style={{'clear': 'both'}}></span>
<div className="request-action-badge">
{this.props.badge}
{badge}
</div>
</div>
</div>
{this.props.children}
{children}
</div>
);
}

View File

@ -11,6 +11,7 @@ import PieceListStore from '../../stores/piece_list_store';
import Button from 'react-bootstrap/lib/Button';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
@ -75,7 +76,10 @@ let AccordionListItemEditionWidget = React.createClass({
// PLEASE FUTURE TIM, DO NOT FUCKING REMOVE IT AGAIN!
if(typeof this.state.editionList[pieceId] === 'undefined') {
return (
<span className="glyph-ascribe-spool-chunked ascribe-color spin"/>
<AscribeSpinner
size='sm'
color='white'
classNames='pull-right margin-left-2px'/>
);
} else {
return (
@ -98,7 +102,7 @@ let AccordionListItemEditionWidget = React.createClass({
return (
<CreateEditionsButton
label={getLangText('Create editions')}
className="btn-xs pull-right"
className="btn-secondary btn-sm pull-right"
piece={piece}
toggleCreateEditionsDialog={this.props.toggleCreateEditionsDialog}
onPollingSuccess={this.props.onPollingSuccess}/>
@ -112,20 +116,19 @@ let AccordionListItemEditionWidget = React.createClass({
if(piece.first_edition === null) {
// user has deleted all his editions and only the piece is showing
return (
<Button
<button
disabled
title={getLangText('All editions for this have been deleted already.')}
className={classNames('btn', 'btn-default', 'btn-xs', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
className={classNames('btn', 'btn-default', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
{'0 ' + getLangText('Editions')}
</Button>
</button>
);
} else {
let editionMapping = piece && piece.first_edition ? piece.first_edition.num_editions_available + '/' + piece.num_editions : '';
return (
<button
onClick={this.toggleTable}
className={classNames('btn', 'btn-default', 'btn-xs', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
className={classNames('btn', 'btn-secondary', 'btn-sm', 'ascribe-accordion-list-item-edition-widget', this.props.className)}>
{editionMapping + ' ' + getLangText('Editions')} {this.getGlyphicon()}
</button>
);

View File

@ -34,28 +34,42 @@ let AccordionListItemPiece = React.createClass({
},
render() {
const { className, piece, artistName, buttons, badge, children, subsubheading } = this.props;
const { url, url_safe } = piece.thumbnail;
let thumbnail;
// Since we're going to refactor the thumbnail generation anyway at one point,
// for not use the annoying ascribe_spiral.png, we're matching the url against
// this name and replace it with a CSS version of the new logo.
if(url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) {
thumbnail = (
<span className="ascribe-logo-circle">
<span>A</span>
</span>
);
} else {
thumbnail = (
<div style={{backgroundImage: 'url("' + url_safe + '")'}}/>
);
}
return (
<AccordionListItem
className={this.props.className}
thumbnail={
<Link to={this.getLinkData()}>
<img src={this.props.piece.thumbnail.url_safe}/>
</Link>}
heading={
<Link to={this.getLinkData()}>
<h1>{this.props.piece.title}</h1>
</Link>}
className={className}
thumbnail={thumbnail}
heading={<h1>{piece.title}</h1>}
subheading={
<h3>
{getLangText('by ')}
{this.props.artistName ? this.props.artistName : this.props.piece.artist_name}
{artistName ? artistName : piece.artist_name}
</h3>
}
subsubheading={this.props.subsubheading}
buttons={this.props.buttons}
badge={this.props.badge}
subsubheading={subsubheading}
buttons={buttons}
badge={badge}
linkData={this.getLinkData()}
>
{this.props.children}
{children}
</AccordionListItem>
);
}

View File

@ -14,10 +14,12 @@ import { ColumnModel, TransitionModel } from '../ascribe_table/models/table_mode
import TableItemText from '../ascribe_table/table_item_text';
import TableItemCheckbox from '../ascribe_table/table_item_checkbox';
import TableItemAclFiltered from '../ascribe_table/table_item_acl_filtered';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
let AccordionListItemTableEditions = React.createClass({
propTypes: {
@ -88,7 +90,7 @@ let AccordionListItemTableEditions = React.createClass({
let showExpandOption = false;
let editionsForPiece = this.state.editionList[this.props.parentId];
let loadingSpinner = <span className="glyph-ascribe-spool-chunked ascribe-color spin"/>;
let loadingSpinner = <AscribeSpinner size="sm" color="dark-blue" />;
// here we need to check if all editions of a specific
// piece are already defined. Otherwise .length will throw an error and we'll not

View File

@ -1,6 +1,7 @@
'use strict';
import React from 'react';
import Moment from 'moment';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
@ -129,7 +130,7 @@ let AccordionListItemWallet = React.createClass({
piece={this.props.content}
subsubheading={
<div className="pull-left">
<span>{this.props.content.date_created.split('-')[0]}</span>
<span>{Moment(this.props.content.date_created, 'YYYY-MM-DD').year()}</span>
{this.getLicences()}
</div>}
buttons={
@ -144,7 +145,7 @@ let AccordionListItemWallet = React.createClass({
onPollingSuccess={this.onPollingSuccess}/>
</AclProxy>
</div>}
badge={this.getGlyphicon()}>
badge={this.getGlyphicon()}>
{this.getCreateEditionsDialog()}
{/* this.props.children is AccordionListItemTableEditions */}
{this.props.children}

View File

@ -23,7 +23,9 @@ let AscribeApp = React.createClass({
<div className="container ascribe-default-app">
<Header routes={routes} />
{/* Routes are injected here */}
{children}
<div className="ascribe-body">
{children}
</div>
<Footer />
<GlobalNotification />
<div id="modal" className="container"></div>

View File

@ -1,186 +0,0 @@
'use strict';
import React from 'react';
import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan';
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
import ShareForm from '../ascribe_forms/form_share_email';
import ModalWrapper from '../ascribe_modal/modal_wrapper';
import AppConstants from '../../constants/application_constants';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import ApiUrls from '../../constants/api_urls';
import { getAclFormMessage } from '../../utils/form_utils';
import { getLangText } from '../../utils/lang_utils';
let AclButton = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
availableAcls: React.PropTypes.object.isRequired,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
currentUser: React.PropTypes.object,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
isPiece(){
return this.props.pieceOrEditions.constructor !== Array;
},
actionProperties(){
let message = getAclFormMessage(this.props.action, this.getTitlesString(), this.props.currentUser.username);
if (this.props.action === 'acl_consign'){
return {
title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork'),
form: (
<ConsignForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_consigns}/>
),
handleSuccess: this.showNotification
};
}
if (this.props.action === 'acl_unconsign'){
return {
title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again'),
form: (
<UnConsignForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_unconsigns}/>
),
handleSuccess: this.showNotification
};
}else if (this.props.action === 'acl_transfer') {
return {
title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork'),
form: (
<TransferForm
message={message}
id={this.getFormDataId()}
url={ApiUrls.ownership_transfers}/>
),
handleSuccess: this.showNotification
};
}
else if (this.props.action === 'acl_loan'){
return {
title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time'),
form: (<LoanForm
message={message}
id={this.getFormDataId()}
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
};
}
else if (this.props.action === 'acl_share'){
return {
title: getLangText('Share artwork'),
tooltip: getLangText('Share the artwork'),
form: (
<ShareForm
message={message}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_shares_pieces : ApiUrls.ownership_shares_editions }/>
),
handleSuccess: this.showNotification
};
} else {
throw new Error('Your specified action did not match a form.');
}
},
showNotification(response){
this.props.handleSuccess();
if(response.notification) {
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
// plz move to share form
getTitlesString(){
if (this.isPiece()){
return '\"' + this.props.pieceOrEditions.title + '\"';
}
else {
return this.props.pieceOrEditions.map(function(edition) {
return '- \"' + edition.title + ', ' + getLangText('edition') + ' ' + edition.edition_number + '\"\n';
}).join('');
}
},
getFormDataId(){
if (this.isPiece()) {
return {piece_id: this.props.pieceOrEditions.id};
}
else {
return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
},
// Removes the acl_ prefix and converts to upper case
sanitizeAction() {
if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName;
}
return this.props.action.split('acl_')[1].toUpperCase();
},
render() {
if (this.props.availableAcls){
let shouldDisplay = this.props.availableAcls[this.props.action];
let aclProps = this.actionProperties();
let buttonClassName = this.props.buttonAcceptClassName ? this.props.buttonAcceptClassName : '';
return (
<ModalWrapper
trigger={
<button className={shouldDisplay ? 'btn btn-default btn-sm ' + buttonClassName : 'hidden'}>
{this.sanitizeAction()}
</button>
}
handleSuccess={aclProps.handleSuccess}
title={aclProps.title}>
{aclProps.form}
</ModalWrapper>
);
}
return null;
}
});
export default AclButton;

View File

@ -1,21 +1,29 @@
'use strict';
import React from 'react';
import React from 'react/addons';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import AclButton from '../ascribe_buttons/acl_button';
import ConsignButton from './acls/consign_button';
import EmailButton from './acls/email_button';
import LoanButton from './acls/loan_button';
import LoanRequestButton from './acls/loan_request_button';
import TransferButton from './acls/transfer_button';
import UnconsignButton from './acls/unconsign_button';
import { mergeOptions } from '../../utils/general_utils';
let AclButtonList = React.createClass({
propTypes: {
className: React.PropTypes.string,
editions: React.PropTypes.oneOfType([
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]),
availableAcls: React.PropTypes.object,
handleSuccess: React.PropTypes.func,
]).isRequired,
availableAcls: React.PropTypes.object.isRequired,
buttonsStyle: React.PropTypes.object,
handleSuccess: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
@ -23,59 +31,95 @@ let AclButtonList = React.createClass({
},
getInitialState() {
return UserStore.getState();
return mergeOptions(
UserStore.getState(),
{
buttonListSize: 0
}
);
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
UserActions.fetchCurrentUser.defer();
window.addEventListener('resize', this.handleResize);
window.dispatchEvent(new Event('resize'));
},
componentDidUpdate(prevProps) {
if(prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) {
window.dispatchEvent(new Event('resize'));
}
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
window.removeEventListener('resize', this.handleResize);
},
handleResize() {
this.setState({
buttonListSize: this.refs.buttonList.getDOMNode().offsetWidth
});
},
onChange(state) {
this.setState(state);
},
renderChildren() {
const { children } = this.props;
const { buttonListSize } = this.state;
return React.Children.map(children, (child) => {
return React.addons.cloneWithProps(child, { buttonListSize });
});
},
render() {
const { className,
buttonsStyle,
availableAcls,
pieceOrEditions,
handleSuccess } = this.props;
const { currentUser } = this.state;
return (
<div className={this.props.className}>
<AclButton
availableAcls={this.props.availableAcls}
action="acl_transfer"
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess}/>
<AclButton
availableAcls={this.props.availableAcls}
action="acl_consign"
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="acl_unconsign"
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="acl_loan"
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
<AclButton
availableAcls={this.props.availableAcls}
action="acl_share"
pieceOrEditions={this.props.editions}
currentUser={this.state.currentUser}
handleSuccess={this.props.handleSuccess} />
{this.props.children}
<div className={className}>
<span ref="buttonList" style={buttonsStyle}>
<EmailButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
<TransferButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess}/>
<ConsignButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
<UnconsignButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
<LoanButton
availableAcls={availableAcls}
pieceOrEditions={pieceOrEditions}
currentUser={currentUser}
handleSuccess={handleSuccess} />
{this.renderChildren()}
</span>
</div>
);
}
});
export default AclButtonList;
export default AclButtonList;

View File

@ -0,0 +1,139 @@
'use strict';
import React from 'react';
import classnames from 'classnames';
import { AclInformationText } from '../../constants/acl_information_text';
import { replaceSubstringAtIndex, sanitize, intersectLists } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
let AclInformation = React.createClass({
propTypes: {
verbs: React.PropTypes.arrayOf(React.PropTypes.string),
aim: React.PropTypes.string.isRequired,
aclObject: React.PropTypes.object,
// Must be inserted from the outside
buttonListSize: React.PropTypes.number.isRequired
},
getDefaultProps() {
return {
buttonListSize: 400
};
},
getInitialState() {
return { isVisible: false };
},
onOff() {
if(!this.state.isVisible) {
this.setState({ isVisible: true });
}
else {
this.setState({ isVisible: false });
}
},
getInfoText(title, info, example){
const aim = this.props.aim;
if(aim) {
if(aim === 'form') {
return (
<p>
<span className="info">
{replaceSubstringAtIndex(info.slice(2), 's ', ' ')}
</span>
<span className="example">
{' ' + example}
</span>
</p>
);
}
else if(aim === 'button') {
return (
<p>
<span className="title">
{title}
</span>
<span className="info">
{info + ' '}
</span>
<span className="example">
{example}
</span>
</p>
);
}
}
else {
console.log('Aim is required when you want to place information text');
}
},
produceInformationBlock() {
const { titles, informationSentences, exampleSentences } = AclInformationText;
const { verbs, aim } = this.props;
const availableInformations = intersectLists(verbs, Object.keys(titles));
// sorting is not needed, as `this.props.verbs` takes care of sorting already
// So we assume a user of `AclInformationButton` puts an ordered version of
// `verbs` into `propTypes`
let verbsToDisplay = [];
if(aim === 'form' && availableInformations.length > 0) {
verbsToDisplay = verbsToDisplay.concat(verbs);
} else if(aim === 'button' && this.props.aclObject) {
const { aclObject } = this.props;
const sanitizedAclObject = sanitize(aclObject, (val) => !val);
verbsToDisplay = verbsToDisplay.concat(intersectLists(verbs, Object.keys(sanitizedAclObject)));
}
return verbsToDisplay.map((verb) => {
const title = titles[verb];
const informationSentence = informationSentences[verb];
const exampleSentence = exampleSentences[verb];
if (title && informationSentence && exampleSentence) {
return this.getInfoText(getLangText(title), getLangText(informationSentence), getLangText(exampleSentence));
}
});
},
getButton() {
return this.props.aim === 'button' ?
<button
style={{ marginTop: 0 }}
className="btn btn-transparent glyphicon glyphicon-question-sign" onClick={this.onOff} /> :
null;
},
render() {
const { aim, buttonListSize, verbs } = this.props;
const { isVisible } = this.state;
/* Lets just fucking get this widget out... */
const aclInformationSize = buttonListSize - 30;
return (
<span >
{this.getButton()}
<div
style={{
width: verbs.length > 1 && aclInformationSize > 300 ? aclInformationSize : verbs.length === 1 ? null : '100%',
marginLeft: verbs.length === 1 ? '.25em' : null
}}
className={classnames({'acl-information-dropdown-list': true, 'hidden': aim === 'button' && !isVisible})}>
<span>{this.produceInformationBlock()}</span>
</div>
</span>
);
}
});
export default AclInformation;

View File

@ -0,0 +1,79 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
import AclProxy from '../../acl_proxy';
import AclFormFactory from '../../ascribe_forms/acl_form_factory';
import ModalWrapper from '../../ascribe_modal/modal_wrapper';
import AppConstants from '../../../constants/application_constants';
import { AclInformationText } from '../../../constants/acl_information_text';
export default function ({ action, displayName, title, tooltip }) {
if (AppConstants.aclList.indexOf(action) < 0) {
console.warn('Your specified aclName did not match a an acl class.');
}
return React.createClass({
displayName: displayName,
propTypes: {
availableAcls: React.PropTypes.object.isRequired,
buttonAcceptName: React.PropTypes.string,
buttonAcceptClassName: React.PropTypes.string,
currentUser: React.PropTypes.object.isRequired,
email: React.PropTypes.string,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
handleSuccess: React.PropTypes.func.isRequired,
className: React.PropTypes.string
},
sanitizeAction() {
if (this.props.buttonAcceptName) {
return this.props.buttonAcceptName;
}
return AclInformationText.titles[action];
},
render() {
const {
availableAcls,
buttonAcceptClassName,
currentUser,
email,
pieceOrEditions,
handleSuccess } = this.props;
return (
<AclProxy
aclName={action}
aclObject={availableAcls}>
<ModalWrapper
trigger={
<button
className={classNames('btn', 'btn-default', 'btn-sm', buttonAcceptClassName)}>
{this.sanitizeAction()}
</button>
}
handleSuccess={handleSuccess}
title={title}>
<AclFormFactory
action={action}
currentUser={currentUser}
email={email}
pieceOrEditions={pieceOrEditions}
showNotification />
</ModalWrapper>
</AclProxy>
);
}
});
}

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_consign',
displayName: 'ConsignButton',
title: getLangText('Consign artwork'),
tooltip: getLangText('Have someone else sell the artwork')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_share',
displayName: 'EmailButton',
title: getLangText('Share artwork via email'),
tooltip: getLangText("Share the artwork to another user's collection through email")
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_loan',
displayName: 'LoanButton',
title: getLangText('Loan artwork'),
tooltip: getLangText('Loan your artwork for a limited period of time')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_loan_request',
displayName: 'LoanRequestButton',
title: getLangText('Loan artwork'),
tooltip: getLangText('Someone requested you to loan your artwork for a limited period of time')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_transfer',
displayName: 'TransferButton',
title: getLangText('Transfer artwork'),
tooltip: getLangText('Transfer the ownership of the artwork')
});

View File

@ -0,0 +1,14 @@
'use strict';
import React from 'react';
import AclButton from './acl_button';
import { getLangText } from '../../../utils/lang_utils';
export default AclButton({
action: 'acl_unconsign',
displayName: 'UnconsignButton',
title: getLangText('Unconsign artwork'),
tooltip: getLangText('Have the owner manage his sales again')
});

View File

@ -5,6 +5,8 @@ import React from 'react';
import EditionListActions from '../../actions/edition_list_actions';
import EditionListStore from '../../stores/edition_list_store';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import classNames from 'classnames';
@ -75,14 +77,17 @@ let CreateEditionsButton = React.createClass({
return (
<button
disabled
className={classNames('btn', 'btn-default', this.props.className)}>
{getLangText('Creating editions')} <span className="glyph-ascribe-spool-chunked spin"/>
className={classNames('btn', this.props.className)}>
{getLangText('Creating editions')} <AscribeSpinner
size='sm'
color='white'
classNames='pull-right margin-left-2px'/>
</button>
);
} else {
return (
<button
className={classNames('btn', 'btn-default', this.props.className)}
className={classNames('btn', this.props.className)}
onClick={this.props.toggleCreateEditionsDialog}>
{this.props.label}
</button>

View File

@ -39,13 +39,13 @@ let DeleteButton = React.createClass({
if(this.props.piece && !this.props.editions) {
content = <PieceDeleteForm pieceId={this.props.piece.id}/>;
title = getLangText('Remove Piece');
title = getLangText('Delete Piece');
} else {
content = <EditionDeleteForm editions={this.props.editions}/>;
title = getLangText('Remove Edition');
title = getLangText('Delete Edition');
}
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('DELETE')}</Button>;
btnDelete = <button className="btn btn-sm btn-tertiary">{getLangText('DELETE')}</button>;
} else if(availableAcls.acl_unshare){
@ -57,7 +57,7 @@ let DeleteButton = React.createClass({
title = getLangText('Remove Piece from Collection');
}
btnDelete = <Button bsStyle="danger" className="btn-delete" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>;
btnDelete = <Button bsStyle="default" bsSize="small">{getLangText('REMOVE FROM COLLECTION')}</Button>;
} else {
return null;

View File

@ -21,13 +21,13 @@ let CollapsibleButton = React.createClass({
this.setState({expanded: !this.state.expanded});
},
render() {
let isVisible = (this.state.expanded) ? '' : 'invisible';
let isHidden = (this.state.expanded) ? '' : 'hidden';
return (
<span>
<span onClick={this.handleToggle}>
{this.props.button}
</span>
<div ref='panel' className={isVisible}>
<div ref='panel' className={isHidden}>
{this.props.panel}
</div>
</span>

View File

@ -2,6 +2,7 @@
import React from 'react';
let DetailProperty = React.createClass({
propTypes: {
label: React.PropTypes.string,
@ -12,20 +13,29 @@ let DetailProperty = React.createClass({
separator: React.PropTypes.string,
labelClassName: React.PropTypes.string,
valueClassName: React.PropTypes.string,
ellipsis: React.PropTypes.bool
ellipsis: React.PropTypes.bool,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
getDefaultProps() {
return {
separator: '',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height col-bottom ascribe-detail-property-label',
labelClassName: 'col-xs-3 col-sm-3 col-md-2 col-lg-2 col-xs-height ascribe-detail-property-label',
valueClassName: 'col-xs-9 col-sm-9 col-md-10 col-lg-10 col-xs-height col-bottom ascribe-detail-property-value'
};
},
render() {
let value = this.props.value;
let styles = {};
const { labelClassName,
label,
separator,
valueClassName,
children,
value } = this.props;
if(this.props.ellipsis) {
styles = {
@ -35,30 +45,16 @@ let DetailProperty = React.createClass({
};
}
if (this.props.children){
value = (
<div className="row-same-height">
<div className="col-xs-6 col-xs-height col-bottom no-padding">
{ this.props.value }
</div>
<div
className="col-xs-6 col-xs-height"
style={styles}>
{ this.props.children }
</div>
</div>);
}
return (
<div className="row ascribe-detail-property">
<div className="row-same-height">
<div className={this.props.labelClassName}>
{ this.props.label } { this.props.separator}
<div className={labelClassName}>
{label} {separator}
</div>
<div
className={this.props.valueClassName}
className={valueClassName}
style={styles}>
{value}
{children || value}
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@
import React from 'react';
import { Link, History } from 'react-router';
import Moment from 'moment';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
@ -25,11 +26,12 @@ import LicenseDetail from './license_detail';
import FurtherDetails from './further_details';
import EditionActionPanel from './edition_action_panel';
import AclProxy from '../acl_proxy';
import Note from './note';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -40,8 +42,7 @@ import { getLangText } from '../../utils/lang_utils';
let Edition = React.createClass({
propTypes: {
edition: React.PropTypes.object,
loadEdition: React.PropTypes.func,
location: React.PropTypes.object
loadEdition: React.PropTypes.func
},
mixins: [History],
@ -81,10 +82,10 @@ let Edition = React.createClass({
</Col>
<Col md={6} className="ascribe-edition-details">
<div className="ascribe-detail-header">
<hr/>
<hr style={{marginTop: 0}}/>
<h1 className="ascribe-detail-title">{this.props.edition.title}</h1>
<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={Moment(this.props.edition.date_created, 'YYYY-MM-DD').year()} />
<hr/>
</div>
<EditionSummary
@ -144,7 +145,6 @@ let Edition = React.createClass({
url={ApiUrls.note_public_edition}
currentUser={this.state.currentUser}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Further Details')}
show={this.props.edition.acl.acl_edit
@ -155,10 +155,8 @@ let Edition = React.createClass({
pieceId={this.props.edition.parent}
extraData={this.props.edition.extra_data}
otherData={this.props.edition.other_data}
handleSuccess={this.props.loadEdition}
location={this.props.location}/>
handleSuccess={this.props.loadEdition} />
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('SPOOL Details')}>
<SpoolDetails
@ -212,10 +210,15 @@ let EditionSummary = React.createClass({
value={ edition.owner } />
<LicenseDetail license={edition.license_type}/>
{this.getStatus()}
<EditionActionPanel
edition={edition}
currentUser={currentUser}
handleSuccess={this.handleSuccess} />
<AclProxy show={currentUser && currentUser.email}>
<EditionDetailProperty
label={getLangText('ACTIONS')}>
<EditionActionPanel
edition={edition}
currentUser={currentUser}
handleSuccess={this.handleSuccess} />
</EditionDetailProperty>
</AclProxy>
<hr/>
</div>
);
@ -279,7 +282,7 @@ let CoaDetails = React.createClass({
}
return (
<div className="text-center">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import Router from 'react-router';
import { History } from 'react-router';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
@ -22,6 +22,8 @@ import DeleteButton from '../ascribe_buttons/delete_button';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AclInformation from '../ascribe_buttons/acl_information';
import AclProxy from '../acl_proxy';
import ApiUrls from '../../constants/api_urls';
@ -39,7 +41,7 @@ let EditionActionPanel = React.createClass({
handleSuccess: React.PropTypes.func
},
mixins: [Router.Navigation],
mixins: [History],
getInitialState() {
return PieceListStore.getState();
@ -66,7 +68,7 @@ let EditionActionPanel = React.createClass({
let notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
this.transitionTo('pieces');
this.history.pushState(null, '/collection');
},
refreshCollection() {
@ -103,9 +105,9 @@ let EditionActionPanel = React.createClass({
<Row>
<Col md={12}>
<AclButtonList
className="text-center ascribe-button-list"
className="ascribe-button-list"
availableAcls={edition.acl}
editions={[edition]}
pieceOrEditions={[edition]}
handleSuccess={this.handleSuccess}>
<AclProxy
aclObject={edition.acl}
@ -122,7 +124,7 @@ let EditionActionPanel = React.createClass({
type="text"
value={edition.bitcoin_id} />
</Property>
<Button bsStyle="danger" className="btn-delete pull-center" bsSize="small" type="submit">
<Button bsStyle="default" className="pull-center" bsSize="small" type="submit">
{getLangText('WITHDRAW TRANSFER')}
</Button>
</Form>
@ -158,6 +160,10 @@ let EditionActionPanel = React.createClass({
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
editions={[edition]}/>
<AclInformation
aim="button"
verbs={['acl_share', 'acl_transfer', 'acl_consign', 'acl_loan', 'acl_delete']}
aclObject={edition.acl}/>
</AclButtonList>
</Col>
</Row>
@ -166,4 +172,4 @@ let EditionActionPanel = React.createClass({
}
});
export default EditionActionPanel;
export default EditionActionPanel;

View File

@ -8,7 +8,9 @@ import EditionStore from '../../stores/edition_store';
import Edition from './edition';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
@ -16,7 +18,6 @@ import AppConstants from '../../constants/application_constants';
*/
let EditionContainer = React.createClass({
propTypes: {
location: React.PropTypes.object,
params: React.PropTypes.object
},
@ -34,7 +35,13 @@ let EditionContainer = React.createClass({
EditionActions.updateEdition({});
EditionStore.listen(this.onChange);
EditionActions.fetchOne(this.props.params.editionId);
// Every time we enter the edition detail page, just reset the edition
// store as it will otherwise display wrong/old data once the user loads
// the edition detail a second time
EditionActions.updateEdition({});
this.loadEdition();
},
// This is done to update the container when the user clicks on the prev or next
@ -78,17 +85,18 @@ let EditionContainer = React.createClass({
},
render() {
if(this.state.edition && this.state.edition.title) {
if(this.state.edition && this.state.edition.id) {
setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', '));
return (
<Edition
edition={this.state.edition}
loadEdition={this.loadEdition}
location={this.props.location}/>
loadEdition={this.loadEdition} />
);
} else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -9,7 +9,6 @@ import Form from './../ascribe_forms/form';
import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
@ -17,14 +16,14 @@ import FurtherDetailsFileuploader from './further_details_fileuploader';
import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils';
let FurtherDetails = React.createClass({
propTypes: {
editable: React.PropTypes.bool,
pieceId: React.PropTypes.number,
extraData: React.PropTypes.object,
otherData: React.PropTypes.arrayOf(React.PropTypes.object),
handleSuccess: React.PropTypes.func,
location: React.PropTypes.object
handleSuccess: React.PropTypes.func
},
getInitialState() {
@ -86,8 +85,7 @@ let FurtherDetails = React.createClass({
overrideForm={true}
pieceId={this.props.pieceId}
otherData={this.props.otherData}
multiple={true}
location={this.props.location}/>
multiple={true} />
</Form>
</Col>
</Row>

View File

@ -20,8 +20,7 @@ let FurtherDetailsFileuploader = React.createClass({
submitFile: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
editable: React.PropTypes.bool,
multiple: React.PropTypes.bool,
location: React.PropTypes.object
multiple: React.PropTypes.bool
},
getDefaultProps() {
@ -44,6 +43,7 @@ let FurtherDetailsFileuploader = React.createClass({
return (
<Property
name="other_data_key"
label="Additional files">
<ReactS3FineUploader
uploadStarted={this.props.uploadStarted}
@ -89,11 +89,10 @@ let FurtherDetailsFileuploader = React.createClass({
}}
areAssetsDownloadable={true}
areAssetsEditable={this.props.editable}
multiple={this.props.multiple}
location={this.props.location}/>
multiple={this.props.multiple} />
</Property>
);
}
});
export default FurtherDetailsFileuploader;
export default FurtherDetailsFileuploader;

View File

@ -5,11 +5,33 @@ import React from 'react';
import Form from '../ascribe_forms/form';
import Property from '../ascribe_forms/property';
import { replaceSubstringAtIndex } from '../../utils/general_utils';
let HistoryIterator = React.createClass({
propTypes: {
history: React.PropTypes.array
},
composeHistoryDescription(historicalEvent) {
if(historicalEvent.length === 3) {
// We want to get the capturing group without the quotes,
// which is why we access the match list at index 1 and not 0
const contractName = historicalEvent[1].match(/\"(.*)\"/)[1];
const historicalEventDescription = replaceSubstringAtIndex(historicalEvent[1], `"${contractName}"`, '');
return (
<span>
{historicalEventDescription}
<a href={historicalEvent[2]} target="_blank">{contractName}</a>
</span>
);
} else if(historicalEvent.length === 2) {
return historicalEvent[1];
} else {
throw new Error('Expected an historical event list with either 3 or 2 items. Got less or more.');
}
},
render() {
return (
<Form>
@ -20,7 +42,7 @@ let HistoryIterator = React.createClass({
key={i}
label={ historicalEvent[0] }
editable={false}>
<pre className="ascribe-pre">{ historicalEvent[1] }</pre>
<pre className="ascribe-pre">{this.composeHistoryDescription(historicalEvent)}</pre>
</Property>
);
})}

View File

@ -7,10 +7,18 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import MediaPlayer from './../ascribe_media/media_player';
import FacebookShareButton from '../ascribe_social_share/facebook_share_button';
import TwitterShareButton from '../ascribe_social_share/twitter_share_button';
import CollapsibleButton from './../ascribe_collapsible/collapsible_button';
import AclProxy from '../acl_proxy';
import UserActions from '../../actions/user_actions';
import UserStore from '../../stores/user_store';
import { mergeOptions } from '../../utils/general_utils.js';
import { getLangText } from '../../utils/lang_utils.js';
const EMBED_IFRAME_HEIGHT = {
video: 315,
@ -24,10 +32,17 @@ let MediaContainer = React.createClass({
},
getInitialState() {
return {timerId: null};
return mergeOptions(
UserStore.getState(),
{
timerId: null
});
},
componentDidMount() {
UserStore.listen(this.onChange);
UserActions.fetchCurrentUser();
if (!this.props.content.digital_work) {
return;
}
@ -45,19 +60,32 @@ let MediaContainer = React.createClass({
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
window.clearInterval(this.state.timerId);
},
onChange(state) {
this.setState(state);
},
render() {
let thumbnail = this.props.content.thumbnail.thumbnail_sizes && this.props.content.thumbnail.thumbnail_sizes['600x600'] ?
this.props.content.thumbnail.thumbnail_sizes['600x600'] : this.props.content.thumbnail.url_safe;
let mimetype = this.props.content.digital_work.mime;
const { content } = this.props;
// Pieces and editions are joined to the user by a foreign key in the database, so
// the information in content will be updated if a user updates their username.
// We also force uniqueness of usernames, so this check is safe to dtermine if the
// content was registered by the current user.
const didUserRegisterContent = this.state.currentUser && (this.state.currentUser.username === content.user_registered);
let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ?
content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe;
let mimetype = content.digital_work.mime;
let embed = null;
let extraData = null;
let isEmbedDisabled = mimetype === 'video' && this.props.content.digital_work.isEncoding !== undefined && this.props.content.digital_work.isEncoding !== 100;
let isEmbedDisabled = mimetype === 'video' && content.digital_work.isEncoding !== undefined && content.digital_work.isEncoding !== 100;
if (this.props.content.digital_work.encoding_urls) {
extraData = this.props.content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
if (content.digital_work.encoding_urls) {
extraData = content.digital_work.encoding_urls.map(e => { return { url: e.url, type: e.label }; });
}
if (['video', 'audio'].indexOf(mimetype) > -1) {
@ -73,7 +101,7 @@ let MediaContainer = React.createClass({
panel={
<pre className="">
{'<iframe width="560" height="' + height + '" src="https://embed.ascribe.io/content/'
+ this.props.content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
+ content.bitcoin_id + '" frameborder="0" allowfullscreen></iframe>'}
</pre>
}/>
);
@ -83,16 +111,22 @@ let MediaContainer = React.createClass({
<MediaPlayer
mimetype={mimetype}
preview={thumbnail}
url={this.props.content.digital_work.url}
url={content.digital_work.url}
extraData={extraData}
encodingStatus={this.props.content.digital_work.isEncoding} />
encodingStatus={content.digital_work.isEncoding} />
<p className="text-center">
<span className="ascribe-social-button-list">
<FacebookShareButton />
<TwitterShareButton
text={getLangText('Check out %s ascribed piece', didUserRegisterContent ? 'my latest' : 'this' )} />
</span>
<AclProxy
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || this.props.content.acl.acl_download}
aclObject={this.props.content.acl}
show={['video', 'audio', 'image'].indexOf(mimetype) === -1 || content.acl.acl_download}
aclObject={content.acl}
aclName="acl_download">
<Button bsSize="xsmall" className="ascribe-margin-1px" href={this.props.content.digital_work.url} target="_blank">
Download <Glyphicon glyph="cloud-download"/>
Download .{mimetype} <Glyphicon glyph="cloud-download"/>
</Button>
</AclProxy>
{embed}

View File

@ -2,6 +2,7 @@
import React from 'react';
import { History } from 'react-router';
import Moment from 'moment';
import PieceActions from '../../actions/piece_actions';
import PieceStore from '../../stores/piece_store';
@ -27,6 +28,9 @@ import CreateEditionsForm from '../ascribe_forms/create_editions_form';
import CreateEditionsButton from '../ascribe_buttons/create_editions_button';
import DeleteButton from '../ascribe_buttons/delete_button';
import AclInformation from '../ascribe_buttons/acl_information';
import AclProxy from '../acl_proxy';
import ListRequestActions from '../ascribe_forms/list_form_request_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
@ -35,16 +39,17 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import Note from './note';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
/**
* This is the component that implements resource/data specific functionality
*/
let PieceContainer = React.createClass({
propTypes: {
location: React.PropTypes.object,
params: React.PropTypes.object
},
@ -71,6 +76,12 @@ let PieceContainer = React.createClass({
UserStore.listen(this.onChange);
PieceListStore.listen(this.onChange);
PieceStore.listen(this.onChange);
// Every time we enter the piece detail page, just reset the piece
// store as it will otherwise display wrong/old data once the user loads
// the piece detail a second time
PieceActions.updatePiece({});
this.loadPiece();
UserActions.fetchCurrentUser();
PieceActions.fetchOne(this.props.params.pieceId);
@ -82,7 +93,7 @@ let PieceContainer = React.createClass({
if(pieceError && pieceError.status === 404) {
// Even though this path doesn't exist we can redirect
// to it as it catches all unknown paths
this.history.pushState(null, '/404');
this.history.pushState(null, '/404');
}
},
@ -190,39 +201,50 @@ let PieceContainer = React.createClass({
},
getActions() {
if (this.state.piece &&
this.state.piece.notifications &&
this.state.piece.notifications.length > 0) {
const { piece, currentUser } = this.state;
if (piece && piece.notifications && piece.notifications.length > 0) {
return (
<ListRequestActions
pieceOrEditions={this.state.piece}
currentUser={this.state.currentUser}
pieceOrEditions={piece}
currentUser={currentUser}
handleSuccess={this.loadPiece}
notifications={this.state.piece.notifications}/>);
}
else {
notifications={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>
<AclProxy
show={currentUser && currentUser.email}>
<DetailProperty label={getLangText('ACTIONS')}>
<AclButtonList
className="ascribe-button-list"
availableAcls={piece.acl}
pieceOrEditions={piece}
handleSuccess={this.loadPiece}>
<CreateEditionsButton
label={getLangText('CREATE EDITIONS')}
className="btn-sm"
piece={piece}
toggleCreateEditionsDialog={this.toggleCreateEditionsDialog}
onPollingSuccess={this.handlePollingSuccess}/>
<DeleteButton
handleSuccess={this.handleDeleteSuccess}
piece={piece}/>
<AclInformation
aim="button"
verbs={['acl_share', 'acl_transfer', 'acl_create_editions', 'acl_loan', 'acl_delete',
'acl_consign']}
aclObject={piece.acl}/>
</AclButtonList>
</DetailProperty>
</AclProxy>
);
}
},
render() {
if(this.state.piece && this.state.piece.title) {
if(this.state.piece && this.state.piece.id) {
setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', '));
return (
<Piece
piece={this.state.piece}
@ -232,7 +254,7 @@ let PieceContainer = React.createClass({
<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) } />
<DetailProperty label="DATE" value={Moment(this.state.piece.date_created, 'YYYY-MM-DD').year() } />
{this.state.piece.num_editions > 0 ? <DetailProperty label="EDITIONS" value={ this.state.piece.num_editions } /> : null}
<hr/>
</div>
@ -265,6 +287,15 @@ let PieceContainer = React.createClass({
successMessage={getLangText('Private note saved')}
url={ApiUrls.note_private_piece}
currentUser={this.state.currentUser}/>
<Note
id={this.getId}
label={getLangText('Piece note (public)')}
defaultValue={this.state.piece.public_note || null}
placeholder={getLangText('Enter your comments ...')}
editable={!!this.state.piece.acl.acl_edit}
successMessage={getLangText('Public piece note saved')}
url={ApiUrls.note_public_piece}
currentUser={this.state.currentUser}/>
</CollapsibleParagraph>
<CollapsibleParagraph
title={getLangText('Further Details')}
@ -277,8 +308,7 @@ let PieceContainer = React.createClass({
pieceId={this.state.piece.id}
extraData={this.state.piece.extra_data}
otherData={this.state.piece.other_data}
handleSuccess={this.loadPiece}
location={this.props.location}/>
handleSuccess={this.loadPiece} />
</CollapsibleParagraph>
</Piece>
@ -286,7 +316,7 @@ let PieceContainer = React.createClass({
} else {
return (
<div className="fullpage-spinner">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />
<AscribeSpinner color='dark-blue' size='lg'/>
</div>
);
}

View File

@ -0,0 +1,128 @@
'use strict';
import React from 'react';
import ConsignForm from '../ascribe_forms/form_consign';
import UnConsignForm from '../ascribe_forms/form_unconsign';
import TransferForm from '../ascribe_forms/form_transfer';
import LoanForm from '../ascribe_forms/form_loan';
import LoanRequestAnswerForm from '../ascribe_forms/form_loan_request_answer';
import ShareForm from '../ascribe_forms/form_share_email';
import AppConstants from '../../constants/application_constants';
import ApiUrls from '../../constants/api_urls';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import { getAclFormMessage, getAclFormDataId } from '../../utils/form_utils';
let AclFormFactory = React.createClass({
propTypes: {
action: React.PropTypes.oneOf(AppConstants.aclList).isRequired,
currentUser: React.PropTypes.object.isRequired,
email: React.PropTypes.string,
message: React.PropTypes.string,
pieceOrEditions: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array
]).isRequired,
handleSuccess: React.PropTypes.func,
showNotification: React.PropTypes.bool
},
isPiece() {
return this.props.pieceOrEditions.constructor !== Array;
},
getFormDataId() {
return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
},
showSuccessNotification(response) {
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess();
}
if (response.notification) {
const notification = new GlobalNotificationModel(response.notification, 'success');
GlobalNotificationActions.appendGlobalNotification(notification);
}
},
render() {
const {
action,
pieceOrEditions,
currentUser,
email,
message,
handleSuccess,
showNotification } = this.props;
const formMessage = message || getAclFormMessage({
aclName: action,
entities: pieceOrEditions,
isPiece: this.isPiece(),
senderName: currentUser.username
});
if (action === 'acl_consign') {
return (
<ConsignForm
email={email}
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_consigns}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_unconsign') {
return (
<UnConsignForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_unconsigns}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_transfer') {
return (
<TransferForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_transfers}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_loan') {
return (
<LoanForm
email={email}
message={formMessage}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_loans_pieces
: ApiUrls.ownership_loans_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_loan_request') {
return (
<LoanRequestAnswerForm
message={formMessage}
id={this.getFormDataId()}
url={ApiUrls.ownership_loans_pieces_request_confirm}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else if (action === 'acl_share') {
return (
<ShareForm
message={formMessage}
id={this.getFormDataId()}
url={this.isPiece() ? ApiUrls.ownership_shares_pieces
: ApiUrls.ownership_shares_editions}
handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} />
);
} else {
throw new Error('Your specified action did not match a form.');
}
}
});
export default AclFormFactory;

View File

@ -8,6 +8,7 @@ import Property from '../ascribe_forms/property';
import GlobalNotificationModel from '../../models/global_notification_model';
import GlobalNotificationActions from '../../actions/global_notification_actions';
import AscribeSpinner from '../ascribe_spinner';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils';
@ -43,12 +44,12 @@ let CreateEditionsForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Create editions')}
</button>}
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" />
<button className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</button>
}>
<Property

View File

@ -12,7 +12,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import requests from '../../utils/requests';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptionsWithDuplicates } from '../../utils/general_utils';
import { sanitize } from '../../utils/general_utils';
let Form = React.createClass({
@ -124,12 +124,12 @@ let Form = React.createClass({
getFormData() {
let data = {};
for(let ref in this.refs) {
for (let ref in this.refs) {
data[this.refs[ref].props.name] = this.refs[ref].state.value;
}
if(typeof this.props.getFormData === 'function') {
data = mergeOptionsWithDuplicates(data, this.props.getFormData());
if (typeof this.props.getFormData === 'function') {
data = Object.assign(data, this.props.getFormData());
}
return data;
@ -155,7 +155,7 @@ let Form = React.createClass({
});
},
handleError(err){
handleError(err) {
if (err.json) {
for (let input in err.json.errors){
if (this.refs && this.refs[input] && this.refs[input].state) {
@ -185,7 +185,7 @@ let Form = React.createClass({
this.setState({submitted: false});
},
clearErrors(){
clearErrors() {
for(let ref in this.refs){
if (this.refs[ref] && typeof this.refs[ref].clearErrors === 'function'){
this.refs[ref].clearErrors();
@ -236,12 +236,12 @@ let Form = React.createClass({
},
renderChildren() {
return ReactAddons.Children.map(this.props.children, (child) => {
return ReactAddons.Children.map(this.props.children, (child, i) => {
if (child) {
return ReactAddons.addons.cloneWithProps(child, {
handleChange: this.handleChangeChild,
ref: child.props.name,
key: i,
// 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
@ -269,6 +269,83 @@ let Form = React.createClass({
}
},
/**
* Validates a single ref and returns a human-readable error message
* @param {object} refToValidate A customly constructed object to check
* @return {oneOfType([arrayOf(string), bool])} Either an error message or false, saying that
* everything is valid
*/
_hasRefErrors(refToValidate) {
let errors = Object
.keys(refToValidate)
.reduce((a, constraintKey) => {
const contraintValue = refToValidate[constraintKey];
if(!contraintValue) {
switch(constraintKey) {
case 'min' || 'max':
a.push(getLangText('The field you defined is not in the valid range'));
break;
case 'pattern':
a.push(getLangText('The value you defined is not matching the valid pattern'));
break;
case 'required':
a.push(getLangText('This field is required'));
break;
}
}
return a;
}, []);
return errors.length ? errors : false;
},
/**
* This method validates all child inputs of the form.
*
* As of now, it only considers
* - `max`
* - `min`
* - `pattern`
* - `required`
*
* The idea is to enhance this method everytime we need more thorough validation.
* So feel free to add props that additionally should be checked, if they're present
* in the input's props.
*
* @return {[type]} [description]
*/
validate() {
this.clearErrors();
const validatedFormInputs = {};
Object
.keys(this.refs)
.forEach((refName) => {
let refToValidate = {};
const property = this.refs[refName];
const input = property.refs.input;
const value = input.getDOMNode().value || input.state.value;
const { max,
min,
pattern,
required,
type } = input.props;
refToValidate.required = required ? value : true;
refToValidate.pattern = pattern && typeof value === 'string' ? value.match(pattern) : true;
refToValidate.max = type === 'number' ? parseInt(value, 10) <= max : true;
refToValidate.min = type === 'number' ? parseInt(value, 10) >= min : true;
const validatedRef = this._hasRefErrors(refToValidate);
validatedFormInputs[refName] = validatedRef;
});
const errorMessagesForRefs = sanitize(validatedFormInputs, (val) => !val);
this.handleError({ json: { errors: errorMessagesForRefs } });
return !Object.keys(errorMessagesForRefs).length;
},
render() {
let className = 'ascribe-form';
@ -288,10 +365,8 @@ let Form = React.createClass({
{this.renderChildren()}
{this.getButtons()}
</form>
);
}
});
export default Form;

View File

@ -8,9 +8,9 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
import AclInformation from '../ascribe_buttons/acl_information';
let ConsignForm = React.createClass({
propTypes: {
@ -43,8 +43,11 @@ let ConsignForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<AclInformation aim={'form'} verbs={['acl_consign']}/>
<Property
name='consignee'
label={getLangText('Email')}>
@ -62,7 +65,7 @@ let ConsignForm = React.createClass({
rows={1}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
required />
</Property>
<Property
name='password'
@ -78,4 +81,4 @@ let ConsignForm = React.createClass({
}
});
export default ConsignForm;
export default ConsignForm;

View File

@ -15,6 +15,7 @@ import PropertyCollapsible from './property_collapsible';
import InputTextAreaToggable from './input_textarea_toggable';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
@ -101,12 +102,12 @@ let ContractAgreementForm = React.createClass({
handleSuccess={this.handleSubmitSuccess}
buttons={<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{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 className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">

View File

@ -47,7 +47,7 @@ let CopyrightAssociationForm = React.createClass({
handleSuccess={this.handleSubmitSuccess}>
<Property
name="copyright_association"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
label={getLangText('Copyright Association')}
style={{paddingBottom: 0}}>
<select defaultValue={selectedState} name="contract">

View File

@ -28,8 +28,7 @@ let CreateContractForm = React.createClass({
fileClassToUpload: React.PropTypes.shape({
singular: React.PropTypes.string,
plural: React.PropTypes.string
}),
location: React.PropTypes.object
})
},
getInitialState() {
@ -87,8 +86,7 @@ let CreateContractForm = React.createClass({
areAssetsEditable={true}
setIsUploadReady={this.setIsUploadReady}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
fileClassToUpload={this.props.fileClassToUpload} />
</Property>
<Property
name='name'
@ -110,4 +108,4 @@ let CreateContractForm = React.createClass({
}
});
export default CreateContractForm;
export default CreateContractForm;

View File

@ -5,10 +5,10 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import AclInformation from '../ascribe_buttons/acl_information';
let EditionDeleteForm = React.createClass({
@ -55,9 +55,12 @@ let EditionDeleteForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<AclInformation aim={'form'} verbs={['acl_delete']}/>
<p>{getLangText('Are you sure you would like to permanently delete this edition')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
</Form>

View File

@ -4,8 +4,10 @@ import React from 'react';
import Form from '../ascribe_forms/form';
import AclInformation from '../ascribe_buttons/acl_information';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -46,9 +48,12 @@ let PieceDeleteForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<AclInformation aim={'form'} verbs={['acl_delete']}/>
<p>{getLangText('Are you sure you would like to permanently delete this piece')}&#63;</p>
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
</Form>

View File

@ -15,11 +15,11 @@ import InputCheckbox from './input_checkbox';
import ContractAgreementListStore from '../../stores/contract_agreement_list_store';
import ContractAgreementListActions from '../../actions/contract_agreement_list_actions';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import AclInformation from '../ascribe_buttons/acl_information';
let LoanForm = React.createClass({
propTypes: {
@ -144,7 +144,7 @@ let LoanForm = React.createClass({
return (
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
key="terms_explicitly"
@ -194,7 +194,7 @@ let LoanForm = React.createClass({
return (
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Finish process')}
</button>
);
@ -225,11 +225,14 @@ let LoanForm = React.createClass({
buttons={this.getButtons()}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<div className={classnames({'ascribe-form-header': true, 'hidden': !this.props.loanHeading})}>
<h3>{this.props.loanHeading}</h3>
</div>
<AclInformation aim={'form'} verbs={['acl_loan']}/>
<Property
name='loanee'
label={getLangText('Loanee Email')}
@ -282,7 +285,7 @@ let LoanForm = React.createClass({
rows={1}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required={this.props.showPersonalMessage ? 'required' : ''}/>
required={this.props.showPersonalMessage}/>
</Property>
{this.getContractCheckbox()}
{this.getAppendix()}
@ -301,4 +304,4 @@ let LoanForm = React.createClass({
}
});
export default LoanForm;
export default LoanForm;

View File

@ -14,6 +14,7 @@ import Property from './property';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -59,7 +60,7 @@ let LoginForm = React.createClass({
GlobalNotificationActions.appendGlobalNotification(notification);
if(success) {
UserActions.fetchCurrentUser();
UserActions.fetchCurrentUser(true);
}
},
@ -75,12 +76,12 @@ let LoginForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{this.props.submitMessage}
</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 className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">

View File

@ -50,7 +50,7 @@ let PieceExtraDataForm = React.createClass({
rows={1}
defaultValue={defaultValue}
placeholder={getLangText('Fill in%s', ' ') + this.props.title}
required="required"/>
required />
</Property>
<hr />
</Form>

View File

@ -11,6 +11,7 @@ import InputFineUploader from './input_fineuploader';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
import { mergeOptions } from '../../utils/general_utils';
@ -25,12 +26,15 @@ let RegisterPieceForm = React.createClass({
isFineUploaderActive: React.PropTypes.bool,
isFineUploaderEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool,
children: React.PropTypes.element,
onLoggedOut: React.PropTypes.func,
// For this form to work with SlideContainer, we sometimes have to disable it
disabled: React.PropTypes.bool,
location: React.PropTypes.object
location: React.PropTypes.object,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element
])
},
getDefaultProps() {
@ -84,14 +88,14 @@ let RegisterPieceForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login"
className="btn btn-default btn-wide"
disabled={!this.state.isUploadReady || this.props.disabled}>
{this.props.submitMessage}
</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 className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -115,7 +119,7 @@ let RegisterPieceForm = React.createClass({
onLoggedOut={this.props.onLoggedOut}
disabled={!this.props.isFineUploaderEditable}
enableLocalHashing={enableLocalHashing}
location={this.props.location}/>
uploadMethod={this.props.location.query.method} />
</Property>
<Property
name='artist_name'

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -53,7 +53,9 @@ let EditionRemoveFromCollectionForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to remove these editions from your collection')}&#63;</p>

View File

@ -5,7 +5,7 @@ import React from 'react';
import Form from './form';
import ApiUrls from '../../constants/api_urls';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -46,7 +46,9 @@ let PieceRemoveFromCollectionForm = React.createClass({
}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>
}>
<p>{getLangText('Are you sure you would like to remove this piece from your collection')}&#63;</p>

View File

@ -2,10 +2,13 @@
import React from 'react';
import AclButton from './../ascribe_buttons/acl_button';
import ActionPanel from '../ascribe_panel/action_panel';
import Form from './form';
import LoanRequestButton from '../ascribe_buttons/acls/loan_request_button';
import UnconsignButton from '../ascribe_buttons/acls/unconsign_button';
import ActionPanel from '../ascribe_panel/action_panel';
import NotificationActions from '../../actions/notification_actions';
import GlobalNotificationModel from '../../models/global_notification_model';
@ -13,9 +16,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import ApiUrls from '../../constants/api_urls';
import { getAclFormDataId } from '../../utils/form_utils';
import { getLangText } from '../../utils/lang_utils.js';
let RequestActionForm = React.createClass({
propTypes: {
pieceOrEditions: React.PropTypes.oneOfType([
@ -27,26 +30,26 @@ let RequestActionForm = React.createClass({
handleSuccess: React.PropTypes.func
},
isPiece(){
isPiece() {
return this.props.pieceOrEditions.constructor !== Array;
},
getUrls() {
let urls = {};
if (this.props.notifications.action === 'consign'){
if (this.props.notifications.action === 'consign') {
urls.accept = ApiUrls.ownership_consigns_confirm;
urls.deny = ApiUrls.ownership_consigns_deny;
} else if (this.props.notifications.action === 'unconsign'){
} 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()){
} 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()){
} 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()){
} 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;
}
@ -54,37 +57,28 @@ let RequestActionForm = React.createClass({
return urls;
},
getFormData(){
if (this.isPiece()) {
return {piece_id: this.props.pieceOrEditions.id};
}
else {
return {bitcoin_id: this.props.pieceOrEditions.map(function(edition){
return edition.bitcoin_id;
}).join()};
}
getFormData() {
return getAclFormDataId(this.isPiece(), this.props.pieceOrEditions);
},
showNotification(option, action, owner) {
return () => {
let message = getLangText('You have successfully') + ' ' + option + ' the ' + action + ' request ' + getLangText('from') + ' ' + owner;
let notifications = new GlobalNotificationModel(message, 'success');
const message = getLangText('You have successfully %s the %s request from %s', getLangText(option), getLangText(action), owner);
const notifications = new GlobalNotificationModel(message, 'success');
GlobalNotificationActions.appendGlobalNotification(notifications);
this.handleSuccess();
};
},
handleSuccess() {
if (this.isPiece()){
if (this.isPiece()) {
NotificationActions.fetchPieceListNotifications();
}
else {
} else {
NotificationActions.fetchEditionListNotifications();
}
if(this.props.handleSuccess) {
if (typeof this.props.handleSuccess === 'function') {
this.props.handleSuccess();
}
},
@ -98,21 +92,19 @@ let RequestActionForm = React.createClass({
},
getAcceptButtonForm(urls) {
if(this.props.notifications.action === 'unconsign') {
if (this.props.notifications.action === 'unconsign') {
return (
<AclButton
<UnconsignButton
availableAcls={{'acl_unconsign': true}}
action="acl_unconsign"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
currentUser={this.props.currentUser}
handleSuccess={this.handleSuccess} />
);
} else if(this.props.notifications.action === 'loan_request') {
} else if (this.props.notifications.action === 'loan_request') {
return (
<AclButton
<LoanRequestButton
availableAcls={{'acl_loan_request': true}}
action="acl_loan_request"
buttonAcceptName="LOAN"
buttonAcceptClassName='inline pull-right btn-sm ascribe-margin-1px'
pieceOrEditions={this.props.pieceOrEditions}
@ -125,7 +117,7 @@ let RequestActionForm = React.createClass({
url={urls.accept}
getFormData={this.getFormData}
handleSuccess={
this.showNotification(getLangText('accepted'), this.props.notifications.action, this.props.notifications.by)
this.showNotification('accepted', this.props.notifications.action, this.props.notifications.by)
}
isInline={true}
className='inline pull-right'>
@ -140,8 +132,8 @@ let RequestActionForm = React.createClass({
},
getButtonForm() {
let urls = this.getUrls();
let acceptButtonForm = this.getAcceptButtonForm(urls);
const urls = this.getUrls();
const acceptButtonForm = this.getAcceptButtonForm(urls);
return (
<div>
@ -150,13 +142,13 @@ let RequestActionForm = React.createClass({
isInline={true}
getFormData={this.getFormData}
handleSuccess={
this.showNotification(getLangText('denied'), this.props.notifications.action, this.props.notifications.by)
this.showNotification('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')}
{getLangText('REJECT')}
</button>
</Form>
{acceptButtonForm}
@ -168,10 +160,10 @@ let RequestActionForm = React.createClass({
return (
<ActionPanel
content={this.getContent()}
buttons={this.getButtonForm()}/>
buttons={this.getButtonForm()} />
);
}
});
export default RequestActionForm;
export default RequestActionForm;

View File

@ -8,7 +8,9 @@ import InputTextAreaToggable from './input_textarea_toggable';
import Button from 'react-bootstrap/lib/Button';
import AppConstants from '../../constants/application_constants';
import AclInformation from '../ascribe_buttons/acl_information';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -47,8 +49,11 @@ let ShareForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<AclInformation aim={'form'} verbs={['acl_share']}/>
<Property
name='share_emails'
label={getLangText('Emails')}>
@ -66,11 +71,11 @@ let ShareForm = React.createClass({
rows={1}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
required />
</Property>
</Form>
);
}
});
export default ShareForm;
export default ShareForm;

View File

@ -3,8 +3,6 @@
import React from 'react';
import { History } from 'react-router';
import { getLangText } from '../../utils/lang_utils';
import UserStore from '../../stores/user_store';
import UserActions from '../../actions/user_actions';
@ -16,6 +14,9 @@ import Property from './property';
import InputCheckbox from './input_checkbox';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
let SignupForm = React.createClass({
@ -60,7 +61,7 @@ let SignupForm = React.createClass({
// Refactor this to its own component
this.props.handleSuccess(getLangText('We sent an email to your address') + ' ' + response.user.email + ', ' + getLangText('please confirm') + '.');
} else {
UserActions.fetchCurrentUser();
UserActions.fetchCurrentUser(true);
}
},
@ -86,12 +87,12 @@ let SignupForm = React.createClass({
getFormData={this.getFormData}
handleSuccess={this.handleSuccess}
buttons={
<button type="submit" className="btn ascribe-btn ascribe-btn-login">
<button type="submit" className="btn btn-default btn-wide">
{this.props.submitMessage}
</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 className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<div className="ascribe-form-header">
@ -130,7 +131,7 @@ let SignupForm = React.createClass({
{this.props.children}
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>

View File

@ -9,7 +9,7 @@ import InputCheckbox from './input_checkbox';
import Alert from 'react-bootstrap/lib/Alert';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import ApiUrls from '../../constants/api_urls';
import { getLangText } from '../../utils/lang_utils.js';
@ -40,7 +40,9 @@ let PieceSubmitToPrizeForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='artist_statement'
@ -50,7 +52,7 @@ let PieceSubmitToPrizeForm = React.createClass({
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter your statement')}
required="required"/>
required />
</Property>
<Property
name='work_description'
@ -60,11 +62,11 @@ let PieceSubmitToPrizeForm = React.createClass({
<InputTextAreaToggable
rows={1}
placeholder={getLangText('Enter the description for your work')}
required="required"/>
required />
</Property>
<Property
name="terms"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox>
<span>

View File

@ -9,8 +9,10 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AclInformation from '../ascribe_buttons/acl_information';
import AscribeSpinner from '../ascribe_spinner';
import AppConstants from '../../constants/application_constants';
import { getLangText } from '../../utils/lang_utils.js';
@ -48,8 +50,11 @@ let TransferForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<AclInformation aim={'form'} verbs={['acl_transfer']}/>
<Property
name='transferee'
label={getLangText('Email')}>
@ -67,7 +72,7 @@ let TransferForm = React.createClass({
rows={1}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
required />
</Property>
<Property
name='password'
@ -88,4 +93,4 @@ let TransferForm = React.createClass({
}
});
export default TransferForm;
export default TransferForm;

View File

@ -8,7 +8,7 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -45,7 +45,9 @@ let UnConsignForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='unconsign_message'
@ -56,7 +58,7 @@ let UnConsignForm = React.createClass({
rows={1}
defaultValue={this.props.message}
placeholder={getLangText('Enter a message...')}
required="required"/>
required />
</Property>
<Property
name='password'
@ -72,4 +74,4 @@ let UnConsignForm = React.createClass({
}
});
export default UnConsignForm;
export default UnConsignForm;

View File

@ -8,8 +8,7 @@ import Form from './form';
import Property from './property';
import InputTextAreaToggable from './input_textarea_toggable';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils.js';
@ -45,7 +44,9 @@ let UnConsignRequestForm = React.createClass({
</div>}
spinner={
<div className="modal-footer">
<img src={AppConstants.baseUrl + 'static/img/ascribe_animated_small.gif'} />
<p className="pull-right">
<AscribeSpinner color='dark-blue' size='md'/>
</p>
</div>}>
<Property
name='unconsign_request_message'

View File

@ -3,64 +3,80 @@
import React from 'react';
import ReactS3FineUploader from '../ascribe_uploader/react_s3_fine_uploader';
import FileDragAndDrop from '../ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop';
import AppConstants from '../../constants/application_constants';
import { getCookie } from '../../utils/fetch_api_utils';
let InputFineUploader = React.createClass({
const { func, bool, object, shape, string, number, arrayOf } = React.PropTypes;
const InputFineUploader = React.createClass({
propTypes: {
setIsUploadReady: React.PropTypes.func,
isReadyForFormSubmission: React.PropTypes.func,
submitFileName: React.PropTypes.func,
setIsUploadReady: func,
isReadyForFormSubmission: func,
submitFileName: func,
fileInputElement: func,
areAssetsDownloadable: React.PropTypes.bool,
areAssetsDownloadable: bool,
onClick: React.PropTypes.func,
keyRoutine: React.PropTypes.shape({
url: React.PropTypes.string,
fileClass: React.PropTypes.string
keyRoutine: shape({
url: string,
fileClass: string
}),
createBlobRoutine: React.PropTypes.shape({
url: React.PropTypes.string
createBlobRoutine: shape({
url: string
}),
validation: React.PropTypes.shape({
itemLimit: React.PropTypes.number,
sizeLimit: React.PropTypes.string,
allowedExtensions: React.PropTypes.arrayOf(React.PropTypes.string)
validation: shape({
itemLimit: number,
sizeLimit: string,
allowedExtensions: arrayOf(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,
isFineUploaderActive: bool,
onLoggedOut: func,
enableLocalHashing: React.PropTypes.bool,
enableLocalHashing: bool,
uploadMethod: string,
// provided by Property
disabled: React.PropTypes.bool,
disabled: 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
}),
location: React.PropTypes.object
fileClassToUpload: shape({
singular: string,
plural: string
})
},
getDefaultProps() {
return {
fileInputElement: FileDragAndDrop
};
},
getInitialState() {
return {
value: null
value: null,
file: null
};
},
submitFile(file){
submitFile(file) {
this.setState({
file,
value: file.key
});
if(this.state.value && typeof this.props.onChange === 'function') {
this.props.onChange({ target: { value: this.state.value } });
}
if(typeof this.props.submitFileName === 'function') {
this.props.submitFileName(file.originalName);
}
@ -70,7 +86,25 @@ let InputFineUploader = React.createClass({
this.refs.fineuploader.reset();
},
createBlobRoutine() {
const { fineuploader } = this.refs;
const { file } = this.state;
fineuploader.createBlob(file);
},
render() {
const { fileInputElement,
keyRoutine,
createBlobRoutine,
validation,
setIsUploadReady,
isReadyForFormSubmission,
areAssetsDownloadable,
onLoggedOut,
enableLocalHashing,
fileClassToUpload,
location } = this.props;
let editable = this.props.isFineUploaderActive;
// if disabled is actually set by property, we want to override
@ -82,14 +116,14 @@ let InputFineUploader = React.createClass({
return (
<ReactS3FineUploader
ref="fineuploader"
onClick={this.props.onClick}
keyRoutine={this.props.keyRoutine}
createBlobRoutine={this.props.createBlobRoutine}
validation={this.props.validation}
fileInputElement={fileInputElement}
keyRoutine={keyRoutine}
createBlobRoutine={createBlobRoutine}
validation={validation}
submitFile={this.submitFile}
setIsUploadReady={this.props.setIsUploadReady}
isReadyForFormSubmission={this.props.isReadyForFormSubmission}
areAssetsDownloadable={this.props.areAssetsDownloadable}
setIsUploadReady={setIsUploadReady}
isReadyForFormSubmission={isReadyForFormSubmission}
areAssetsDownloadable={areAssetsDownloadable}
areAssetsEditable={editable}
signature={{
endpoint: AppConstants.serverUrl + 's3/signature/',
@ -107,10 +141,10 @@ let InputFineUploader = React.createClass({
}}
onInactive={this.props.onLoggedOut}
enableLocalHashing={this.props.enableLocalHashing}
fileClassToUpload={this.props.fileClassToUpload}
location={this.props.location}/>
uploadMethod={this.props.uploadMethod}
fileClassToUpload={this.props.fileClassToUpload} />
);
}
});
export default InputFineUploader;
export default InputFineUploader;

View File

@ -9,8 +9,11 @@ let InputTextAreaToggable = React.createClass({
propTypes: {
disabled: React.PropTypes.bool,
rows: React.PropTypes.number.isRequired,
required: React.PropTypes.string,
defaultValue: React.PropTypes.string
required: React.PropTypes.bool,
defaultValue: React.PropTypes.string,
placeholder: React.PropTypes.string,
onBlur: React.PropTypes.func,
onChange: React.PropTypes.func
},
getInitialState() {
@ -66,4 +69,4 @@ let InputTextAreaToggable = React.createClass({
});
export default InputTextAreaToggable;
export default InputTextAreaToggable;

View File

@ -147,7 +147,12 @@ let Property = React.createClass({
if(typeof this.props.onClick === 'function') {
this.props.onClick();
}
// skip the focus of non-input elements
let nonInputHTMLElements = ['pre', 'div'];
if (this.refs.input &&
nonInputHTMLElements.indexOf(this.refs.input.getDOMNode().nodeName.toLowerCase()) > -1 ) {
return;
}
this.refs.input.getDOMNode().focus();
this.setState({
isFocused: true
@ -176,9 +181,7 @@ let Property = React.createClass({
setErrors(errors){
this.setState({
errors: errors.map((error) => {
return <span className="pull-right" key={error}>{error}</span>;
})
errors: errors.pop()
});
},
@ -242,17 +245,18 @@ let Property = React.createClass({
return (
<div
className={'ascribe-settings-wrapper ' + this.getClassName()}
className={'ascribe-property-wrapper ' + this.getClassName()}
onClick={this.handleFocus}
onFocus={this.handleFocus}
style={style}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
<div className={'ascribe-settings-property ' + this.props.className}>
{this.state.errors}
<span>{this.props.label}</span>
<div className={'ascribe-property ' + this.props.className}>
<p>
<span className="pull-left">{this.props.label}</span>
<span className="pull-right">{this.state.errors}</span>
</p>
{this.renderChildren(style)}
{footer}
</div>

View File

@ -62,14 +62,14 @@ let PropertyCollapsile = React.createClass({
return (
<div
className={'ascribe-settings-wrapper'}
className={'ascribe-property-wrapper'}
style={style}>
<OverlayTrigger
delay={500}
placement="top"
overlay={tooltip}>
<div
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
onClick={this.handleFocus}
onFocus={this.handleFocus}>
<input
@ -84,7 +84,7 @@ let PropertyCollapsile = React.createClass({
collapsible
expanded={this.state.show}
className="bs-custom-panel">
<div className="ascribe-settings-property">
<div className="ascribe-property">
{this.renderChildren()}
</div>
</Panel>

View File

@ -3,12 +3,13 @@
import React from 'react';
import Q from 'q';
import { escapeHTML } from '../../utils/general_utils';
import InjectInHeadMixin from '../../mixins/inject_in_head_mixin';
import Panel from 'react-bootstrap/lib/Panel';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../constants/application_constants.js';
import AppConstants from '../../constants/application_constants';
import { escapeHTML } from '../../utils/general_utils';
import { InjectInHeadUtils } from '../../utils/inject_utils';
/**
* This is the component that implements display-specific functionality.
@ -50,25 +51,33 @@ let Other = React.createClass({
let Image = React.createClass({
propTypes: {
url: React.PropTypes.string.isRequired,
url: React.PropTypes.string,
preview: React.PropTypes.string.isRequired
},
mixins: [InjectInHeadMixin],
componentDidMount() {
this.inject('https://code.jquery.com/jquery-2.1.4.min.js')
.then(() =>
Q.all([
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/shmui.css'),
this.inject(AppConstants.baseUrl + 'static/thirdparty/shmui/jquery.shmui.js')
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
if(this.props.url) {
InjectInHeadUtils.inject(AppConstants.jquery.sdkUrl)
.then(() =>
Q.all([
InjectInHeadUtils.inject(AppConstants.shmui.cssUrl),
InjectInHeadUtils.inject(AppConstants.shmui.sdkUrl)
]).then(() => { window.jQuery('.shmui-ascribe').shmui(); }));
}
},
render() {
return (
<img className="shmui-ascribe" src={this.props.preview} data-large-src={this.props.url}/>
);
const { url, preview } = this.props;
if(url) {
return (
<img className="shmui-ascribe" src={preview} data-large-src={url}/>
);
} else {
return (
<img src={preview}/>
);
}
}
});
@ -77,10 +86,8 @@ let Audio = React.createClass({
url: React.PropTypes.string.isRequired
},
mixins: [InjectInHeadMixin],
componentDidMount() {
this.inject(AppConstants.baseUrl + 'static/thirdparty/audiojs/audiojs/audio.min.js').then(this.ready);
InjectInHeadUtils.inject(AppConstants.audiojs.sdkUrl).then(this.ready);
},
ready() {
@ -103,14 +110,17 @@ let Video = React.createClass({
* ReactJS is responsible for DOM manipulation but VideoJS updates the DOM
* to install itself to display the video, therefore ReactJS complains that we are
* changing the DOM under its feet.
* The component supports a fall-back to HTML5 video tag.
*
* What we do is the following:
* 1) set `state.ready = false`
* 2) render the cover using the `<Image />` component (because ready is false)
* 1) set `state.libraryLoaded = null` (state.libraryLoaded can be in three states: `null`
* if we don't know anything about it, `true` if the external library has been loaded,
* `false` if we failed to load the external library)
* 2) render the cover using the `<Image />` component (because libraryLoaded is null)
* 3) on `componentDidMount`, we load the external `css` and `js` resources using
* the `InjectInHeadMixin`, attaching a function to `Promise.then` to change
* `state.ready` to true
* 4) when the promise is succesfully resolved, we change `state.ready` triggering
* the `InjectInHeadUtils`, attaching a function to `Promise.then` to change
* `state.libraryLoaded` to true
* 4) when the promise is succesfully resolved, we change `state.libraryLoaded` triggering
* a re-render
* 5) the new render calls `prepareVideoHTML` to get the raw HTML of the video tag
* (that will be later processed and expanded by VideoJS)
@ -126,21 +136,24 @@ let Video = React.createClass({
encodingStatus: React.PropTypes.number
},
mixins: [InjectInHeadMixin],
getInitialState() {
return { ready: false, videoMounted: false };
return { libraryLoaded: null, videoMounted: false };
},
componentDidMount() {
Q.all([
this.inject('//vjs.zencdn.net/4.12/video-js.css'),
this.inject('//vjs.zencdn.net/4.12/video.js')
]).then(this.ready);
InjectInHeadUtils.inject(AppConstants.videojs.cssUrl),
InjectInHeadUtils.inject(AppConstants.videojs.sdkUrl)])
.then(() => this.setState({libraryLoaded: true}))
.fail(() => this.setState({libraryLoaded: false}));
},
shouldComponentUpdate(nextProps, nextState) {
return nextState.videoMounted === false;
},
componentDidUpdate() {
if (this.state.ready && !this.state.videoMounted) {
if (this.state.libraryLoaded && !this.state.videoMounted) {
window.videojs('#mainvideo');
/* eslint-disable */
this.setState({videoMounted: true});
@ -149,11 +162,9 @@ let Video = React.createClass({
},
componentWillUnmount() {
window.videojs('#mainvideo').dispose();
},
ready() {
this.setState({ready: true, videoMounted: false});
if (this.state.videoMounted) {
window.videojs('#mainvideo').dispose();
}
},
prepareVideoHTML() {
@ -166,12 +177,8 @@ let Video = React.createClass({
return html.join('\n');
},
shouldComponentUpdate(nextProps, nextState) {
return nextState.videoMounted === false;
},
render() {
if (this.state.ready) {
if (this.state.libraryLoaded !== null) {
return (
<div dangerouslySetInnerHTML={{__html: this.prepareVideoHTML() }}/>
);
@ -200,26 +207,50 @@ let MediaPlayer = React.createClass({
},
render() {
if (this.props.mimetype === 'video' && this.props.encodingStatus !== undefined && this.props.encodingStatus !== 100) {
const { mimetype,
preview,
url,
extraData,
encodingStatus } = this.props;
if (mimetype === 'video' && encodingStatus !== undefined && encodingStatus !== 100) {
return (
<div className="ascribe-detail-header ascribe-media-player">
<p>
<em>We successfully received your video and it is now being encoded.
<br />You can leave this page and check back on the status later.</em>
</p>
<ProgressBar now={this.props.encodingStatus}
<ProgressBar now={encodingStatus}
label="%(percent)s%"
className="ascribe-progress-bar" />
</div>
);
} else {
let Component = resourceMap[this.props.mimetype] || Other;
let Component = resourceMap[mimetype] || Other;
let componentProps = {
preview,
url,
extraData,
encodingStatus
};
// Since the launch of the portfolio whitelabel submission,
// we allow the user to specify a thumbnail upon piece-registration.
// As the `Component` is chosen according to its filetype but could potentially
// have a manually submitted thumbnail, we match if the to `Mediaplayer` submitted thumbnail
// is not the generally used fallback `url` (ascribe_spiral.png).
//
// If this is the case, we disable shmui by deleting the original `url` prop and replace
// the assigned component to `Image`.
if(!decodeURIComponent(preview).match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/) &&
Component === Other) {
Component = resourceMap.image;
delete componentProps.url;
}
return (
<div className="ascribe-media-player">
<Component preview={this.props.preview}
url={this.props.url}
extraData={this.props.extraData}
encodingStatus={this.props.encodingStatus} />
<Component {...componentProps}/>
</div>
);
}

View File

@ -7,7 +7,7 @@ import Modal from 'react-bootstrap/lib/Modal';
let ModalWrapper = React.createClass({
propTypes: {
trigger: React.PropTypes.element.isRequired,
trigger: React.PropTypes.element,
title: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.element),
React.PropTypes.element,
@ -38,7 +38,7 @@ let ModalWrapper = React.createClass({
});
},
handleSuccess(response){
handleSuccess(response) {
this.props.handleSuccess(response);
this.hide();
},
@ -52,20 +52,22 @@ let ModalWrapper = React.createClass({
},
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});
const { trigger, title } = this.props;
// If the trigger component exists, we add the ModalWrapper's show() as its onClick method.
// The trigger component should, in most cases, be a button.
const clonedTrigger = React.isValidElement(trigger) ? React.cloneElement(trigger, {onClick: this.show})
: null;
return (
<span>
{trigger}
{clonedTrigger}
<Modal show={this.state.showModal} onHide={this.hide}>
<Modal.Header closeButton>
<Modal.Title>
{this.props.title}
{title}
</Modal.Title>
</Modal.Header>
<div className="modal-body">
<div className="modal-body" >
{this.renderChildren()}
</div>
</Modal>

View File

@ -3,6 +3,7 @@
import React from 'react';
import { Link } from 'react-router';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { getLangText } from '../../utils/lang_utils';
@ -28,14 +29,14 @@ let PaginationButton = React.createClass({
page -= 1;
directionDisplay = (
<span>
<span aria-hidden="true">&larr;</span> {getLangText('Previous')}
<span aria-hidden="true"><Glyphicon glyph='chevron-left'/></span> {getLangText('Previous')}
</span>
);
} else {
page += 1;
directionDisplay = (
<span>
{getLangText('Next')} <span aria-hidden="true">&rarr;</span>
{getLangText('Next')} <span aria-hidden="true"><Glyphicon glyph='chevron-right'/></span>
</span>
);
}

View File

@ -117,7 +117,7 @@ let PieceListBulkModal = React.createClass({
<div className="row-fluid">
<AclButtonList
availableAcls={availableAcls}
editions={selectedEditions}
pieceOrEditions={selectedEditions}
handleSuccess={this.handleSuccess}
className="text-center ascribe-button-list collapse-group">
<DeleteButton
@ -136,4 +136,4 @@ let PieceListBulkModal = React.createClass({
}
});
export default PieceListBulkModal;
export default PieceListBulkModal;

View File

@ -74,15 +74,15 @@ let PieceListToolbar = React.createClass({
<span className="pull-left">
{children}
</span>
<span className="pull-right">
{this.getOrderWidget()}
{this.getFilterWidget()}
</span>
<SearchBar
className="pull-right search-bar ascribe-input-glyph"
searchFor={searchFor}
searchQuery={searchQuery}
threshold={AppConstants.searchThreshold}/>
<span className="pull-right">
{this.getOrderWidget()}
{this.getFilterWidget()}
</span>
</div>
</div>
</div>

View File

@ -7,7 +7,7 @@ import DropdownButton from 'react-bootstrap/lib/DropdownButton';
import { getLangText } from '../../utils/lang_utils.js';
let PieceListToolbarFilterWidgetFilter = React.createClass({
let PieceListToolbarFilterWidget = React.createClass({
propTypes: {
filterParams: React.PropTypes.arrayOf(
React.PropTypes.shape({
@ -76,13 +76,14 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({
render() {
let filterIcon = (
<span>
<span className="glyphicon glyphicon-filter" aria-hidden="true"></span>
<span className="ascribe-icon icon-ascribe-filter" aria-hidden="true"></span>
<span style={this.isFilterActive()}>*</span>
</span>
);
return (
<DropdownButton
pullRight={true}
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">
{/* We iterate over filterParams, to receive the label and then for each
@ -139,4 +140,4 @@ let PieceListToolbarFilterWidgetFilter = React.createClass({
}
});
export default PieceListToolbarFilterWidgetFilter;
export default PieceListToolbarFilterWidget;

View File

@ -47,13 +47,14 @@ let PieceListToolbarOrderWidget = React.createClass({
render() {
let filterIcon = (
<span>
<span className="glyphicon glyphicon-sort-by-alphabet" aria-hidden="true"></span>
<span style={this.isOrderActive()}>*</span>
<span className="ascribe-icon icon-ascribe-sort" aria-hidden="true"></span>
<span style={this.isOrderActive()}>&middot;</span>
</span>
);
return (
<DropdownButton
pullRight={true}
title={filterIcon}
className="ascribe-piece-list-toolbar-filter-widget">
<li style={{'textAlign': 'center'}}>
@ -72,7 +73,7 @@ let PieceListToolbarOrderWidget = React.createClass({
</span>
<input
readOnly
type="checkbox"
type="radio"
checked={param.indexOf(this.props.orderBy) > -1} />
</div>
</li>

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react';
import { History } from 'react-router';
import { History, RouteContext } from 'react-router';
import UserStore from '../../../stores/user_store';
import UserActions from '../../../actions/user_actions';
@ -31,11 +31,15 @@ export default function AuthProxyHandler({to, when}) {
return (Component) => {
return React.createClass({
displayName: 'AuthProxyHandler',
propTypes: {
location: object
},
mixins: [History],
// We need insert `RouteContext` here in order to be able
// to use the `Lifecycle` widget in further down nested components
mixins: [History, RouteContext],
getInitialState() {
return UserStore.getState();
@ -47,7 +51,11 @@ export default function AuthProxyHandler({to, when}) {
},
componentDidUpdate() {
this.redirectConditionally();
// Only refresh this component, when UserSources are not loading
// data from the server
if(!UserStore.isLoading()) {
this.redirectConditionally();
}
},
componentWillUnmount() {

View File

@ -15,7 +15,7 @@ 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 AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -27,7 +27,7 @@ let AccountSettings = React.createClass({
},
handleSuccess(){
this.props.loadUser();
this.props.loadUser(true);
let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
@ -37,7 +37,7 @@ let AccountSettings = React.createClass({
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
let profile = null;
if (this.props.currentUser.username) {
@ -78,7 +78,7 @@ let AccountSettings = React.createClass({
getFormData={this.getFormDataProfile}>
<Property
name="hash_locally"
className="ascribe-settings-property-collapsible-toggle"
className="ascribe-property-collapsible-toggle"
style={{paddingBottom: 0}}>
<InputCheckbox
defaultChecked={this.props.currentUser.profile.hash_locally}>
@ -96,11 +96,15 @@ let AccountSettings = React.createClass({
title={getLangText('Account')}
defaultExpanded={true}>
{content}
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
<AclProxy
aclObject={this.props.whitelabel}
aclName="acl_view_settings_copyright_association">
<CopyrightAssociationForm currentUser={this.props.currentUser}/>
</AclProxy>
{profile}
</CollapsibleParagraph>
);
}
});
export default AccountSettings;
export default AccountSettings;

View File

@ -15,7 +15,7 @@ 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 AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -57,7 +57,7 @@ let APISettings = React.createClass({
},
getApplications(){
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.applications.length > -1) {
content = this.state.applications.map(function(app, i) {

View File

@ -10,7 +10,7 @@ import Property from '../ascribe_forms/property';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import AppConstants from '../../constants/application_constants';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
@ -38,7 +38,7 @@ let BitcoinWalletSettings = React.createClass({
},
render() {
let content = <img src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.walletSettings.btc_public_key) {
content = (

View File

@ -23,6 +23,7 @@ import GlobalNotificationActions from '../../actions/global_notification_actions
import AclProxy from '../acl_proxy';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils';
@ -86,6 +87,8 @@ let ContractSettings = React.createClass({
let privateContracts = this.getPrivateContracts();
let createPublicContractForm = null;
setDocumentTitle(getLangText('Contracts settings'));
if(publicContracts.length === 0) {
createPublicContractForm = (
<CreateContractForm
@ -193,4 +196,4 @@ let ContractSettings = React.createClass({
}
});
export default ContractSettings;
export default ContractSettings;

View File

@ -20,8 +20,7 @@ import { getLangText } from '../../utils/lang_utils';
let ContractSettingsUpdateButton = React.createClass({
propTypes: {
contract: React.PropTypes.object,
location: React.PropTypes.object
contract: React.PropTypes.object
},
submitFile(file) {
@ -56,7 +55,6 @@ let ContractSettingsUpdateButton = React.createClass({
render() {
return (
<ReactS3FineUploader
ref="fineuploader"
fileInputElement={UploadButton}
keyRoutine={{
url: AppConstants.serverUrl + 's3/key/',
@ -90,10 +88,9 @@ let ContractSettingsUpdateButton = React.createClass({
plural: getLangText('UPDATE')
}}
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
submitFile={this.submitFile}
location={this.props.location}/>
submitFile={this.submitFile} />
);
}
});
export default ContractSettingsUpdateButton;
export default ContractSettingsUpdateButton;

View File

@ -11,10 +11,13 @@ import WhitelabelActions from '../../actions/whitelabel_actions';
import AccountSettings from './account_settings';
import BitcoinWalletSettings from './bitcoin_wallet_settings';
import APISettings from './api_settings';
import WebhookSettings from './webhook_settings';
import AclProxy from '../acl_proxy';
import { mergeOptions } from '../../utils/general_utils';
import { getLangText } from '../../utils/lang_utils';
import { setDocumentTitle } from '../../utils/dom_utils';
let SettingsContainer = React.createClass({
@ -44,8 +47,8 @@ let SettingsContainer = React.createClass({
UserStore.unlisten(this.onChange);
},
loadUser(){
UserActions.fetchCurrentUser();
loadUser(invalidateCache){
UserActions.fetchCurrentUser(invalidateCache);
},
onChange(state) {
@ -53,6 +56,8 @@ let SettingsContainer = React.createClass({
},
render() {
setDocumentTitle(getLangText('Account settings'));
if (this.state.currentUser && this.state.currentUser.username) {
return (
<div className="settings-container">
@ -66,6 +71,7 @@ let SettingsContainer = React.createClass({
aclName="acl_view_settings_api">
<APISettings />
</AclProxy>
<WebhookSettings />
<AclProxy
aclObject={this.state.whitelabel}
aclName="acl_view_settings_bitcoin">

View File

@ -0,0 +1,165 @@
'use strict';
import React from 'react';
import WebhookStore from '../../stores/webhook_store';
import WebhookActions from '../../actions/webhook_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 AclProxy from '../acl_proxy';
import ActionPanel from '../ascribe_panel/action_panel';
import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph';
import ApiUrls from '../../constants/api_urls';
import AscribeSpinner from '../ascribe_spinner';
import { getLangText } from '../../utils/lang_utils';
let WebhookSettings = React.createClass({
propTypes: {
defaultExpanded: React.PropTypes.bool
},
getInitialState() {
return WebhookStore.getState();
},
componentDidMount() {
WebhookStore.listen(this.onChange);
WebhookActions.fetchWebhooks();
WebhookActions.fetchWebhookEvents();
},
componentWillUnmount() {
WebhookStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
onRemoveWebhook(webhookId) {
return (event) => {
WebhookActions.removeWebhook(webhookId);
let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000);
GlobalNotificationActions.appendGlobalNotification(notification);
};
},
handleCreateSuccess() {
this.refs.webhookCreateForm.reset();
WebhookActions.fetchWebhooks(true);
let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getWebhooks(){
let content = <AscribeSpinner color='dark-blue' size='lg'/>;
if (this.state.webhooks) {
content = this.state.webhooks.map(function(webhook, i) {
const event = webhook.event.split('.')[0];
return (
<ActionPanel
name={webhook.event}
key={i}
content={
<div>
<div className='ascribe-panel-title'>
{event.toUpperCase()}
</div>
<div className="ascribe-panel-subtitle">
{webhook.target}
</div>
</div>
}
buttons={
<div className="pull-right">
<div className="pull-right">
<button
className="pull-right btn btn-tertiary btn-sm"
onClick={this.onRemoveWebhook(webhook.id)}>
{getLangText('DELETE')}
</button>
</div>
</div>
}/>
);
}, this);
}
return content;
},
getEvents() {
if (this.state.webhookEvents && this.state.webhookEvents.length) {
return (
<Property
name='event'
label={getLangText('Select the event to trigger a webhook', '...')}>
<select name="events">
{this.state.webhookEvents.map((event, i) => {
return (
<option
name={i}
key={i}
value={ event + '.webhook' }>
{ event.toUpperCase() }
</option>
);
})}
</select>
</Property>);
}
return null;
},
render() {
return (
<CollapsibleParagraph
title={getLangText('Webhooks')}
defaultExpanded={this.props.defaultExpanded}>
<div>
<p>
Webhooks allow external services to receive notifications from ascribe.
Currently we support webhook notifications when someone transfers, consigns, loans or shares
(by email) a work to you.
</p>
<p>
To get started, simply choose the prefered action that you want to be notified upon and supply
a target url.
</p>
</div>
<AclProxy
show={this.state.webhookEvents && this.state.webhookEvents.length}>
<Form
ref="webhookCreateForm"
url={ApiUrls.webhooks}
handleSuccess={this.handleCreateSuccess}>
{ this.getEvents() }
<Property
name='target'
label={getLangText('Redirect Url')}>
<input
type="text"
placeholder={getLangText('Enter the url to be triggered')}
required/>
</Property>
<hr />
</Form>
</AclProxy>
{this.getWebhooks()}
</CollapsibleParagraph>
);
}
});
export default WebhookSettings;

View File

@ -1,7 +1,7 @@
'use strict';
import React from 'react/addons';
import { History } from 'react-router';
import { History, Lifecycle } from 'react-router';
import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs';
@ -17,14 +17,16 @@ const SlidesContainer = React.createClass({
pending: string,
complete: string
}),
location: object
location: object,
pageExitWarning: string
},
mixins: [History],
mixins: [History, Lifecycle],
getInitialState() {
return {
containerWidth: 0
containerWidth: 0,
pageExitWarning: null
};
},
@ -41,6 +43,10 @@ const SlidesContainer = React.createClass({
window.removeEventListener('resize', this.handleContainerResize);
},
routerWillLeave() {
return this.props.pageExitWarning;
},
handleContainerResize() {
this.setState({
// +30 to get rid of the padding of the container which is 15px + 15px left and right

View File

@ -0,0 +1,51 @@
'use strict';
import React from 'react';
import AppConstants from '../../constants/application_constants';
import { InjectInHeadUtils } from '../../utils/inject_utils';
let FacebookShareButton = React.createClass({
propTypes: {
type: React.PropTypes.string
},
getDefaultProps() {
return {
type: 'button'
};
},
componentDidMount() {
/**
* Ideally we would only use FB.XFBML.parse() on the component that we're
* mounting, but doing this when we first load the FB sdk causes unpredictable behaviour.
* The button sometimes doesn't get initialized, likely because FB hasn't properly
* been initialized yet.
*
* To circumvent this, we always have the sdk parse the entire DOM on the initial load
* (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later.
*/
InjectInHeadUtils
.inject(AppConstants.facebook.sdkUrl)
.then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) });
},
shouldComponentUpdate(nextProps) {
return this.props.type !== nextProps.type;
},
render() {
return (
<span
ref="fbShareButton"
className="fb-share-button btn btn-ascribe-social"
data-layout={this.props.type}>
</span>
);
}
});
export default FacebookShareButton;

View File

@ -0,0 +1,55 @@
'use strict';
import React from 'react';
import AppConstants from '../../constants/application_constants';
import { InjectInHeadUtils } from '../../utils/inject_utils';
let TwitterShareButton = React.createClass({
propTypes: {
count: React.PropTypes.string,
counturl: React.PropTypes.string,
hashtags: React.PropTypes.string,
size: React.PropTypes.string,
text: React.PropTypes.string,
url: React.PropTypes.string,
via: React.PropTypes.string
},
getDefaultProps() {
return {
count: 'none',
via: 'ascribeIO'
};
},
componentDidMount() {
InjectInHeadUtils.inject(AppConstants.twitter.sdkUrl).then(this.loadTwitterButton);
},
loadTwitterButton() {
const { count, counturl, hashtags, size, text, url, via } = this.props;
twttr.widgets.createShareButton(url, this.refs.twitterShareButton.getDOMNode(), {
count,
counturl,
hashtags,
size,
text,
via,
dnt: true // Do not track
});
},
render() {
return (
<span
ref="twitterShareButton"
className="btn btn-ascribe-social">
</span>
);
}
});
export default TwitterShareButton;

View File

@ -0,0 +1,35 @@
'use strict';
import React from 'react';
import classNames from 'classnames';
let AscribeSpinner = React.createClass({
propTypes: {
classNames: React.PropTypes.string,
size: React.PropTypes.oneOf(['sm', 'md', 'lg']),
color: React.PropTypes.oneOf(['blue', 'dark-blue', 'light-blue', 'pink', 'black', 'loop'])
},
getDefaultProps() {
return {
inline: false,
size: 'md',
color: 'loop'
};
},
render() {
return (
<div
className={
classNames('spinner-wrapper-' + this.props.size,
'spinner-wrapper-' + this.props.color,
this.props.classNames)}>
<div className={classNames('spinner-circle')}></div>
<div className={classNames('spinner-inner')}>A</div>
</div>
);
}
});
export default AscribeSpinner;

View File

@ -27,6 +27,7 @@ let FileDragAndDrop = React.createClass({
areAssetsEditable: React.PropTypes.bool,
enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
// triggers a FileDragAndDrop-global spinner
hashingProgress: React.PropTypes.number,
@ -41,8 +42,11 @@ let FileDragAndDrop = React.createClass({
plural: React.PropTypes.string
}),
allowedExtensions: React.PropTypes.string,
location: React.PropTypes.object
allowedExtensions: React.PropTypes.string
},
clearSelection() {
this.refs.fileSelector.getDOMNode().value = '';
},
handleDragOver(event) {
@ -81,30 +85,30 @@ let FileDragAndDrop = React.createClass({
},
handleDeleteFile(fileId) {
// input's value is not change the second time someone
// input's value is not changed the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.clearSelection();
this.props.handleDeleteFile(fileId);
},
handleCancelFile(fileId) {
// input's value is not change the second time someone
// input's value is not changed the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.clearSelection();
this.props.handleCancelFile(fileId);
},
handlePauseFile(fileId) {
// input's value is not change the second time someone
// input's value is not changed the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.clearSelection();
this.props.handlePauseFile(fileId);
},
handleResumeFile(fileId) {
// input's value is not change the second time someone
// input's value is not changed the second time someone
// inputs the same file again, therefore we need to reset its value
this.refs.fileinput.getDOMNode().value = '';
this.clearSelection();
this.props.handleResumeFile(fileId);
},
@ -133,23 +137,23 @@ let FileDragAndDrop = React.createClass({
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
}
this.refs.fileinput.getDOMNode().dispatchEvent(evt);
this.refs.fileSelector.getDOMNode().dispatchEvent(evt);
},
render: function () {
let { filesToUpload,
dropzoneInactive,
className,
hashingProgress,
handleCancelHashing,
multiple,
enableLocalHashing,
fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions,
location
} = this.props;
const {
filesToUpload,
dropzoneInactive,
className,
hashingProgress,
handleCancelHashing,
multiple,
enableLocalHashing,
uploadMethod,
fileClassToUpload,
areAssetsDownloadable,
areAssetsEditable,
allowedExtensions } = this.props;
// has files only is true if there are files that do not have the status deleted or canceled
let hasFiles = filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1).length > 0;
@ -185,8 +189,8 @@ let FileDragAndDrop = React.createClass({
hasFiles={hasFiles}
onClick={this.handleOnClick}
enableLocalHashing={enableLocalHashing}
fileClassToUpload={fileClassToUpload}
location={location}/>
uploadMethod={uploadMethod}
fileClassToUpload={fileClassToUpload} />
<FileDragAndDropPreviewIterator
files={filesToUpload}
handleDeleteFile={this.handleDeleteFile}
@ -206,7 +210,7 @@ let FileDragAndDrop = React.createClass({
*/}
<input
multiple={multiple}
ref="fileinput"
ref="fileSelector"
type="file"
style={{
visibility: 'hidden',

View File

@ -3,30 +3,28 @@
import React from 'react';
import { Link } from 'react-router';
import { getLangText } from '../../../utils/lang_utils';
import { dragAndDropAvailable } from '../../../utils/feature_detection_utils';
import { getLangText } from '../../../utils/lang_utils';
import { getCurrentQueryParams } from '../../../utils/url_utils';
let FileDragAndDropDialog = React.createClass({
propTypes: {
hasFiles: React.PropTypes.bool,
multipleFiles: React.PropTypes.bool,
onClick: React.PropTypes.func,
enableLocalHashing: React.PropTypes.bool,
uploadMethod: React.PropTypes.string,
onClick: React.PropTypes.func,
// 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
}),
location: React.PropTypes.object
})
},
getDragDialog(fileClass) {
if(dragAndDropAvailable) {
if (dragAndDropAvailable) {
return [
<p>{getLangText('Drag %s here', fileClass)}</p>,
<p>{getLangText('or')}</p>
@ -37,26 +35,37 @@ let FileDragAndDropDialog = React.createClass({
},
render() {
const queryParams = this.props.location.query;
const {
hasFiles,
multipleFiles,
enableLocalHashing,
uploadMethod,
fileClassToUpload,
onClick } = this.props;
if(this.props.hasFiles) {
if (hasFiles) {
return null;
} else {
if(this.props.enableLocalHashing && !queryParams.method) {
if (enableLocalHashing && !uploadMethod) {
const currentQueryParams = getCurrentQueryParams();
let queryParamsHash = Object.assign({}, queryParams);
const queryParamsHash = Object.assign({}, currentQueryParams);
queryParamsHash.method = 'hash';
let queryParamsUpload = Object.assign({}, queryParams);
const queryParamsUpload = Object.assign({}, currentQueryParams);
queryParamsUpload.method = 'upload';
let { location } = this.props;
return (
<div className="file-drag-and-drop-dialog present-options">
<p>{getLangText('Would you rather')}</p>
{/*
The frontend in live is hosted under /app,
Since `Link` is appending that base url, if its defined
by itself, we need to make sure to not set it at this point.
Otherwise it will be appended twice.
*/}
<Link
to={location.pathname}
to={`/${window.location.pathname.split('/').pop()}`}
query={queryParamsHash}>
<span className="btn btn-default btn-sm">
{getLangText('Hash your work')}
@ -64,9 +73,9 @@ let FileDragAndDropDialog = React.createClass({
</Link>
<span> or </span>
<Link
to={location.pathname}
to={`/${window.location.pathname.split('/').pop()}`}
query={queryParamsUpload}>
<span className="btn btn-default btn-sm">
{getLangText('Upload and hash your work')}
@ -75,26 +84,27 @@ let FileDragAndDropDialog = React.createClass({
</div>
);
} else {
if(this.props.multipleFiles) {
if (multipleFiles) {
return (
<span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.plural)}
{this.getDragDialog(fileClassToUpload.plural)}
<span
className="btn btn-default"
onClick={this.props.onClick}>
{getLangText('choose %s to upload', this.props.fileClassToUpload.plural)}
onClick={onClick}>
{getLangText('choose %s to upload', fileClassToUpload.plural)}
</span>
</span>
);
} else {
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);
const dialog = uploadMethod === 'hash' ? getLangText('choose a %s to hash', fileClassToUpload.singular)
: getLangText('choose a %s to upload', fileClassToUpload.singular);
return (
<span className="file-drag-and-drop-dialog">
{this.getDragDialog(this.props.fileClassToUpload.singular)}
{this.getDragDialog(fileClassToUpload.singular)}
<span
className="btn btn-default"
onClick={this.props.onClick}>
onClick={onClick}>
{dialog}
</span>
</span>
@ -105,4 +115,4 @@ let FileDragAndDropDialog = React.createClass({
}
});
export default FileDragAndDropDialog;
export default FileDragAndDropDialog;

View File

@ -3,7 +3,7 @@
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../../constants/application_constants';
import AscribeSpinner from '../../ascribe_spinner';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewImage = React.createClass({
@ -53,7 +53,11 @@ let FileDragAndDropPreviewImage = React.createClass({
}
} else {
actionSymbol = <img height={35} className="action-file" src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
actionSymbol = (
<div className="spinner-file">
<AscribeSpinner color='dark-blue' size='md' />
</div>
);
}
return (

View File

@ -3,7 +3,7 @@
import React from 'react';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
import AppConstants from '../../../constants/application_constants';
import AscribeSpinner from '../../ascribe_spinner';
import { getLangText } from '../../../utils/lang_utils';
let FileDragAndDropPreviewOther = React.createClass({
@ -49,7 +49,11 @@ let FileDragAndDropPreviewOther = React.createClass({
}
} else {
actionSymbol = <img height={35} src={AppConstants.baseUrl + 'static/img/ascribe_animated_medium.gif'} />;
actionSymbol = (
<div className="spinner-file">
<AscribeSpinner color='dark-blue' size='md' />
</div>
);
}
return (

View File

@ -2,24 +2,30 @@
import React from 'react';
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils';
import { getLangText } from '../../../utils/lang_utils';
import { truncateTextAtCharIndex } from '../../../utils/general_utils';
const { func, array, bool, shape, string } = React.PropTypes;
let UploadButton = React.createClass({
propTypes: {
onDrop: React.PropTypes.func.isRequired,
filesToUpload: React.PropTypes.array,
multiple: React.PropTypes.bool,
onDrop: func.isRequired,
filesToUpload: array,
multiple: 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
fileClassToUpload: shape({
singular: string,
plural: string
}),
allowedExtensions: React.PropTypes.string
allowedExtensions: string,
handleCancelFile: func // provided by ReactS3FineUploader
},
handleDrop(event) {
@ -37,11 +43,20 @@ let UploadButton = React.createClass({
return this.props.filesToUpload.filter((file) => file.status === 'uploading');
},
handleOnClick() {
let uploadingFiles = this.getUploadingFiles();
getUploadedFile() {
return this.props.filesToUpload.filter((file) => file.status === 'upload successful')[0];
},
// We only want the button to be clickable if there are no files currently uploading
handleOnClick() {
const uploadingFiles = this.getUploadingFiles();
const uploadedFile = this.getUploadedFile();
if(uploadedFile) {
this.props.handleCancelFile(uploadedFile.id);
}
if(uploadingFiles.length === 0) {
// We only want the button to be clickable if there are no files currently uploading
// 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
@ -62,40 +77,61 @@ let UploadButton = React.createClass({
// 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) {
if(this.getUploadingFiles().length !== 0) {
return getLangText('Upload progress') + ': ' + Math.ceil(filesToUpload[0].progress) + '%';
} else {
return fileClassToUpload.singular;
}
},
render() {
let {
multiple,
fileClassToUpload,
allowedExtensions
} = this.props;
getUploadedFileLabel() {
const uploadedFile = this.getUploadedFile();
if(uploadedFile) {
return (
<span>
<Glyphicon glyph="ok" />
{' ' + truncateTextAtCharIndex(uploadedFile.name, 40)}
</span>
);
} else {
return (
<span>{getLangText('No file chosen')}</span>
);
}
},
render() {
let { multiple,
allowedExtensions } = this.props;
/*
* We do not want a button that submits here.
* As UploadButton could be used in forms that want to be submitted independent
* of clicking the selector.
* Therefore the wrapping component needs to be an `anchor` tag instead of a `button`
*/
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>
<div className="upload-button-wrapper">
<a
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}/>
</a>
{this.getUploadedFileLabel()}
</div>
);
}
});

View File

@ -18,7 +18,6 @@ import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp }
import { getCookie } from '../../utils/fetch_api_utils';
import { getLangText } from '../../utils/lang_utils';
let ReactS3FineUploader = React.createClass({
propTypes: {
keyRoutine: React.PropTypes.shape({
@ -107,11 +106,14 @@ let ReactS3FineUploader = React.createClass({
// One solution we found in the process of tackling this problem was to hash
// the file in the browser using md5 and then uploading the resulting text document instead
// of the actual file.
// This boolean essentially enables that behavior
//
// This boolean and string essentially enable that behavior.
// Right now, we determine which upload method to use by appending a query parameter,
// which should be passed into 'uploadMethod':
// 'hash': upload using the hash
// 'upload': upload full file (default if not specified)
enableLocalHashing: React.PropTypes.bool,
// automatically injected by React-Router
query: React.PropTypes.object,
uploadMethod: React.PropTypes.oneOf(['hash', 'upload']),
// A class of a file the user has to upload
// Needs to be defined both in singular as well as in plural
@ -126,9 +128,7 @@ let ReactS3FineUploader = React.createClass({
fileInputElement: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.element
]),
location: React.PropTypes.object
])
},
getDefaultProps() {
@ -192,11 +192,11 @@ let ReactS3FineUploader = React.createClass({
filesToUpload: [],
uploader: new fineUploader.s3.FineUploaderBasic(this.propsToConfig()),
csrfToken: getCookie(AppConstants.csrftoken),
// -1: aborted
// -2: uninitialized
hashingProgress: -2,
// this is for logging
chunks: {}
};
@ -259,7 +259,7 @@ let ReactS3FineUploader = React.createClass({
// Resets the whole react fineuploader component to its initial state
reset() {
// Cancel all currently ongoing uploads
this.state.uploader.cancelAll();
this.cancelUploads();
// and reset component in general
this.state.uploader.reset();
@ -271,6 +271,22 @@ let ReactS3FineUploader = React.createClass({
this.setState(this.getInitialState());
},
// Cancel uploads and clear previously selected files on the input element
cancelUploads(id) {
!!id ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll();
// Reset the file input element to clear the previously selected files so that
// the user can reselect them again.
this.clearFileSelection();
},
clearFileSelection() {
const { fileInput } = this.refs;
if (fileInput && typeof fileInput.clearSelection === 'function') {
fileInput.clearSelection();
}
},
requestKey(fileId) {
let filename = this.state.uploader.getName(fileId);
let uuid = this.state.uploader.getUuid(fileId);
@ -298,18 +314,27 @@ let ReactS3FineUploader = React.createClass({
resolve(res.key);
})
.catch((err) => {
console.logGlobal(err, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
});
this.onErrorPromiseProxy(err);
reject(err);
});
});
},
createBlob(file) {
const { createBlobRoutine } = this.props;
return Q.Promise((resolve, reject) => {
window.fetch(this.props.createBlobRoutine.url, {
// if createBlobRoutine is not defined,
// we're progressing right away without posting to S3
// so that this can be done manually by the form
if(!createBlobRoutine) {
// still we warn the user of this component
console.warn('createBlobRoutine was not defined for ReactS3FineUploader. Continuing without creating the blob on the server.');
resolve();
}
window.fetch(createBlobRoutine.url, {
method: 'post',
headers: {
'Accept': 'application/json',
@ -320,13 +345,13 @@ let ReactS3FineUploader = React.createClass({
body: JSON.stringify({
'filename': file.name,
'key': file.key,
'piece_id': this.props.createBlobRoutine.pieceId
'piece_id': createBlobRoutine.pieceId
})
})
.then((res) => {
return res.json();
})
.then((res) =>{
.then((res) => {
if(res.otherdata) {
file.s3Url = res.otherdata.url_safe;
file.s3UrlSafe = res.otherdata.url_safe;
@ -336,16 +361,16 @@ let ReactS3FineUploader = React.createClass({
} else if(res.contractblob) {
file.s3Url = res.contractblob.url_safe;
file.s3UrlSafe = res.contractblob.url_safe;
} else if(res.thumbnail) {
file.s3Url = res.thumbnail.url_safe;
file.s3UrlSafe = res.thumbnail.url_safe;
} else {
throw new Error(getLangText('Could not find a url to download.'));
}
resolve(res);
})
.catch((err) => {
console.logGlobal(err, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
});
this.onErrorPromiseProxy(err);
reject(err);
});
});
@ -354,7 +379,6 @@ let ReactS3FineUploader = React.createClass({
/* FineUploader specific callback function handlers */
onUploadChunk(id, name, chunkData) {
let chunks = this.state.chunks;
chunks[id + '-' + chunkData.startByte + '-' + chunkData.endByte] = {
@ -370,10 +394,9 @@ let ReactS3FineUploader = React.createClass({
},
onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
let chunks = this.state.chunks;
let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte;
if(chunks[chunkKey]) {
chunks[chunkKey].completed = true;
chunks[chunkKey].responseJson = responseJson;
@ -387,13 +410,15 @@ let ReactS3FineUploader = React.createClass({
},
onComplete(id, name, res, xhr) {
// there has been an issue with the server's connection
if((xhr && xhr.status === 0) || res.error) {
console.logGlobal(new Error(res.error || 'Complete was called but there wasn\t a success'), false, {
// There has been an issue with the server's connection
if (xhr && xhr.status === 0 && res.success) {
console.logGlobal(new Error('Upload succeeded with a status code 0'), false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
chunks: this.state.chunks,
xhr: this.getXhrErrorComment(xhr)
});
} else {
// onError will catch any errors, so we can ignore them here
} else if (!res.error || res.success) {
let files = this.state.filesToUpload;
// Set the state of the completed file to 'upload successful' in order to
@ -412,9 +437,9 @@ let ReactS3FineUploader = React.createClass({
if(this.props.submitFile) {
this.props.submitFile(files[id]);
} else {
console.warn('You didn\'t define submitFile in as a prop in react-s3-fine-uploader');
console.warn('You didn\'t define submitFile 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,
@ -429,28 +454,46 @@ let ReactS3FineUploader = React.createClass({
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);
});
.catch(this.onErrorPromiseProxy);
}
},
onError(id, name, errorReason) {
/**
* We want to channel all errors in this component through one single method.
* As fineuploader's `onError` method cannot handle the callback parameters of
* a promise we define this proxy method to crunch them into the correct form.
*
* @param {error} err a plain Javascript error
*/
onErrorPromiseProxy(err) {
this.onError(null, null, err.message);
},
onError(id, name, errorReason, xhr) {
console.logGlobal(errorReason, false, {
files: this.state.filesToUpload,
chunks: this.state.chunks
chunks: this.state.chunks,
xhr: this.getXhrErrorComment(xhr)
});
this.state.uploader.cancelAll();
this.props.setIsUploadReady(true);
this.cancelUploads();
let notification = new GlobalNotificationModel(errorReason || this.props.defaultErrorMessage, 'danger', 5000);
GlobalNotificationActions.appendGlobalNotification(notification);
},
getXhrErrorComment(xhr) {
if (xhr) {
return {
response: xhr.response,
url: xhr.responseURL,
status: xhr.status,
statusText: xhr.statusText
};
}
},
isFileValid(file) {
if(file.size > this.props.validation.sizeLimit) {
@ -588,7 +631,7 @@ let ReactS3FineUploader = React.createClass({
},
handleCancelFile(fileId) {
this.state.uploader.cancel(fileId);
this.cancelUploads(fileId);
},
handlePauseFile(fileId) {
@ -597,7 +640,6 @@ let ReactS3FineUploader = React.createClass({
} else {
throw new Error(getLangText('File upload could not be paused.'));
}
},
handleResumeFile(fileId) {
@ -609,9 +651,14 @@ let ReactS3FineUploader = React.createClass({
},
handleUploadFile(files) {
// While files are being uploaded, the form cannot be ready
// for submission
this.props.setIsUploadReady(false);
// If multiple set and user already uploaded its work,
// cancel upload
if(!this.props.multiple && this.state.filesToUpload.filter(displayValidFilesFilter).length > 0) {
this.clearFileSelection();
return;
}
@ -647,16 +694,14 @@ let ReactS3FineUploader = React.createClass({
// md5 hash of a file locally and just upload a txt file containing that hash.
//
// In the view this only happens when the user is allowed to do local hashing as well
// as when the correct query parameter is present in the url ('hash' and not 'upload')
let queryParams = this.props.location.query;
if(this.props.enableLocalHashing && queryParams && queryParams.method === 'hash') {
let convertedFilePromises = [];
// as when the correct method prop is present ('hash' and not 'upload')
if (this.props.enableLocalHashing && this.props.uploadMethod === 'hash') {
const convertedFilePromises = [];
let overallFileSize = 0;
// "files" is not a classical Javascript array but a Javascript FileList, therefore
// we can not use map to convert values
for(let i = 0; i < files.length; i++) {
// for calculating the overall progress of all submitted files
// we'll need to calculate the overall sum of all files' sizes
overallFileSize += files[i].size;
@ -668,7 +713,6 @@ let ReactS3FineUploader = React.createClass({
// we're using promises to handle that
let hashedFilePromise = computeHashOfFile(files[i]);
convertedFilePromises.push(hashedFilePromise);
}
// To react after the computation of all files, we define the resolvement
@ -676,7 +720,6 @@ let ReactS3FineUploader = React.createClass({
// with their txt representative
Q.all(convertedFilePromises)
.progress(({index, value: {progress, reject}}) => {
// hashing progress has been aborted from outside
// To get out of the executing, we need to call reject from the
// inside of the promise's execution.
@ -696,18 +739,14 @@ let ReactS3FineUploader = React.createClass({
// currently hashing files
let overallHashingProgress = 0;
for(let i = 0; i < files.length; i++) {
let filesSliceOfOverall = files[i].size / overallFileSize;
overallHashingProgress += filesSliceOfOverall * files[i].progress;
}
// Multiply by 100, since react-progressbar expects decimal numbers
this.setState({ hashingProgress: overallHashingProgress * 100});
})
.then((convertedFiles) => {
// clear hashing progress, since its done
this.setState({ hashingProgress: -2});
@ -823,20 +862,18 @@ let ReactS3FineUploader = React.createClass({
changeSet.status = { $set: status };
let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet });
this.setState({ filesToUpload });
},
isDropzoneInactive() {
let filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
let queryParams = this.props.location.query;
const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1);
if((this.props.enableLocalHashing && !queryParams.method) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) {
return true;
} else {
return false;
}
},
getAllowedExtensions() {
@ -850,28 +887,24 @@ let ReactS3FineUploader = React.createClass({
},
render() {
let {
const {
multiple,
areAssetsDownloadable,
areAssetsEditable,
onInactive,
enableLocalHashing,
fileClassToUpload,
validation,
fileInputElement,
location
} = this.props;
fileInputElement: FileInputElement,
uploadMethod } = this.props;
// Here we initialize the template that has been either provided from the outside
// or the default input that is FileDragAndDrop.
return React.createElement(fileInputElement, {
const props = {
multiple,
areAssetsDownloadable,
areAssetsEditable,
onInactive,
enableLocalHashing,
uploadMethod,
fileClassToUpload,
location,
onDrop: this.handleUploadFile,
filesToUpload: this.state.filesToUpload,
handleDeleteFile: this.handleDeleteFile,
@ -882,10 +915,14 @@ let ReactS3FineUploader = React.createClass({
dropzoneInactive: this.isDropzoneInactive(),
hashingProgress: this.state.hashingProgress,
allowedExtensions: this.getAllowedExtensions()
});
}
};
return (
<FileInputElement
ref="fileInput"
{...props} />
);
}
});
export default ReactS3FineUploader;

View File

@ -4399,7 +4399,9 @@ qq.UploadHandlerController = function(o, namespace) {
}
)
.done(function() {
handler.clearXhr(id, chunkIdx);
if (handler._getFileState(id)) {
handler.clearXhr(id, chunkIdx);
}
}) ;
}
}
@ -8681,7 +8683,7 @@ qq.s3.RequestSigner = function(o) {
options.log(errorMessage, "error");
}
promise.failure(errorMessage);
promise.failure(errorMessage, xhrOrXdr);
}
else {
promise.success(response);
@ -8813,7 +8815,7 @@ qq.s3.RequestSigner = function(o) {
credentialsProvider.get().accessKey,
credentialsProvider.get().sessionToken);
}, function(errorMsg) {
options.log("Attempt to update expired credentials apparently failed! Unable to sign request. ", "error");
options.log("Attempt to update expired credentials apparently failed! Unable to sign request: " + errorMsg, "error");
signatureEffort.failure("Unable to sign request - expired credentials.");
});
}
@ -9627,8 +9629,8 @@ qq.s3.XhrUploadHandler = function(spec, proxy) {
});
xhr.send(chunkData.blob);
}, function() {
promise.failure({error: "Problem signing the chunk!"}, xhr);
}, function(errorMsg, xhr) {
promise.failure({error: "Problem signing the chunk: " + errorMsg}, xhr);
});
return promise;
@ -9672,10 +9674,10 @@ qq.s3.XhrUploadHandler = function(spec, proxy) {
uploadIdPromise.success(uploadId);
promise.success(uploadId);
},
function(errorMsg) {
function(errorMsg, xhr) {
handler._getPersistableData(id).uploadId = null;
promise.failure(errorMsg);
uploadIdPromise.failure(errorMsg);
promise.failure(errorMsg, xhr);
uploadIdPromise.failure(errorMsg, xhr);
}
);
}

File diff suppressed because one or more lines are too long

View File

@ -9,12 +9,17 @@ import Form from './ascribe_forms/form';
import Property from './ascribe_forms/property';
import InputTextAreaToggable from './ascribe_forms/input_textarea_toggable';
import AscribeSpinner from './ascribe_spinner';
import ApiUrls from '../constants/api_urls';
import { getLangText } from '../utils/lang_utils';
import { setDocumentTitle } from '../utils/dom_utils';
let CoaVerifyContainer = React.createClass({
render() {
setDocumentTitle(getLangText('Verify your Certificate of Authenticity'));
return (
<div className="ascribe-login-wrapper">
<br/>
@ -59,12 +64,12 @@ let CoaVerifyForm = React.createClass({
buttons={
<button
type="submit"
className="btn ascribe-btn ascribe-btn-login">
className="btn btn-default btn-wide">
{getLangText('Verify your Certificate of Authenticity')}
</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 className="btn btn-default btn-wide btn-spinner">
<AscribeSpinner color="dark-blue" size="md" />
</span>
}>
<Property

View File

@ -1,36 +0,0 @@
'use strict';
import React from 'react';
import NotificationStore from '../stores/notification_store';
import { mergeOptions } from '../utils/general_utils';
let ContractNotification = React.createClass({
getInitialState() {
return mergeOptions(
NotificationStore.getState()
);
},
componentDidMount() {
NotificationStore.listen(this.onChange);
},
componentWillUnmount() {
NotificationStore.unlisten(this.onChange);
},
onChange(state) {
this.setState(state);
},
render() {
return (
null
);
}
});
export default ContractNotification;

View File

@ -8,11 +8,10 @@ let Footer = React.createClass({
render() {
return (
<div className="ascribe-footer">
<hr />
<p className="ascribe-sub-sub-statement">
<br />
<a href="https://github.com/ascribe/REST-main/" target="_blank">api</a> |
<a href="https://www.ascribe.io/impressum/" target="_blank"> impressum</a> |
<a href="http://docs.ascribe.apiary.io/" target="_blank">api</a> |
<a href="https://www.ascribe.io/imprint/" target="_blank"> {getLangText('imprint')}</a> |
<a href="https://www.ascribe.io/terms/" target="_blank"> {getLangText('terms of service')}</a> |
<a href="https://www.ascribe.io/privacy/" target="_blank"> {getLangText('privacy')}</a>
</p>

View File

@ -1,6 +1,9 @@
'use strict';
import React from 'react';
import { Link } from 'react-router';
import history from '../history';
import Nav from 'react-bootstrap/lib/Nav';
import Navbar from 'react-bootstrap/lib/Navbar';
@ -29,6 +32,8 @@ import NavRoutesLinks from './nav_routes_links';
import { mergeOptions } from '../utils/general_utils';
import { getLangText } from '../utils/lang_utils';
import { constructHead } from '../utils/dom_utils';
let Header = React.createClass({
propTypes: {
@ -54,21 +59,37 @@ let Header = React.createClass({
UserStore.listen(this.onChange);
WhitelabelActions.fetchWhitelabel();
WhitelabelStore.listen(this.onChange);
// react-bootstrap 0.25.1 has a bug in which it doesn't
// close the mobile expanded navigation after a click by itself.
// To get rid of this, we set the state of the component ourselves.
history.listen(this.onRouteChange);
},
componentWillUnmount() {
UserStore.unlisten(this.onChange);
WhitelabelStore.unlisten(this.onChange);
history.unlisten(this.onRouteChange);
},
getLogo(){
if (this.state.whitelabel && this.state.whitelabel.logo){
return <img className="img-brand" src={this.state.whitelabel.logo} />;
getLogo() {
let { whitelabel } = this.state;
if (whitelabel.head) {
constructHead(whitelabel.head);
}
if (whitelabel.subdomain && whitelabel.subdomain !== 'www' && whitelabel.logo){
return (
<Link to="/collection">
<img className="img-brand" src={whitelabel.logo} alt="Whitelabel brand"/>
</Link>
);
}
return (
<span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
<Link className="icon-ascribe-logo" to="/collection"/>
</span>
);
},
@ -79,10 +100,9 @@ let Header = React.createClass({
aclObject={this.state.whitelabel}
aclName="acl_view_powered_by">
<li>
<a className="pull-right" href="https://www.ascribe.io/" target="_blank">
<a className="pull-right ascribe-powered-by" href="https://www.ascribe.io/" target="_blank">
<span id="powered">{getLangText('powered by')} </span>
<span>ascribe </span>
<span className="glyph-ascribe-spool-chunked ascribe-color"></span>
<span className="icon-ascribe-logo"></span>
</a>
</li>
</AclProxy>
@ -122,6 +142,13 @@ let Header = React.createClass({
this.refs.dropdownbutton.setDropdownState(false);
},
// On route change, close expanded navbar again since react-bootstrap doesn't close
// the collapsibleNav by itself on click. setState() isn't available on a ref so
// doing this explicitly is the only way for now.
onRouteChange() {
this.refs.navbar.state.navExpanded = false;
},
render() {
let account;
let signup;
@ -188,13 +215,15 @@ let Header = React.createClass({
<Navbar
brand={this.getLogo()}
toggleNavKey={0}
fixedTop={true}>
<CollapsibleNav eventKey={0}>
fixedTop={true}
ref="navbar">
<CollapsibleNav
eventKey={0}>
<Nav navbar left>
{this.getPoweredBy()}
</Nav>
<Nav navbar right>
<HeaderNotificationDebug show={false}/>
<HeaderNotificationDebug show = {false}/>
{account}
{signup}
</Nav>

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