mirror of
https://github.com/ascribe/onion.git
synced 2024-06-30 13:41:57 +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:
commit
5a6c827f0b
|
@ -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,
|
||||
|
|
22
README.md
22
README.md
|
@ -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
|
||||
=====================
|
||||
|
|
|
@ -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
BIN
fonts/ascribe-logo.eot
Normal file
Binary file not shown.
19
fonts/ascribe-logo.svg
Normal file
19
fonts/ascribe-logo.svg
Normal 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=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" d="M796.444 459.378h-273.067v273.067h-34.133v-273.067h-261.689v-34.133h261.689v-261.689h34.133v261.689h273.067z" />
|
||||
<glyph unicode="" 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="" 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
BIN
fonts/ascribe-logo.ttf
Normal file
Binary file not shown.
BIN
fonts/ascribe-logo.woff
Normal file
BIN
fonts/ascribe-logo.woff
Normal file
Binary file not shown.
|
@ -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 |
|
@ -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());
|
||||
});
|
||||
|
|
27
index.html
27
index.html
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
19
js/actions/webhook_actions.js
Normal file
19
js/actions/webhook_actions.js
Normal 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);
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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')}
|
||||
<a href="register_piece">{getLangText('here')}</a>!
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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,56 +31,92 @@ 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>
|
||||
);
|
||||
}
|
||||
|
|
139
js/components/ascribe_buttons/acl_information.js
Normal file
139
js/components/ascribe_buttons/acl_information.js
Normal 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;
|
79
js/components/ascribe_buttons/acls/acl_button.js
Normal file
79
js/components/ascribe_buttons/acls/acl_button.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
14
js/components/ascribe_buttons/acls/consign_button.js
Normal file
14
js/components/ascribe_buttons/acls/consign_button.js
Normal 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')
|
||||
});
|
14
js/components/ascribe_buttons/acls/email_button.js
Normal file
14
js/components/ascribe_buttons/acls/email_button.js
Normal 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")
|
||||
});
|
14
js/components/ascribe_buttons/acls/loan_button.js
Normal file
14
js/components/ascribe_buttons/acls/loan_button.js
Normal 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')
|
||||
});
|
14
js/components/ascribe_buttons/acls/loan_request_button.js
Normal file
14
js/components/ascribe_buttons/acls/loan_request_button.js
Normal 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')
|
||||
});
|
14
js/components/ascribe_buttons/acls/transfer_button.js
Normal file
14
js/components/ascribe_buttons/acls/transfer_button.js
Normal 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')
|
||||
});
|
14
js/components/ascribe_buttons/acls/unconsign_button.js
Normal file
14
js/components/ascribe_buttons/acls/unconsign_button.js
Normal 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')
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,8 +89,7 @@ let FurtherDetailsFileuploader = React.createClass({
|
|||
}}
|
||||
areAssetsDownloadable={true}
|
||||
areAssetsEditable={this.props.editable}
|
||||
multiple={this.props.multiple}
|
||||
location={this.props.location}/>
|
||||
multiple={this.props.multiple} />
|
||||
</Property>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
128
js/components/ascribe_forms/acl_form_factory.js
Normal file
128
js/components/ascribe_forms/acl_form_factory.js
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')}?</p>
|
||||
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
|
||||
</Form>
|
||||
|
|
|
@ -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')}?</p>
|
||||
<p>{getLangText('This is an irrevocable action%s', '.')}</p>
|
||||
</Form>
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')}?</p>
|
||||
|
|
|
@ -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')}?</p>
|
||||
|
|
|
@ -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,7 +160,7 @@ let RequestActionForm = React.createClass({
|
|||
return (
|
||||
<ActionPanel
|
||||
content={this.getContent()}
|
||||
buttons={this.getButtonForm()}/>
|
||||
buttons={this.getButtonForm()} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,7 +71,7 @@ let ShareForm = React.createClass({
|
|||
rows={1}
|
||||
defaultValue={this.props.message}
|
||||
placeholder={getLangText('Enter a message...')}
|
||||
required="required"/>
|
||||
required />
|
||||
</Property>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,8 +141,8 @@ 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} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">←</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">→</span>
|
||||
{getLangText('Next')} <span aria-hidden="true"><Glyphicon glyph='chevron-right'/></span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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()}>·</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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,7 +96,11 @@ 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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,8 +88,7 @@ let ContractSettingsUpdateButton = React.createClass({
|
|||
plural: getLangText('UPDATE')
|
||||
}}
|
||||
isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile}
|
||||
submitFile={this.submitFile}
|
||||
location={this.props.location}/>
|
||||
submitFile={this.submitFile} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
165
js/components/ascribe_settings/webhook_settings.js
Normal file
165
js/components/ascribe_settings/webhook_settings.js
Normal 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;
|
|
@ -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
|
||||
|
|
51
js/components/ascribe_social_share/facebook_share_button.js
Normal file
51
js/components/ascribe_social_share/facebook_share_button.js
Normal 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;
|
55
js/components/ascribe_social_share/twitter_share_button.js
Normal file
55
js/components/ascribe_social_share/twitter_share_button.js
Normal 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;
|
35
js/components/ascribe_spinner.js
Normal file
35
js/components/ascribe_spinner.js
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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')}
|
||||
|
@ -66,7 +75,7 @@ let FileDragAndDropDialog = React.createClass({
|
|||
<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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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,7 +394,6 @@ let ReactS3FineUploader = React.createClass({
|
|||
},
|
||||
|
||||
onUploadChunkSuccess(id, chunkData, responseJson, xhr) {
|
||||
|
||||
let chunks = this.state.chunks;
|
||||
let chunkKey = id + '-' + chunkData.startByte + '-' + chunkData.endByte;
|
||||
|
||||
|
@ -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,7 +437,7 @@ 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
|
||||
|
@ -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});
|
||||
|
||||
|
@ -828,15 +867,13 @@ let ReactS3FineUploader = React.createClass({
|
|||
},
|
||||
|
||||
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;
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user