From e0b35e0a24d9fcb05448c6144acdece7b74bd27c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 8 Dec 2015 20:55:13 +0100 Subject: [PATCH] Merge with master --- README.md | 1 + fonts/ascribe-font.eot | Bin 0 -> 3020 bytes fonts/{ascribe-logo.svg => ascribe-font.svg} | 7 +- fonts/ascribe-font.ttf | Bin 0 -> 2836 bytes fonts/ascribe-font.woff | Bin 0 -> 2912 bytes fonts/ascribe-logo.eot | Bin 2856 -> 0 bytes fonts/ascribe-logo.ttf | Bin 2692 -> 0 bytes fonts/ascribe-logo.woff | Bin 2768 -> 0 bytes js/actions/contract_agreement_list_actions.js | 40 +- js/actions/edition_actions.js | 4 +- js/actions/global_notification_actions.js | 6 +- js/actions/piece_actions.js | 4 +- js/actions/webhook_actions.js | 19 + .../accordion_list_item_edition_widget.js | 3 +- .../accordion_list_item_piece.js | 28 +- ...cordion_list_item_thumbnail_placeholder.js | 15 + .../accordion_list_item_wallet.js | 23 +- .../ascribe_buttons/acl_button_list.js | 2 +- .../ascribe_buttons/acls/acl_button.js | 2 +- .../collapsible_paragraph.js | 4 +- .../ascribe_detail/detail_property.js | 1 + js/components/ascribe_detail/edition.js | 34 +- .../ascribe_detail/edition_action_panel.js | 26 +- .../ascribe_detail/edition_container.js | 33 +- .../ascribe_detail/further_details.js | 6 +- .../further_details_fileuploader.js | 6 +- js/components/ascribe_detail/piece.js | 5 +- .../ascribe_detail/piece_container.js | 55 ++- .../ascribe_forms/acl_form_factory.js | 10 +- js/components/ascribe_forms/form.js | 15 +- js/components/ascribe_forms/form_consign.js | 72 +++- js/components/ascribe_forms/form_loan.js | 248 ++++-------- .../ascribe_forms/form_loan_request_answer.js | 6 +- .../ascribe_forms/form_register_piece.js | 27 +- ...ent.js => form_send_contract_agreement.js} | 6 +- .../ascribe_forms/form_unconsign_request.js | 4 +- .../input_contract_agreement_checkbox.js | 206 ++++++++++ .../ascribe_forms/input_fineuploader.js | 35 +- .../ascribe_forms/input_textarea_toggable.js | 6 + .../list_form_request_actions.js | 4 +- js/components/ascribe_forms/property.js | 44 +- .../piece_list_bulk_modal.js | 102 +---- .../piece_list_toolbar_filter_widget.js | 10 +- .../proxy_routes/auth_proxy_handler.js | 6 +- .../ascribe_settings/settings_container.js | 2 + .../ascribe_settings/webhook_settings.js | 165 ++++++++ .../slides_container.js | 14 +- .../facebook_share_button.js | 16 +- js/components/ascribe_spinner.js | 18 +- .../file_drag_and_drop.js | 68 ++-- .../ascribe_upload_button/upload_button.js | 101 +++-- .../react_s3_fine_uploader.js | 13 +- js/components/contract_notification.js | 36 -- js/components/error_not_found_page.js | 12 +- js/components/footer.js | 1 + js/components/global_notification.js | 95 ++--- js/components/header.js | 22 +- js/components/piece_list.js | 205 ++++++++-- js/components/piece_list_filter_display.js | 2 +- js/components/register_piece.js | 16 +- .../components/pr_login_container.js | 0 .../components/pr_register_piece.js | 5 + .../ascribe_detail/prize_piece_container.js | 17 +- ...cordion_list_item_thumbnail_placeholder.js | 15 + .../components/23vivi/23vivi_landing.js | 78 ++++ .../components/23vivi/23vivi_piece_list.js | 24 ++ .../ascribe_detail/wallet_piece_container.js | 2 +- .../cyland_detail/cyland_piece_container.js | 2 +- .../cyland_additional_data_form.js | 16 +- .../components/cyland/cyland_landing.js | 4 +- .../cyland/cyland_register_piece.js | 21 +- .../ikonotv_detail/ikonotv_piece_container.js | 2 +- .../ikonotv_artist_details_form.js | 8 +- .../ikonotv_artwork_details_form.js | 12 +- .../ikonotv/ikonotv_register_piece.js | 60 ++- .../components/lumenus/lumenus_landing.js | 84 ++++ .../market_buttons/market_acl_button_list.js | 74 ++++ .../market_buttons/market_submit_button.js | 160 ++++++++ .../market_detail/market_edition_container.js | 24 ++ .../market_detail/market_further_details.js | 23 ++ .../market_detail/market_piece_container.js | 21 + .../market_additional_data_form.js | 235 +++++++++++ .../components/market/market_piece_list.js | 90 +++++ .../market/market_register_piece.js | 174 ++++++++ .../wallet/constants/wallet_api_urls.js | 16 +- js/components/whitelabel/wallet/wallet_app.js | 2 +- .../whitelabel/wallet/wallet_routes.js | 89 ++++- js/constants/api_urls.js | 3 + js/constants/application_constants.js | 21 +- js/mixins/react_error.js | 16 + js/models/errors.js | 31 ++ js/sources/webhook_source.js | 46 +++ js/stores/edition_list_store.js | 2 +- js/stores/edition_store.js | 6 + js/stores/global_notification_store.js | 57 ++- js/stores/piece_store.js | 6 + js/stores/webhook_store.js | 88 ++++ js/utils/acl_utils.js | 22 +- js/utils/error_utils.js | 3 +- js/utils/file_utils.js | 7 +- js/utils/form_utils.js | 6 + js/utils/general_utils.js | 8 +- js/utils/inject_utils.js | 23 +- js/utils/regex_utils.js | 7 + js/utils/requests.js | 19 +- package.json | 5 +- sass/ascribe-fonts/ascribe-fonts.scss | 57 +-- sass/ascribe_accordion_list.scss | 32 +- sass/ascribe_acl_information.scss | 2 +- sass/ascribe_custom_style.scss | 64 +-- sass/ascribe_notification_list.scss | 7 +- sass/ascribe_notification_page.scss | 11 +- sass/ascribe_panel.scss | 2 +- sass/ascribe_piece_list_toolbar.scss | 4 + sass/ascribe_spinner.scss | 18 +- sass/ascribe_uploader.scss | 9 +- sass/main.scss | 2 +- .../wallet/23vivi/23vivi_custom_style.scss | 377 ++++++++++++++++++ .../whitelabel/wallet/cc/cc_custom_style.scss | 16 + .../wallet/cyland/cyland_custom_style.scss | 108 +++-- .../wallet/ikonotv/ikonotv_custom_style.scss | 36 ++ sass/whitelabel/wallet/index.scss | 1 + 122 files changed, 3342 insertions(+), 949 deletions(-) create mode 100755 fonts/ascribe-font.eot rename fonts/{ascribe-logo.svg => ascribe-font.svg} (90%) mode change 100644 => 100755 create mode 100755 fonts/ascribe-font.ttf create mode 100755 fonts/ascribe-font.woff delete mode 100644 fonts/ascribe-logo.eot delete mode 100644 fonts/ascribe-logo.ttf delete mode 100644 fonts/ascribe-logo.woff create mode 100644 js/actions/webhook_actions.js create mode 100644 js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js rename js/components/ascribe_forms/{form_contract_agreement.js => form_send_contract_agreement.js} (97%) create mode 100644 js/components/ascribe_forms/input_contract_agreement_checkbox.js create mode 100644 js/components/ascribe_settings/webhook_settings.js delete mode 100644 js/components/contract_notification.js create mode 100644 js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js create mode 100644 js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js create mode 100644 js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js create mode 100644 js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js create mode 100644 js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js create mode 100644 js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js create mode 100644 js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js create mode 100644 js/components/whitelabel/wallet/components/market/market_piece_list.js create mode 100644 js/components/whitelabel/wallet/components/market/market_register_piece.js create mode 100644 js/mixins/react_error.js create mode 100644 js/models/errors.js create mode 100644 js/sources/webhook_source.js create mode 100644 js/stores/webhook_store.js create mode 100644 js/utils/regex_utils.js create mode 100644 sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss diff --git a/README.md b/README.md index 36f47954..e07eca0d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Additionally, to work on the white labeling functionality, you need to edit your 127.0.0.1 sluice.localhost.com 127.0.0.1 lumenus.localhost.com 127.0.0.1 portfolioreview.localhost.com +127.0.0.1 23vivi.localhost.com ``` diff --git a/fonts/ascribe-font.eot b/fonts/ascribe-font.eot new file mode 100755 index 0000000000000000000000000000000000000000..860c534b024e95a4fed9250ff21ac9b15619115e GIT binary patch literal 3020 zcma(TZA@F&^_=(o0fri!*w{40Ha5nAfU&Xh3*dmsh%vzsAb|pdB#vXi1TZmVgJr3b zR8c3$luVmIRXathqG`XVCau#{st9%K{;W#1s?t`qkMVq>z%iX0QA%mIN03~ZXu&Y`ghZrY_>i3>4V7QJOC3!VG&}Gf&`p}IMl-= zBw-$wNK^}hM45t%FiTP?qLjffQBtIlkff~O0T;N5^S|s6n*~t>SAYli@e>V=C3{p} zqP$N1Ol&T?uoKz&EeTZ+K0Xy)T%fs>@MW3{re;?rBYVn!Mfeo})t{#0(R2D2zV|$_ zMu_j9CW78xO7;qF5neStx3t{;bKD^OmxKpqld-7!2O1UOe<$2J7hPTe3)TV@k`J)X zN9W>i)_!n~a5wpQVIq%ex4oo}7_LXs`Xqtd|M*;W8W&rV*Y&aSI8RO-CCN(G4akZ8yI zA-seR0+2#|ZQvf>Ba;l92sTfgB>I}in8*AlJ+gaSAq5Q@gU%S3;oPKze ze?5^No94G3b|#GL>tqKqzv0tl$3#aHl-Pz%J{M=VX*q-5%y}FA0tybBi)-?=5oRW; zmb3F|@t(J(Be^j&w7$^Mv9LZga8tlWQ~Gw_>SQ1=x!TvaI@Q`bwc6*vM&af_PHJc) z+0o(^@0s3aynNYgP9U#?3ibl*#l3C z@b|m?D!%qx0L5T~LuAIzdxc7)m*@AP-LdN+(8_r1Z1Z!A#um}TRrh=`){g4-(_jAh z{OljOx&GCy^XIo#`*U+!*QGn-<99A~yGyb0ox_i<69%t6Qv(in59BsUG2P@I8_6lC3 z*Qpka_TA3xQvBJ(3?BT&Q^%2YAvF4VsfYllpYiS-kY~eBoBQtHmg%h5G#}`7Y&)Sw|{X+p8$0-!)uW+CL zWT5qguUz~V_vXG4YuQAZZNyhy9eFh!(>*YJo<&6yiJ zYihc@`v(o9xcqKaf1l4AQ5OAFGuEd!_&kk1M^#;I!^z>{9<5e5+}0}8*LaSVyF>Ms ziJEY%wKdx52$|g4Gw$J)W8Etwo>4;sexm8<_-^_-4i63$6}eQ((1#|wUCQ>)>FeAj z@r|yPXBrQO@s)@D%b}ombR{H2!v_zBqr#zRxVAPNJ!B2e9k0)sJkvN>b!5ERTKbf& z??|1iv)96Pzt`OS-t*C@E}LBH>m!C8oE5-mgl`7c!(kvZ8dGC9^$mF@ zI3vpN$807WpzH)QgQcdS_uNdc);PQj_t1<%9K%iC$A|e@$y*5?U~dPtTvQSw4_eri zV?uiUq!p7S)$l`^&J+Ep3@ZqJS%#JL##YI&is%U$*1*&7J60hm3W;JOP8Q~<(EEuD z^F$wzVFkgPGOVQ1uR(@YM87D*8t}n`=wd9DI2*5@OwKR;w{mhgo?1*K=dB)>`Z!$UWf{IaX1H7Dw|hm?4}|*3BA-=`Rt;-l4?kKH@{)}H*B4I5C8xG literal 0 HcmV?d00001 diff --git a/fonts/ascribe-logo.svg b/fonts/ascribe-font.svg old mode 100644 new mode 100755 similarity index 90% rename from fonts/ascribe-logo.svg rename to fonts/ascribe-font.svg index 2a9bb79b..2628dfee --- a/fonts/ascribe-logo.svg +++ b/fonts/ascribe-font.svg @@ -3,7 +3,7 @@ Generated by IcoMoon - + @@ -12,9 +12,10 @@ - - + + + \ No newline at end of file diff --git a/fonts/ascribe-font.ttf b/fonts/ascribe-font.ttf new file mode 100755 index 0000000000000000000000000000000000000000..a66d8f04d7235861814ad5bb234306b9c4145e44 GIT binary patch literal 2836 zcma(TZA@F&^_=(o0fri!UZ-MkpLg!_b7MMb z%eC%1-=FW^^9%?8MQ{yxaGyTY)Le2v<0Z};v@b*##pS)o-fsY~is;E%ab=lcDbcG0 z3uYJAW+DeFe@XN;0L`D~V&cWJ7r*x`iN;9opCd+@y_EbFJV$iR++u3A^Jln4^e>1G zEF_|$_J=wR(SIk}x+tzLg9YmW3MmFym&C=`TlF7YB-%|e-ds+sq$p2Xy3d##c-0s9 z9H;@f7rEaP=XMt2U%`uoOfPRCbGdH?0rpDy-OMfodDO>i00y#hYE1%}m{3dR zjKJ+%8Id@jDN@juWhSnkv-4@`zPGI_u{AQXx!l#Yyg4#-TfpXu^qqnA znLuD>ePCdHw!M9JeZYau!tJ4)*2q?(tIaFjuXvlu@^!N{fzk&J90WLs2L%IGqaKY0 zE$^#!5)OF1@K<>6A#g`js_$rdUivAYk}mVR>Ay&(SFw#Vr8BBxjr48;Usmf>_>oji zZ$2Ys?(q_T56Yk({M4;n9l5$%zAm?e3Z?Qr`L6+`==P1iz8l-o==SwUaFB1x zT=Bbd>|726nU7E4E?>oc@Xa&CS3OD%f448L>g%`#Pz*LWMsDo9SEx35dHxXE9s3Rf zb;e_7FF)5b)LO%1!B-jB{Vl!L2FFzBO%MJXqsA@s#`Al(A8le5P|{%;!0q3|>nfsKd8wE#4_ZIP54W5L{1MinZ_9 zDk^QZ$_ksQr-yH997=^wUUl{K9Jv|}r-mCNmg5txZ4&{jH89cEI&s{>WsXMXJA%t+ zJV#G2ht{95r55}n0UO7uROzqqkpFC;{fw_t`WE-rp)qUuRJm=;S6dr-KOTw1*)BiV z)pd?-3=NN3EJqs3d_~t8^>|=-I~DxpEsf|`iX90pv>s=H2WMi z4fRcDM@Regdf|9SyU6_N+bCd_0V=J{(*P1-;{IAwdiuJsK8;V`8|z zJ}e%y1{Y5^=3HKA9o$I1|ZqDqg z&HM)YUaCz!LK(f2+J*>sg}C;b6n-xGO}jr>i9 z(r8AaKO0fzW#1~vddl*c8KY@$DUk4tq~M=0NIpQ_4Q7VROGoqEOjBzTUV-~)#vo4M zHt*xZ{DK^<1dp)UK`WP)OehaMY%4h-O+UF~vZfY(r0{v-pHy%a;jbvTnr3W`f@_E$ zS8yFX3BP6k5LAW4sUS%f=RglXR&bv9Lkg}Ud|ScQP!3HBt|9)@3a$emd?>C&lkw?T z<4j^H1tP3K6p|Em8e;U#o`D1`L25LXT!|-^tR9yeMoEw)wYV&@f``VY`*EB+j?Qpw z_UQ#N3Bwd*mhxPnjFOLI7^3X0v$3UEQcT4zTBp~nz0pK}BC+IxP__oJlE)ZT!_pTa dMeri=reTeqy;OAq`f0OzUG%QlKe_+0{sYPEW)lDa literal 0 HcmV?d00001 diff --git a/fonts/ascribe-font.woff b/fonts/ascribe-font.woff new file mode 100755 index 0000000000000000000000000000000000000000..7d18d089b2623098bedccc32e670483ffa0aad7e GIT binary patch literal 2912 zcma)8Z%kX)6+h=a{{ce{PHYSfv5k#!AYg2s;RP^YQesRn1W2I3Ac=7dm;ffmUBl8; zO{S<5q)MwzpsJlBRcP8Ls(t7*l`2C0voD)ct*W$DO{8}12d1f-Hf`0~#?L$V`8k13 z+H$RP@9+FS=lZ_;-m|BIK|r8Ua2^We)tKcO-ak&Au5SRqJd&PgRCGWiL>D4UB%39E zg3)ex_x+j3GLyYeJhNp~JTt#CO|stuX#T_~e4y;tv$4npQg4#JiIMTuAH6tBLXzDh zzLJsAQ7p_Zq%H&G6AOMu9lykl`9zfL@)>Vqr2UCbvk0QV|KRl=?8ihl(G<NcWRF7HDUL9LZ8v|nIFrs~WIx+3#g;kDd{L@0 zONyVYlqUov+aftyR>}d0&2kBk>iEC1%#I4EdO8JIVQ?5KOce&fP@w|J5tJNwKZy77 z3%svzEDr}8nH@e(K6zjuFPGLVl8YI&bd5=$1oKmO*P-8nb`rMquuIEff3nw%*i6}blZm!YSPMn3Ldd|V8 zrMp5)XJTV`czvm}b7_5e@Ro=TrRm%KtJ8k}^lE?q>P&0v%xb?A8^l|KIjiA~L}!a2 z-7Wo)>Gs-Ydje%2G;k2$ARZJASb=&p8MM5w%0(FPTK;eF&I90%s8m1H^1Sp5J|$h^ zchY~6%x_`~XHI8Sg&OJO1ir4;sqk~Dg3eb)%G}{4{tg(ShMjlJ^^xnV@nO5|)F`#@ z-TOD76y3Vs+k1U08r`}U4qw}f&b_@hIJoxq+}!rsz`)w}99pEm*`@DLZASzD`d&-s zX=f&TU8E<_pg6F;>6|Ei8;SXTDIDx|Wuf>TId`rGT3L)QVUsWC?tT9Z@#T+7!{6=8 zt9qC>4+_B!$0&@07sLv)!1IUD;oNr+XlJ|*_UGp|jcuZrtLpt`yaTlz=f3^v#rZ#S z3j?d$7cXwF4&>JM?kk%U6Ps7Mb1Sb;zjpgJ7n82zLFp^cUR&m~*Oa-?{uHq~i=%W# z?cjz{xB{=hZPjug8%o1?%(t#uPdA0RpFsP&URjY1}o5^6-2=-xU{0# zu|JtZu0PwD$&3HAmmmH?sC&^`uA4d8JF^uy*I~6C>vvDa4Lv<2(|vvY;pki3)ir<1 zXjfBXkFC}@89edajq2(b#{I36R>5*5|WPERXuU zXOe*zk_W2s!z!yVX$Xa!d3mDyNo%3@BYSC?-CkB|H+OgQ^>u@(;EAj5?(QR3L!s1A zUD(<--rO?ox7qyTEzRR?RxWciJl7srI_*7rYALw-ygfDFH0-x?oJy7c4i7b*@wcA# zl}S%=9~>IBl}wh{M}1XQ;ZNh?aGX)e+0M?hj4(JfVznNrHTv@0BcpaLXKCoFuI?5N z51GdB^2e0}{XQYAF8H}_yx(Z@c^iDr%G#RxGb1CtdcD}z-YV8rdry{mf_2u(>QJ<` zHPYn_mU{H(JtHe8dsarhW2SoiO4r%>Z2AU{3=J0)xHanF=cNvZ-0h#!H@GX(yWJ~K zH?)QD%?AUQg8^Y|B`8KhM~{Xg;;~4mrY00QW(zEws>_8u-!N2pe4^P_{Di&#c&)pu z&&u_D(%kΞ;*S+g&`IZSHI*J?ECpj@rVnvG=7K#e6j5TT=WIR`C!I;H|jyDfjO< zyAN`AN+B_#D<8FFnvWv#-)eLc{dD-XJmJ^G@V`>iEW<|{eS}0=lzpQp8!0PeZj2||P*%bjNx@&T zm12Op3oH!FLr3?yg>J10cmwXD1p_#aTfC1C@$+)FB0R$G4jQ?tWJa0vu%*<5bo$8*mp3LO#suR42`;dZ#Bqo*0ZhQzU}>r( zTDA!)CDSBOw@i^LRP8VNSEZ>`5o)!+HlcfRZE#$?g< zN#Fg>Ip6QRzSlhsB>>+y03dL%zaiX3CJB%1mosdxmGldrJUa(qge1fv1q;+uut)$S zjKK^f;3~{R43W-1*UFzrK z3$dlj=x^^5sG9iGiP*{#jb+5Mu_!UWc1ioa-$#gF1<>7{orzs6Pik6es)*+Ovm_|D zmwAW}5MMjHkY4Tl1#S_4k@&XxR6M5tk?yC&-y`0-5L;aWD>f0&{0ob*g_+k+6h9{Z zGxG6+rPNBA;-qF^`U3FUQoIXV0Pc0}Hzc`}8}TpT#bSmlXh>o2ZvlX>iut|l9t3#m zG?`y~h7;IF@y0w>(_C)k6;w57ppE1PD5A~fNG5z$oXupjOo?bvXv}_5sUf{Wy9h|A z(gGPB4yj#%JjMNAS$2;M@6pZy)|l+38gq@wYpT(JY!AwId=$h-_ys;vITjNKTiHE6 zNjgPfBCUG8MIsdo>Pek3xbxaZRZP5qyY#7gZG&&x+_cO-InKYE%uLPlyHEO)md#DF z1KIca4B4rK762{wV5`r`*+nB~F1K=Ck6%K`Av(EMUk`Csk{UTXpOGJWJNi;vkYKQ3~&A4I2DhuAyDe%Gn3J%s+r~XaYa2Rz8n>UajAx+e3*``tHJQKsmm1YiQ`! zPCUMIGaSCT6Q6r$V{~-mow>Q)jggU!-8r<%e--7&s1?z~-z!8iO_4zgRpb}Yq$;qy z>71x}mlN}BC>#{3GE@AX;yWJ$jm*ZEu+3L+kW z@H_Y$$f(B>tipPVo(_o#~ilW>Gt{jlH%7QNj@eegLHvf-Fh~O z%4+J^{q(lxjsN$8?q77jyFTZ>zQ#p{96h{p2W!x-_ex%iw_YzeaX{jrx_XQ$0AhUHg#S0;S!)60Py%P%(K2X!{@ zv?&yF6ctI%muv_0ABvS#qF7ZaS_TIAqb;N9;PLCufq{nWp-_6PC2Z@SYVVlp5`?a) zj`pc;8<#y2p6dxLopK*JxfEP~RZP$O$Gb$1(`Yh}@sR&?SLZ2TmHaL4SBEBqis=e* z!dF)p{xlg5Ct0sJ+t+uNbqtP0ZMKHya$k`%Iw9>Ea!Qalm_c%sh#!AJvXTeBQ9O zrEw<^#P`tA<*6#>bx{T*t(X|tU zYZLBC^HKcT(AW3f%x#R0jhB=-b=u(Pm3F(5?VmEYxoh%!18XmPxYvwd{Wt=T=TmEU0hNi9mnXu-GTRmpGo!}DD4xWzvNtL*FT_y zE<3dk(|>%FAK9y@+$#DZv;Tn7NKg5PLRma3Q*jW_TTL zU$T*GfVLm3iiUySU@N^eXW$KZh*k{X6z=doK18!DW+OJR`%Wz%k;14SBkZX0Am~Y! z%aSAdyh`&#`&C*)^i`GC(z|F@X&uRjRN4S9!8-e=Yl=x?q?sJe5uH$Jp6G6s))4)Q zN^7Y&6I5D9@-dY*fDg8l@zg>pwYa~+i_R>sBvXrm+v$3KS@&G6=L*MW5?AMA%lqra z=OTlt#k7!^S)5sprDrY*7uJNqcxsqpb3#xlDbtiif{M&VrO=E)n#PMHy8vtS9aKux QVYNhc(yFt{>3_8U0NQ3t`v3p{ diff --git a/fonts/ascribe-logo.ttf b/fonts/ascribe-logo.ttf deleted file mode 100644 index 61cdce038efc5e33c752040e0e88b55930f8ae6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2692 zcmai0eN0=|6+h=ae}G91ZZL+1_~RKvAmI25KfW;;u}v@zkU+r;NgT&9CV&Yz8!Szg zM9MZnrDU1}YMUw2gr@yP|Ee^VDngs~*QRuH&pY?|xiM{| zUF*K{JKyKrckg`$1b_;-2|Tz?oj&HNd|7jxB=6I_kXVQ>)lUBI9st%8ePox^ zY5?v{?zbelTcG$i@M1Z$D;Y>-?;inxC*}NJeh&gXO;WP8C=RO(ST;86Nmrn}Y*h~` z=peZc%7CLEg7ryxK9|qSUe+fdW6XbEt0KSBxClsAl_fF`KZK@6nqB zv>EIMo6%_N$nkAwIaKgY)k$BN=$E5FC5$)^kqm$xgQaxwqbJ8Pk*Fa`#Vq$Y?U|?x;V(hMnp4!}l==$ZJp3Cde z=z6NVJGCBlphvtrR6^Y#1NzA>sF*dgG-rU^o z#^~tA?i`w>zYEe+R10X}@0VIKPl1_~x=1gfL2+Px(>_u9t|sQ|rEpN{%0lsba_(Xc zL|Ke4V4JV!KK=S0;_IK6hQHaDSM^oiGB^MNI{|3ty`s(J<@rNsckJ5;v@&ixd-=sh zV~gnK8i)TA>ql+>`Tzd-%KRU>h0*ohD_3^cM~k#Cbbb5m+3oA0BIWhDw;w#HBs#cgc#8nk*+Hn#ix{b+Q^wuH&fB+6dK>Kc<7jnx`5YSNl10_$1rBEGo) z71QU3qX}J&EMdEyPM^;&%6Uzq=wng}NC&9htz}A-S5w2zr?9qV;)v*|1T z=|Dr%@mF2vW{hpR<=6XHrhM)*%YmEAFE`@{jTY~WAslv;m5I(*EC;k73bl2DP**FM zLLvTG+gLVu;)XL6I&vc%&W^W5EWNSLu2_%N+7s*QjP+W${L#os-M$%S>}P(cK*%bOdW%`U|efwUa|@Q|@WwG5k_DF!0^n9h@AWsHkvi z)WOec?RL4_zvS+4*QH;E)?W4WhVh+eqpQJycX}--#=}RChT~#KJlxU}j(1oC3#ZzO zAuo8w8;+mtv{t<&M31*R2S+U2(8ryfAO9d8Hx#-{M+(iI8=!M;&hM$s{093@s!=RQ z6TT~@uVNz)=>XnMOP_H6PP6+!9)Ge9R-CWw`4?2sVW;t7_Ro)tl--K*siF@u`wqy1 z{N!&aR3-8f{W(Y!BPn_SeIZhTXKswAxusCTc}c-vFiJ5%JqTvmLq~V8nQof1@Kbn% zW(;5qcX%HkCM~O3k4M;fr%|j(W|W5>c9eP$v?MEZ$q~MwV4kpF!79Sn6s)Fu(Wqbz z$%hrJgI8dkebZItB+-+mfH}e`1@nY^6|5rsnu66(16BoVNItG$9r$27oyaU?GK-Lg z1Y}@=W(F1~lglgV%%aupbipJfVHsA)fD{zRO?R8?gL%m0V0N$Fs>x){AS_p+shcYI8zR&Yh*~DGIYFR}_aV(Mu$|2y65lk{cK) PG(a`d1}S|d-^Bj|`x-nj diff --git a/fonts/ascribe-logo.woff b/fonts/ascribe-logo.woff deleted file mode 100644 index c563df56f6aa31dfe5d9ae9f0acce0d0273ecef8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2768 zcmai0du&tJ8UMa}{m6qlak1m11V66hyhx1Sm%MEj9EUh23B*k@0vKZ)ha?V(S;;6> zL1UW|B$y^8>edNqil+UQ?w?9iQ=w>&zc$gXsy1~Un!5H6rm31VZPGUI^}cg`T}+F# zV}0lRzVCd`bI-%CoCt>jfm+r3P%V$&V*T^liIayt09ZkD=NQ$zs5zQkOe~XZj`&lI z>f`U|NrSE~DD?!rDcW{R%*Hmy!CLzmLtO5*KKi*##IGGAjQZ2}yRF_$EdM zd#z_~F?W?@KO_DKqn@AQ)R48vp;w3VY;uk9~c#>Fs)((Wdw6Z7PuLVabjU!}t(C#fJ*V;&8B4 z*yA%~QvrIiYSEfRGBKf+%o&3_w>KIR!g<`KuHp6u-@dtNntO7Le>an#nd5h#3}#H5 zo8$)yzvT1erye>1)Yy-HpOdo-I?iY?b6!tCMA0EQIlr%;I5SCgoSn~054_!>?AGMu z=5i>syg503M?_D3{$6bTVsG!o^;m2@-P4m^k2%mI-We}xO>SjF-CpTI{d;V#T(?*g zDC?ks{Q&!Mzod>gy^1kL4!hkm_e~$N`0N1KgeM8Ih(oguDbeZ4F|5Y-*h25Mf zUr_DSNFQeLO?8zDKap&-uL@G(J}>e2!2s=SpJSfO^G% z)lK_EsoOxzms8=O+?2WE_vF~69B5=dK7(z(k^9q^_YmLsG&TIqGjUa4#I1mRAg~jF zcHS%6OkSQpfOf|-8-Z5FZD$|9v}kM*-CXm?e`W?yJ8<^D-@ml*2X1j}efQF(-Sx53 z*gte_`}FDUYeS`x*X4hB?;e+uZsLCFb62@8vnf|)CbT{^EYI2`?NI@oFa_7(dvFi7 zVHbV}{{RWKSd9(XLfO+MaiU$&Xifd-_0%0it&T5kW20BE)zM_L-xnA_qeIpuOnN3! zwlW&(OlCA1G^Es`HIoNcv)V;`Y5yy#PY*{6x|&%)znxB>FCfZsO`_;yQgTQK$nVy& zNt9Pp!_KGIUvmD{1$}nW1MZe$_!gUs969=V`3&07uJwvulea}Hn(WU8v&;Dx3p2X$ zpF-oouSbTKER9v^<0I*v;MoC-^+?P)o6!#s*IgVPjYX4hbJsU|yQc;N{$Xo}V>W#3 z)mv?Cug~=M%v!wW%hMg>eGYCeb15(#Y-&0Bvg^#Ov9oIBwSm-9(^H_;ij^v!g2&-7ZY zy))fiGkq4Wa40(8A6!1^K6GL^y#AVyTL?_{3LK|Wx3y^b940LOf;HdRCgv6I>QLV6LE{BwZq`6aK@(uEob%&wzUm;4^9}T@#=?7V=S~)lAG_^tnAgM^i`p;ZyPWh)yT=_4kOKZSLa@u5hPiwk?wE=}8Pa!u2lQIahq` z`0(14d)jyyKd%afUdi9W_{3y&wNs-Ge^PI^%hmoRe~Y^&{bFeCWlvuO-+D53H5~L# zuZ6`#x24QwY~~>o#5)=3WA5J>b|1*?kJiEJvo*c{ zf@(VK)IP}l`C(~fx1xNi=n-e{fZWJVenX))S&-;YL$c&anFr7nJr#Lo#(1JFWh7jX z6#N+*$p@$h<^Kruia)i$*m?s=iu!`^%1*_>^G%8p_@(~4B!Ar2t-gH$ZNpv(*#2n$Yf_cJy3RV$* zRl#bg1FM2HB%e@l75HE~lgut=vrCYHBxGTcC<{yR)XHimyJU4cT@Z&9tiUQM&; 0) { this.actions.updateContractAgreementList(contractAgreementList.results); resolve(contractAgreementList.results); - } - else{ + } else { resolve(null); } }) @@ -35,13 +34,13 @@ class ContractAgreementListActions { ); } - fetchAvailableContractAgreementList(issuer, createContractAgreement) { + fetchAvailableContractAgreementList(issuer, createPublicContractAgreement) { return Q.Promise((resolve, reject) => { OwnershipFetcher.fetchContractAgreementList(issuer, true, null) .then((acceptedContractAgreementList) => { // if there is at least an accepted contract agreement, we're going to // use it - if(acceptedContractAgreementList.count > 0) { + if (acceptedContractAgreementList.count > 0) { this.actions.updateContractAgreementList(acceptedContractAgreementList.results); } else { // otherwise, we're looking for contract agreements that are still pending @@ -50,15 +49,13 @@ class ContractAgreementListActions { // overcomplicate the method OwnershipFetcher.fetchContractAgreementList(issuer, null, true) .then((pendingContractAgreementList) => { - if(pendingContractAgreementList.count > 0) { + if (pendingContractAgreementList.count > 0) { this.actions.updateContractAgreementList(pendingContractAgreementList.results); - } else { + } else if (createPublicContractAgreement) { // if there was neither a pending nor an active contractAgreement - // found and createContractAgreement is set to true, we create a - // new contract agreement - if(createContractAgreement) { - this.actions.createContractAgreementFromPublicContract(issuer); - } + // found and createPublicContractAgreement is set to true, we create a + // new public contract agreement + this.actions.createContractAgreementFromPublicContract(issuer); } }) .catch((err) => { @@ -81,8 +78,7 @@ class ContractAgreementListActions { // create an agreement with the public contract if there is one if (publicContract && publicContract.length > 0) { return this.actions.createContractAgreement(null, publicContract[0]); - } - else { + } else { /* contractAgreementList in the store is already set to null; */ @@ -91,21 +87,17 @@ class ContractAgreementListActions { if (publicContracAgreement) { this.actions.updateContractAgreementList([publicContracAgreement]); } - }).catch((err) => { - console.logGlobal(err); - }); + }).catch(console.logGlobal); } createContractAgreement(issuer, contract){ return Q.Promise((resolve, reject) => { - OwnershipFetcher.createContractAgreement(issuer, contract).then( - (contractAgreement) => { - resolve(contractAgreement); - } - ).catch((err) => { - console.logGlobal(err); - reject(err); - }); + OwnershipFetcher + .createContractAgreement(issuer, contract).then(resolve) + .catch((err) => { + console.logGlobal(err); + reject(err); + }); }); } } diff --git a/js/actions/edition_actions.js b/js/actions/edition_actions.js index 4bdf093a..3f659524 100644 --- a/js/actions/edition_actions.js +++ b/js/actions/edition_actions.js @@ -7,7 +7,8 @@ import EditionFetcher from '../fetchers/edition_fetcher'; class EditionActions { constructor() { this.generateActions( - 'updateEdition' + 'updateEdition', + 'editionFailed' ); } @@ -18,6 +19,7 @@ class EditionActions { }) .catch((err) => { console.logGlobal(err); + this.actions.editionFailed(err.json); }); } } diff --git a/js/actions/global_notification_actions.js b/js/actions/global_notification_actions.js index 2bb8d6e6..73aa9815 100644 --- a/js/actions/global_notification_actions.js +++ b/js/actions/global_notification_actions.js @@ -2,13 +2,15 @@ import { alt } from '../alt'; - class GlobalNotificationActions { constructor() { this.generateActions( 'appendGlobalNotification', + 'showNextGlobalNotification', 'shiftGlobalNotification', - 'emulateEmptyStore' + 'cooldownGlobalNotifications', + 'pauseGlobalNotifications', + 'resumeGlobalNotifications' ); } } diff --git a/js/actions/piece_actions.js b/js/actions/piece_actions.js index 7aed13fc..9002e8c5 100644 --- a/js/actions/piece_actions.js +++ b/js/actions/piece_actions.js @@ -8,7 +8,8 @@ class PieceActions { constructor() { this.generateActions( 'updatePiece', - 'updateProperty' + 'updateProperty', + 'pieceFailed' ); } @@ -19,6 +20,7 @@ class PieceActions { }) .catch((err) => { console.logGlobal(err); + this.actions.pieceFailed(err.json); }); } } diff --git a/js/actions/webhook_actions.js b/js/actions/webhook_actions.js new file mode 100644 index 00000000..f9555ce7 --- /dev/null +++ b/js/actions/webhook_actions.js @@ -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); diff --git a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js index 5d3e033f..8033f239 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -78,7 +78,6 @@ let AccordionListItemEditionWidget = React.createClass({ return ( ); } else { @@ -137,4 +136,4 @@ let AccordionListItemEditionWidget = React.createClass({ } }); -export default AccordionListItemEditionWidget; \ No newline at end of file +export default AccordionListItemEditionWidget; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_piece.js b/js/components/ascribe_accordion_list/accordion_list_item_piece.js index 4547ce3b..006479c5 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_piece.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_piece.js @@ -4,6 +4,7 @@ import React from 'react'; import { Link } from 'react-router'; import AccordionListItem from './accordion_list_item'; +import AccordionListItemThumbnailPlacholder from './accordion_list_item_thumbnail_placeholder'; import { getLangText } from '../../utils/lang_utils'; @@ -19,7 +20,14 @@ let AccordionListItemPiece = React.createClass({ ]), subsubheading: React.PropTypes.object, buttons: React.PropTypes.object, - badge: React.PropTypes.object + badge: React.PropTypes.object, + thumbnailPlaceholder: React.PropTypes.func + }, + + getDefaultProps() { + return { + thumbnailPlaceholder: AccordionListItemThumbnailPlacholder + }; }, getLinkData() { @@ -34,19 +42,23 @@ let AccordionListItemPiece = React.createClass({ }, render() { - const { className, piece, artistName, buttons, badge, children, subsubheading } = this.props; + const { + artistName, + badge, + buttons, + children, + className, + piece, + subsubheading, + thumbnailPlaceholder: ThumbnailPlaceholder } = 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 = ( - - A - - ); + if (url.match(/https:\/\/.*\/media\/thumbnails\/ascribe_spiral.png/)) { + thumbnail = (); } else { thumbnail = (
diff --git a/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js new file mode 100644 index 00000000..37c98371 --- /dev/null +++ b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js @@ -0,0 +1,15 @@ +'use strict' + +import React from 'react'; + +let AccordionListItemThumbnailPlaceholder = React.createClass({ + render() { + return ( + + A + + ); + } +}); + +export default AccordionListItemThumbnailPlaceholder; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index da45d1e8..a8cab166 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -31,6 +31,7 @@ let AccordionListItemWallet = React.createClass({ propTypes: { className: React.PropTypes.string, content: React.PropTypes.object, + thumbnailPlaceholder: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -123,32 +124,36 @@ let AccordionListItemWallet = React.createClass({ }, render() { + const { children, className, content, thumbnailPlaceholder } = this.props; return ( - {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} + {Moment(content.date_created, 'YYYY-MM-DD').year()} {this.getLicences()} -
} + + } buttons={
-
} - badge={this.getGlyphicon()}> + + } + badge={this.getGlyphicon()} + thumbnailPlaceholder={thumbnailPlaceholder}> {this.getCreateEditionsDialog()} {/* this.props.children is AccordionListItemTableEditions */} - {this.props.children} + {children} ); } diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index 42f86320..35e42c20 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -41,7 +41,7 @@ let AclButtonList = React.createClass({ componentDidMount() { UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser.defer(); window.addEventListener('resize', this.handleResize); window.dispatchEvent(new Event('resize')); diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 6a3df7b2..97f2e173 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -26,7 +26,7 @@ export default function ({ action, displayName, title, tooltip }) { availableAcls: React.PropTypes.object.isRequired, buttonAcceptName: React.PropTypes.string, buttonAcceptClassName: React.PropTypes.string, - currentUser: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object, email: React.PropTypes.string, pieceOrEditions: React.PropTypes.oneOfType([ React.PropTypes.object, diff --git a/js/components/ascribe_collapsible/collapsible_paragraph.js b/js/components/ascribe_collapsible/collapsible_paragraph.js index e146b42b..7ad8d0af 100644 --- a/js/components/ascribe_collapsible/collapsible_paragraph.js +++ b/js/components/ascribe_collapsible/collapsible_paragraph.js @@ -12,7 +12,9 @@ const CollapsibleParagraph = React.createClass({ React.PropTypes.object, React.PropTypes.array ]), - iconName: React.PropTypes.string + iconName: React.PropTypes.string, + show: React.PropTypes.bool, + defaultExpanded: React.PropTypes.bool }, getDefaultProps() { diff --git a/js/components/ascribe_detail/detail_property.js b/js/components/ascribe_detail/detail_property.js index 9ea37285..8b0f50b5 100644 --- a/js/components/ascribe_detail/detail_property.js +++ b/js/components/ascribe_detail/detail_property.js @@ -7,6 +7,7 @@ let DetailProperty = React.createClass({ propTypes: { label: React.PropTypes.string, value: React.PropTypes.oneOfType([ + React.PropTypes.number, React.PropTypes.string, React.PropTypes.element ]), diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 6b38ddf8..bc2f0cfa 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -41,13 +41,20 @@ import { getLangText } from '../../utils/lang_utils'; */ let Edition = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, + furtherDetailsType: React.PropTypes.func, edition: React.PropTypes.object, - loadEdition: React.PropTypes.func, - location: React.PropTypes.object + loadEdition: React.PropTypes.func }, mixins: [History], + getDefaultProps() { + return { + furtherDetailsType: FurtherDetails + }; + }, + getInitialState() { return UserStore.getState(); }, @@ -75,6 +82,8 @@ let Edition = React.createClass({ }, render() { + let FurtherDetailsType = this.props.furtherDetailsType; + return ( @@ -90,6 +99,7 @@ let Edition = React.createClass({
@@ -137,7 +147,7 @@ let Edition = React.createClass({ currentUser={this.state.currentUser}/> {return {'bitcoin_id': this.props.edition.bitcoin_id}; }} - label={getLangText('Edition note (public)')} + label={getLangText('Personal note (public)')} defaultValue={this.props.edition.public_note ? this.props.edition.public_note : null} placeholder={getLangText('Enter your comments ...')} editable={!!this.props.edition.acl.acl_edit} @@ -151,13 +161,12 @@ let Edition = React.createClass({ show={this.props.edition.acl.acl_edit || Object.keys(this.props.edition.extra_data).length > 0 || this.props.edition.other_data.length > 0}> - + handleSuccess={this.props.loadEdition} /> @@ -173,6 +182,7 @@ let Edition = React.createClass({ let EditionSummary = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, edition: React.PropTypes.object, currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func @@ -185,7 +195,7 @@ let EditionSummary = React.createClass({ getStatus(){ let status = null; if (this.props.edition.status.length > 0){ - let statusStr = this.props.edition.status.join().replace(/_/, ' '); + let statusStr = this.props.edition.status.join(', ').replace(/_/g, ' '); status = ; if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){ status = ( @@ -197,7 +207,7 @@ let EditionSummary = React.createClass({ }, render() { - let { edition, currentUser } = this.props; + let { actionPanelButtonListType, edition, currentUser } = this.props; return (
{this.getStatus()} - + {/* + `acl_view` is always available in `edition.acl`, therefore if it has + no more than 1 key, we're hiding the `DetailProperty` actions as otherwise + `AclInformation` would show up + */} + 1}> diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 162427d5..36a79e7c 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -36,6 +36,7 @@ import { getLangText } from '../../utils/lang_utils'; */ let EditionActionPanel = React.createClass({ propTypes: { + actionPanelButtonListType: React.PropTypes.func, edition: React.PropTypes.object, currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func @@ -43,6 +44,12 @@ let EditionActionPanel = React.createClass({ mixins: [History], + getDefaultProps() { + return { + actionPanelButtonListType: AclButtonList + }; + }, + getInitialState() { return PieceListStore.getState(); }, @@ -87,7 +94,10 @@ let EditionActionPanel = React.createClass({ }, render(){ - let {edition, currentUser} = this.props; + const { + actionPanelButtonListType: ActionPanelButtonListType, + edition, + currentUser } = this.props; if (edition && edition.notifications && @@ -104,7 +114,7 @@ let EditionActionPanel = React.createClass({ return ( - + type="text" + value={edition.bitcoin_id} + readOnly />
}> + label={labels.email || getLangText('Email')} + editable={!defaultEmail} + onChange={this.handleEmailOnChange} + overrideForm={!!defaultEmail}> + label={labels.message || getLangText('Personal Message')} + editable + overrideForm> + + + diff --git a/js/components/ascribe_forms/form_loan.js b/js/components/ascribe_forms/form_loan.js index d6398a20..a204fb87 100644 --- a/js/components/ascribe_forms/form_loan.js +++ b/js/components/ascribe_forms/form_loan.js @@ -1,33 +1,34 @@ 'use strict'; import React from 'react'; - import classnames from 'classnames'; import Button from 'react-bootstrap/lib/Button'; +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; + import Form from './form'; import Property from './property'; -import InputTextAreaToggable from './input_textarea_toggable'; -import InputDate from './input_date'; -import InputCheckbox from './input_checkbox'; -import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; -import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; +import InputDate from './input_date'; +import InputTextAreaToggable from './input_textarea_toggable'; +import InputContractAgreementCheckbox from './input_contract_agreement_checkbox'; import AscribeSpinner from '../ascribe_spinner'; -import { mergeOptions } from '../../utils/general_utils'; -import { getLangText } from '../../utils/lang_utils'; import AclInformation from '../ascribe_buttons/acl_information'; +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; + + let LoanForm = React.createClass({ propTypes: { loanHeading: React.PropTypes.string, email: React.PropTypes.string, gallery: React.PropTypes.string, - startdate: React.PropTypes.object, - enddate: React.PropTypes.object, + startDate: React.PropTypes.object, + endDate: React.PropTypes.object, showPersonalMessage: React.PropTypes.bool, showEndDate: React.PropTypes.bool, showStartDate: React.PropTypes.bool, @@ -36,7 +37,11 @@ let LoanForm = React.createClass({ id: React.PropTypes.object, message: React.PropTypes.string, createPublicContractAgreement: React.PropTypes.bool, - handleSuccess: React.PropTypes.func + handleSuccess: React.PropTypes.func, + children: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]) }, getDefaultProps() { @@ -45,148 +50,33 @@ let LoanForm = React.createClass({ showPersonalMessage: true, showEndDate: true, showStartDate: true, - showPassword: true, - createPublicContractAgreement: true + showPassword: true }; }, getInitialState() { - return ContractAgreementListStore.getState(); - }, - - componentDidMount() { - ContractAgreementListStore.listen(this.onChange); - this.getContractAgreementsOrCreatePublic(this.props.email); - }, - - /** - * This method needs to be in form_loan as some whitelabel pages (Cyland) load - * the loanee's email async! - * - * SO LEAVE IT IN! - */ - componentWillReceiveProps(nextProps) { - if(nextProps && nextProps.email && this.props.email !== nextProps.email) { - this.getContractAgreementsOrCreatePublic(nextProps.email); - } - }, - - componentWillUnmount() { - ContractAgreementListStore.unlisten(this.onChange); + return { + email: this.props.email || '' + }; }, onChange(state) { this.setState(state); }, - getContractAgreementsOrCreatePublic(email){ - ContractAgreementListActions.flushContractAgreementList.defer(); - if (email) { - // fetch the available contractagreements (pending/accepted) - ContractAgreementListActions.fetchAvailableContractAgreementList(email, true); - } - }, - - getFormData(){ - return mergeOptions( - this.props.id, - this.getContractAgreementId() - ); - }, - - handleOnChange(event) { + handleEmailOnChange(event) { // event.target.value is the submitted email of the loanee - if(event && event.target && event.target.value && event.target.value.match(/.*@.*\..*/)) { - this.getContractAgreementsOrCreatePublic(event.target.value); - } else { - ContractAgreementListActions.flushContractAgreementList(); - } + this.setState({ + email: event && event.target && event.target.value || '' + }); }, - getContractAgreementId() { - if (this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - return {'contract_agreement_id': this.state.contractAgreementList[0].id}; - } - return {}; + handleReset() { + this.handleEmailOnChange(); }, - getContractCheckbox() { - if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - // we need to define a key on the InputCheckboxes as otherwise - // react is not rerendering them on a store switch and is keeping - // the default value of the component (which is in that case true) - let contractAgreement = this.state.contractAgreementList[0]; - let contract = contractAgreement.contract; - - if(contractAgreement.datetime_accepted) { - return ( - - - - {getLangText('Download contract')} - - {/* We still need to send the server information that we're accepting */} - - - ); - } else { - return ( - - - - {getLangText('I agree to the')}  - - {getLangText('terms of ')} {contract.issuer} - - - - - ); - } - } else { - return ( - - - - ); - } - }, - - getAppendix() { - if(this.state.contractAgreementList && this.state.contractAgreementList.length > 0) { - let appendix = this.state.contractAgreementList[0].appendix; - if (appendix && appendix.default) { - return ( - -
{appendix.default}
-
- ); - } - } - return null; + getFormData() { + return this.props.id; }, getButtons() { @@ -214,14 +104,31 @@ let LoanForm = React.createClass({ }, render() { + const { email } = this.state; + const { + children, + createPublicContractAgreement, + email: defaultEmail, + handleSuccess, + gallery, + loanHeading, + message, + showPersonalMessage, + endDate, + startDate, + showEndDate, + showStartDate, + showPassword, + url } = this.props; + return ( @@ -229,18 +136,18 @@ let LoanForm = React.createClass({

}> -
-

{this.props.loanHeading}

+
+

{loanHeading}

+ editable={!defaultEmail} + onChange={this.handleEmailOnChange} + overrideForm={!!defaultEmail}> @@ -248,31 +155,31 @@ let LoanForm = React.createClass({ + editable={!gallery} + overrideForm={!!gallery}> + editable={!startDate} + overrideForm={!!startDate} + expanded={showStartDate}> + expanded={showEndDate}> + expanded={showPersonalMessage}> + required={showPersonalMessage}/> + + + - {this.getContractCheckbox()} - {this.getAppendix()} + expanded={showPassword}> + required={showPassword}/> - {this.props.children} + {children} ); } diff --git a/js/components/ascribe_forms/form_loan_request_answer.js b/js/components/ascribe_forms/form_loan_request_answer.js index 1bfe90db..349b4efc 100644 --- a/js/components/ascribe_forms/form_loan_request_answer.js +++ b/js/components/ascribe_forms/form_loan_request_answer.js @@ -65,8 +65,8 @@ let LoanRequestAnswerForm = React.createClass({ url={this.props.url} email={this.state.loanRequest ? this.state.loanRequest.new_owner : null} gallery={this.state.loanRequest ? this.state.loanRequest.gallery : null} - startdate={startDate} - enddate={endDate} + startDate={startDate} + endDate={endDate} showPassword={true} showPersonalMessage={false} handleSuccess={this.props.handleSuccess}/> @@ -76,4 +76,4 @@ let LoanRequestAnswerForm = React.createClass({ } }); -export default LoanRequestAnswerForm; \ No newline at end of file +export default LoanRequestAnswerForm; diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 8e8b015c..f1c49191 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -31,7 +31,7 @@ let RegisterPieceForm = React.createClass({ isFineUploaderActive: React.PropTypes.bool, isFineUploaderEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, - onLoggedOut: React.PropTypes.func, + enableSeparateThumbnail: React.PropTypes.bool, // For this form to work with SlideContainer, we sometimes have to disable it disabled: React.PropTypes.bool, @@ -46,7 +46,8 @@ let RegisterPieceForm = React.createClass({ return { headerMessage: getLangText('Register your work'), submitMessage: getLangText('Register work'), - enableLocalHashing: true + enableLocalHashing: true, + enableSeparateThumbnail: true }; }, @@ -89,6 +90,11 @@ let RegisterPieceForm = React.createClass({ if (digitalWorkFile && (digitalWorkFile.status === FileStatus.DELETED || digitalWorkFile.status === FileStatus.CANCELED)) { this.refs.form.refs.thumbnail_file.reset(); + + // Manually we need to set the ready state for `thumbnailKeyReady` back + // to `true` as `ReactS3Fineuploader`'s `reset` method triggers + // `setIsUploadReady` with `false` + this.refs.submitButton.setReadyStateForKey('thumbnailKeyReady', true); this.setState({ digitalWorkFile: null }); } else { this.setState({ digitalWorkFile }); @@ -99,13 +105,18 @@ let RegisterPieceForm = React.createClass({ const { digitalWorkFile } = this.state; const { fineuploader } = this.refs.digitalWorkFineUploader.refs; - fineuploader.setThumbnailForFileId(digitalWorkFile.id, thumbnailFile.url); + fineuploader.setThumbnailForFileId( + digitalWorkFile.id, + // if thumbnail was deleted, we delete it from the display as well + thumbnailFile.status !== FileStatus.DELETED ? thumbnailFile.url : null + ); }, isThumbnailDialogExpanded() { + const { enableSeparateThumbnail } = this.props; const { digitalWorkFile } = this.state; - if(digitalWorkFile) { + if(digitalWorkFile && enableSeparateThumbnail) { const { type: mimeType } = digitalWorkFile; const mimeSubType = mimeType && mimeType.split('/').length ? mimeType.split('/')[1] : 'unknown'; @@ -121,7 +132,6 @@ let RegisterPieceForm = React.createClass({ submitMessage, headerMessage, isFineUploaderActive, - onLoggedOut, isFineUploaderEditable, location, children, @@ -172,7 +182,6 @@ let RegisterPieceForm = React.createClass({ setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isFineUploaderActive={isFineUploaderActive} - onLoggedOut={onLoggedOut} disabled={!isFineUploaderEditable} enableLocalHashing={hashLocally} uploadMethod={location.query.method} @@ -189,7 +198,7 @@ let RegisterPieceForm = React.createClass({ url: ApiUrls.blob_thumbnails }} handleChangedFile={this.handleChangedThumbnail} - isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + isReadyForFormSubmission={formSubmissionValidation.fileOptional} keyRoutine={{ url: AppConstants.serverUrl + 's3/key/', fileClass: 'thumbnail' @@ -203,7 +212,9 @@ let RegisterPieceForm = React.createClass({ fileClassToUpload={{ singular: getLangText('Select representative image'), plural: getLangText('Select representative images') - }} /> + }} + isFineUploaderActive={isFineUploaderActive} + disabled={!isFineUploaderEditable} /> + required />
@@ -65,4 +65,4 @@ let UnConsignRequestForm = React.createClass({ } }); -export default UnConsignRequestForm; \ No newline at end of file +export default UnConsignRequestForm; diff --git a/js/components/ascribe_forms/input_contract_agreement_checkbox.js b/js/components/ascribe_forms/input_contract_agreement_checkbox.js new file mode 100644 index 00000000..61235631 --- /dev/null +++ b/js/components/ascribe_forms/input_contract_agreement_checkbox.js @@ -0,0 +1,206 @@ +'use strict'; + +import React from 'react/addons'; + +import InputCheckbox from './input_checkbox'; + +import ContractAgreementListStore from '../../stores/contract_agreement_list_store'; +import ContractAgreementListActions from '../../actions/contract_agreement_list_actions'; + +import { getLangText } from '../../utils/lang_utils'; +import { mergeOptions } from '../../utils/general_utils'; +import { isEmail } from '../../utils/regex_utils'; + + +const InputContractAgreementCheckbox = React.createClass({ + propTypes: { + createPublicContractAgreement: React.PropTypes.bool, + email: React.PropTypes.string, + + required: React.PropTypes.bool, + + // provided by Property + disabled: React.PropTypes.bool, + onChange: React.PropTypes.func, + name: React.PropTypes.string, + setExpanded: React.PropTypes.func, + + // can be used to style the component from the outside + style: React.PropTypes.object + }, + + getDefaultProps() { + return { + createPublicContractAgreement: true + }; + }, + + getInitialState() { + return mergeOptions( + ContractAgreementListStore.getState(), + { + value: { + terms: null, + contract_agreement_id: null + } + } + ); + }, + + componentDidMount() { + ContractAgreementListStore.listen(this.onStoreChange); + this.getContractAgreementsOrCreatePublic(this.props.email); + }, + + componentWillReceiveProps({ email: nextEmail }) { + if (this.props.email !== nextEmail) { + if (isEmail(nextEmail)) { + this.getContractAgreementsOrCreatePublic(nextEmail); + } else if (this.getContractAgreement()) { + ContractAgreementListActions.flushContractAgreementList(); + } + } + }, + + componentWillUnmount() { + ContractAgreementListStore.unlisten(this.onStoreChange); + }, + + onStoreChange(state) { + const contractAgreement = this.getContractAgreement(state.contractAgreementList); + + // If there is no contract available, hide this `Property` from the user + this.props.setExpanded(!!contractAgreement); + + state = mergeOptions(state, { + value: { + // If `email` is defined in this component, `getContractAgreementsOrCreatePublic` + // is either: + // + // - fetching a already existing contract agreement; or + // - trying to create a contract agreement + // + // If both attempts result in `contractAgreement` being not defined, + // it means that the receiver hasn't defined a contract, which means + // a contract agreement cannot be created, which means we don't have to + // specify `contract_agreement_id` when sending a request to the server. + contract_agreement_id: contractAgreement ? contractAgreement.id : null, + // If the receiver hasn't set a contract or the contract was + // previously accepted, we set the terms to `true` + // as we always need to at least give a boolean value for `terms` + // to the API endpoint + terms: !contractAgreement || !!contractAgreement.datetime_accepted + } + }); + + this.setState(state); + }, + + onChange(event) { + // Sync the value between our `InputCheckbox` and this component's `terms` + // so the parent `Property` is able to get the correct value of this component + // when the `Form` queries it. + this.setState({ + value: React.addons.update(this.state.value, { + terms: { $set: event.target.value } + }) + }); + + // Propagate change events from the checkbox up to the parent `Property` + this.props.onChange(event); + }, + + getContractAgreement(contractAgreementList = this.state.contractAgreementList) { + if (contractAgreementList && contractAgreementList.length) { + return contractAgreementList[0]; + } + }, + + getContractAgreementsOrCreatePublic(email) { + ContractAgreementListActions.flushContractAgreementList.defer(); + + if (email) { + // fetch the available contractagreements (pending/accepted) + ContractAgreementListActions.fetchAvailableContractAgreementList(email, this.props.createPublicContractAgreement); + } + }, + + getAppendix() { + const contractAgreement = this.getContractAgreement(); + + if (contractAgreement && + contractAgreement.appendix && + contractAgreement.appendix.default) { + return ( +
+

{getLangText('Appendix')}

+
{contractAgreement.appendix.default}
+
+ ); + } + }, + + getContractCheckbox() { + const contractAgreement = this.getContractAgreement(); + + if(contractAgreement) { + const { + datetime_accepted: datetimeAccepted, + contract: { + issuer: contractIssuer, + blob: { url_safe: contractUrl } + } + } = contractAgreement; + + if(datetimeAccepted) { + return ( + + ); + } else { + const { + name, + disabled, + style } = this.props; + + return ( + + + {getLangText('I agree to the')}  + + {getLangText('terms of ')} {contractIssuer} + + + + ); + } + } + }, + + render() { + return ( +
+ {this.getContractCheckbox()} + {this.getAppendix()} +
+ ); + } +}); + +export default InputContractAgreementCheckbox; diff --git a/js/components/ascribe_forms/input_fineuploader.js b/js/components/ascribe_forms/input_fineuploader.js index 6ee44113..625ac2ff 100644 --- a/js/components/ascribe_forms/input_fineuploader.js +++ b/js/components/ascribe_forms/input_fineuploader.js @@ -18,10 +18,10 @@ const InputFineUploader = React.createClass({ // a user is actually not logged in already to prevent him from droping files // before login in isFineUploaderActive: bool, - onLoggedOut: func, // provided by Property disabled: bool, + onChange: func, // Props for ReactS3FineUploader areAssetsDownloadable: bool, @@ -110,22 +110,22 @@ const InputFineUploader = React.createClass({ }, render() { - const { fileInputElement, - keyRoutine, - createBlobRoutine, - validation, - setIsUploadReady, - isReadyForFormSubmission, - isFineUploaderActive, - areAssetsDownloadable, - onLoggedOut, - enableLocalHashing, - fileClassToUpload, - uploadMethod, - handleChangedFile, - setWarning, - showErrorPrompt, - disabled } = this.props; + const { + areAssetsDownloadable, + createBlobRoutine, + enableLocalHashing, + disabled, + fileClassToUpload, + fileInputElement, + handleChangedFile, + isFineUploaderActive, + isReadyForFormSubmission, + keyRoutine, + setIsUploadReady, + setWarning, + showErrorPrompt, + uploadMethod, + validation } = this.props; let editable = isFineUploaderActive; // if disabled is actually set by property, we want to override @@ -162,7 +162,6 @@ const InputFineUploader = React.createClass({ 'X-CSRFToken': getCookie(AppConstants.csrftoken) } }} - onInactive={onLoggedOut} enableLocalHashing={enableLocalHashing} uploadMethod={uploadMethod} fileClassToUpload={fileClassToUpload} diff --git a/js/components/ascribe_forms/input_textarea_toggable.js b/js/components/ascribe_forms/input_textarea_toggable.js index c17a0e5a..0be8b87a 100644 --- a/js/components/ascribe_forms/input_textarea_toggable.js +++ b/js/components/ascribe_forms/input_textarea_toggable.js @@ -7,6 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize'; let InputTextAreaToggable = React.createClass({ propTypes: { + autoFocus: React.PropTypes.bool, disabled: React.PropTypes.bool, rows: React.PropTypes.number.isRequired, required: React.PropTypes.bool, @@ -23,6 +24,10 @@ let InputTextAreaToggable = React.createClass({ }, componentDidMount() { + if (this.props.autoFocus) { + this.refs.textarea.focus(); + } + this.setState({ value: this.props.defaultValue }); @@ -51,6 +56,7 @@ let InputTextAreaToggable = React.createClass({ className = className + ' ascribe-textarea-editable'; textarea = ( { - // Since refs will be overriden by this functions return statement, // we still want to be able to define refs for nested `Form` or `Property` // children, which is why we're upfront simply invoking the callback-ref- @@ -252,7 +268,8 @@ const Property = React.createClass({ setWarning: this.setWarning, disabled: !this.props.editable, ref: 'input', - name: this.props.name + name: this.props.name, + setExpanded: this.setExpanded }); }); } @@ -271,10 +288,6 @@ const Property = React.createClass({ } }, - handleCheckboxToggle() { - this.setState({expanded: !this.state.expanded}); - }, - getCheckbox() { const { checkboxLabel } = this.props; @@ -298,23 +311,20 @@ const Property = React.createClass({ render() { let footer = null; - let style = this.props.style ? mergeOptions({}, this.props.style) : {}; if(this.props.footer){ footer = (
{this.props.footer} -
); +
+ ); } - style.paddingBottom = !this.state.expanded ? 0 : null; - style.cursor = !this.props.editable ? 'not-allowed' : null; - return (
+ style={this.props.style}> {this.getCheckbox()}
{this.getLabelAndErrors()} - {this.renderChildren(style)} + {this.renderChildren(this.props.style)} {footer}
diff --git a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js index 2eedbd4c..c9c7f9f4 100644 --- a/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js +++ b/js/components/ascribe_piece_list_bulk_modal/piece_list_bulk_modal.js @@ -4,77 +4,24 @@ import React from 'react'; import { mergeOptions } from '../../utils/general_utils'; -import EditionListStore from '../../stores/edition_list_store'; import EditionListActions from '../../actions/edition_list_actions'; -import UserStore from '../../stores/user_store'; -import UserActions from '../../actions/user_actions'; - -import PieceListStore from '../../stores/piece_list_store'; -import PieceListActions from '../../actions/piece_list_actions'; - import PieceListBulkModalSelectedEditionsWidget from './piece_list_bulk_modal_selected_editions_widget'; -import AclButtonList from '../ascribe_buttons/acl_button_list'; -import DeleteButton from '../ascribe_buttons/delete_button'; -import { getAvailableAcls } from '../../utils/acl_utils'; import { getLangText } from '../../utils/lang_utils.js'; let PieceListBulkModal = React.createClass({ propTypes: { - className: React.PropTypes.string - }, - - getInitialState() { - return mergeOptions( - EditionListStore.getState(), - UserStore.getState(), - PieceListStore.getState() - ); - }, - - - - componentDidMount() { - EditionListStore.listen(this.onChange); - UserStore.listen(this.onChange); - PieceListStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - }, - - componentWillUnmount() { - EditionListStore.unlisten(this.onChange); - PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - fetchSelectedPieceEditionList() { - let filteredPieceIdList = Object.keys(this.state.editionList) - .filter((pieceId) => { - return this.state.editionList[pieceId] - .filter((edition) => edition.selected).length > 0; - }); - return filteredPieceIdList; - }, - - fetchSelectedEditionList() { - let selectedEditionList = []; - - Object - .keys(this.state.editionList) - .forEach((pieceId) => { - let filteredEditionsForPiece = this.state.editionList[pieceId].filter((edition) => edition.selected); - selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece); - }); - - return selectedEditionList; + availableAcls: React.PropTypes.object.isRequired, + className: React.PropTypes.string, + selectedEditions: React.PropTypes.oneOfType([ + React.PropTypes.object, + React.PropTypes.array + ]), + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) }, clearAllSelections() { @@ -82,22 +29,8 @@ let PieceListBulkModal = React.createClass({ EditionListActions.closeAllEditionLists(); }, - handleSuccess() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - - this.fetchSelectedPieceEditionList() - .forEach((pieceId) => { - EditionListActions.refreshEditionList({pieceId, filterBy: {}}); - }); - EditionListActions.clearAllEditionSelections(); - }, - render() { - let selectedEditions = this.fetchSelectedEditionList(); - let availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); - - if(Object.keys(availableAcls).length > 0) { + if (Object.keys(this.props.availableAcls).length) { return (
@@ -106,7 +39,7 @@ let PieceListBulkModal = React.createClass({
+ numberOfSelectedEditions={this.props.selectedEditions.length} />          

- - - + {this.props.children}
@@ -132,7 +57,6 @@ let PieceListBulkModal = React.createClass({ } else { return null; } - } }); diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index 38de2af6..c463330c 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -28,7 +28,7 @@ let PieceListToolbarFilterWidget = React.createClass({ }, generateFilterByStatement(param) { - let filterBy = this.props.filterBy; + const filterBy = Object.assign({}, this.props.filterBy); if(filterBy) { // we need hasOwnProperty since the values are all booleans @@ -56,13 +56,13 @@ let PieceListToolbarFilterWidget = React.createClass({ */ filterBy(param) { return () => { - let filterBy = this.generateFilterByStatement(param); + const filterBy = this.generateFilterByStatement(param); this.props.applyFilterBy(filterBy); }; }, isFilterActive() { - let trueValuesOnly = Object.keys(this.props.filterBy).filter((acl) => acl); + const trueValuesOnly = Object.keys(this.props.filterBy).filter((acl) => acl); // We're hiding the star in that complicated matter so that, // the surrounding button is not resized up on appearance @@ -74,7 +74,7 @@ let PieceListToolbarFilterWidget = React.createClass({ }, render() { - let filterIcon = ( + const filterIcon = ( * @@ -140,4 +140,4 @@ let PieceListToolbarFilterWidget = React.createClass({ } }); -export default PieceListToolbarFilterWidget; \ No newline at end of file +export default PieceListToolbarFilterWidget; diff --git a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js index b2d552a7..0eb4ad8f 100644 --- a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js +++ b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js @@ -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'; @@ -37,7 +37,9 @@ export default function AuthProxyHandler({to, when}) { 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(); diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js index 5b05e708..35a6fbe5 100644 --- a/js/components/ascribe_settings/settings_container.js +++ b/js/components/ascribe_settings/settings_container.js @@ -11,6 +11,7 @@ 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'; @@ -70,6 +71,7 @@ let SettingsContainer = React.createClass({ aclName="acl_view_settings_api"> + diff --git a/js/components/ascribe_settings/webhook_settings.js b/js/components/ascribe_settings/webhook_settings.js new file mode 100644 index 00000000..9deecbcd --- /dev/null +++ b/js/components/ascribe_settings/webhook_settings.js @@ -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 = ; + + if (this.state.webhooks) { + content = this.state.webhooks.map(function(webhook, i) { + const event = webhook.event.split('.')[0]; + return ( + +
+ {event.toUpperCase()} +
+
+ {webhook.target} +
+
+ } + buttons={ +
+
+ +
+
+ }/> + ); + }, this); + } + return content; + }, + + getEvents() { + if (this.state.webhookEvents && this.state.webhookEvents.length) { + return ( + + + ); + } + return null; + }, + + + render() { + return ( + +
+

+ 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. +

+

+ To get started, simply choose the prefered action that you want to be notified upon and supply + a target url. +

+
+ +
+ { this.getEvents() } + + + +
+
+
+ {this.getWebhooks()} +
+ ); + } +}); + +export default WebhookSettings; \ No newline at end of file diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 8ed66c1d..39d515a3 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -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 diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index 87a2aef6..aa0b6691 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -8,7 +8,6 @@ import { InjectInHeadUtils } from '../../utils/inject_utils'; let FacebookShareButton = React.createClass({ propTypes: { - url: React.PropTypes.string, type: React.PropTypes.string }, @@ -28,12 +27,14 @@ let FacebookShareButton = React.createClass({ * 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. */ - if (!InjectInHeadUtils.isPresent('script', AppConstants.facebook.sdkUrl)) { - InjectInHeadUtils.inject(AppConstants.facebook.sdkUrl); - } else { - // Parse() searches the children of the element we give it, not the element itself. - FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement); - } + + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + }, + + shouldComponentUpdate(nextProps) { + return this.props.type !== nextProps.type; }, render() { @@ -41,7 +42,6 @@ let FacebookShareButton = React.createClass({ ); diff --git a/js/components/ascribe_spinner.js b/js/components/ascribe_spinner.js index e1daf5b2..ad97d484 100644 --- a/js/components/ascribe_spinner.js +++ b/js/components/ascribe_spinner.js @@ -7,26 +7,26 @@ 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']) + color: React.PropTypes.oneOf(['black', 'blue', 'dark-blue', 'light-blue', 'pink', 'white', 'loop']) }, getDefaultProps() { return { inline: false, - size: 'md', - color: 'loop' + size: 'md' }; }, render() { + const { classNames: classes, color, size } = this.props; + return (
-
-
A
+ className={classNames('spinner-wrapper-' + size, + color ? 'spinner-wrapper-' + color : null, + classes)}> +
+
A
); } diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js index 3c993aea..9ad1facb 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop.js @@ -23,7 +23,6 @@ let FileDragAndDrop = React.createClass({ onDrop: React.PropTypes.func.isRequired, onDragOver: React.PropTypes.func, - onInactive: React.PropTypes.func, handleDeleteFile: React.PropTypes.func, handleCancelFile: React.PropTypes.func, handlePauseFile: React.PropTypes.func, @@ -70,28 +69,21 @@ let FileDragAndDrop = React.createClass({ handleDrop(event) { event.preventDefault(); event.stopPropagation(); - let files; - if(this.props.dropzoneInactive) { - // if there is a handle function for doing stuff - // when the dropzone is inactive, then call it - if(this.props.onInactive) { - this.props.onInactive(); + if (!this.props.dropzoneInactive) { + let files; + + // handle Drag and Drop + if(event.dataTransfer && event.dataTransfer.files.length > 0) { + files = event.dataTransfer.files; + } else if(event.target.files) { // handle input type file + files = event.target.files; } - return; - } - // handle Drag and Drop - if(event.dataTransfer && event.dataTransfer.files.length > 0) { - files = event.dataTransfer.files; - } else if(event.target.files) { // handle input type file - files = event.target.files; + if(typeof this.props.onDrop === 'function' && files) { + this.props.onDrop(files); + } } - - if(typeof this.props.onDrop === 'function' && files) { - this.props.onDrop(files); - } - }, handleDeleteFile(fileId) { @@ -123,31 +115,25 @@ let FileDragAndDrop = React.createClass({ }, handleOnClick() { - let evt; - // when multiple is set to false and the user already uploaded a piece, - // do not propagate event - if(this.props.dropzoneInactive) { - // if there is a handle function for doing stuff - // when the dropzone is inactive, then call it - if(this.props.onInactive) { - this.props.onInactive(); + // do not propagate event if the drop zone's inactive, + // for example when multiple is set to false and the user already uploaded a piece + if (!this.props.dropzoneInactive) { + let evt; + + try { + evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + } catch(e) { + // For browsers that do not support the new MouseEvent syntax + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); } - return; - } - try { - evt = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - } catch(e) { - // For browsers that do not support the new MouseEvent syntax - evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + this.refs.fileSelector.getDOMNode().dispatchEvent(evt); } - - this.refs.fileSelector.getDOMNode().dispatchEvent(evt); }, getErrorDialog(failedFiles) { diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index aabf19d9..94c85f4f 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -32,6 +32,20 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = handleDeleteFile: func }, + getInitialState() { + return { + disabled: this.getUploadingFiles().length !== 0 + }; + }, + + componentWillReceiveProps(nextProps) { + if(this.props.filesToUpload !== nextProps.filesToUpload) { + this.setState({ + disabled: this.getUploadingFiles(nextProps.filesToUpload).length !== 0 + }); + } + }, + handleDrop(event) { event.preventDefault(); event.stopPropagation(); @@ -42,43 +56,62 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = } }, - getUploadingFiles() { - return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOADING); + getUploadingFiles(filesToUpload = this.props.filesToUpload) { + return filesToUpload.filter((file) => file.status === FileStatus.UPLOADING); }, getUploadedFile() { return this.props.filesToUpload.filter((file) => file.status === FileStatus.UPLOAD_SUCESSFUL)[0]; }, + clearSelection() { + this.refs.fileSelector.getDOMNode().value = ''; + }, + handleOnClick() { - const uploadingFiles = this.getUploadingFiles(); - const uploadedFile = this.getUploadedFile(); + if(!this.state.disabled) { + let evt; + 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 - var evt = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); + this.clearSelection(); + if(uploadingFiles.length) { + this.props.handleCancelFile(uploadingFiles[0].id); + } else if(uploadedFile && !uploadedFile.s3UrlSafe) { + this.props.handleCancelFile(uploadedFile.id); + } else if(uploadedFile && uploadedFile.s3UrlSafe) { + this.props.handleDeleteFile(uploadedFile.id); + } + try { + evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + } catch(e) { + // For browsers that do not support the new MouseEvent syntax + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + } evt.stopPropagation(); - this.refs.fileinput.getDOMNode().dispatchEvent(evt); + this.refs.fileSelector.getDOMNode().dispatchEvent(evt); } }, + onClickCancel() { + this.clearSelection(); + const uploadingFile = this.getUploadingFiles()[0]; + this.props.handleCancelFile(uploadingFile.id); + }, + onClickRemove() { + this.clearSelection(); const uploadedFile = this.getUploadedFile(); this.props.handleDeleteFile(uploadedFile.id); }, + getButtonLabel() { let { filesToUpload, fileClassToUpload } = this.props; @@ -94,8 +127,16 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = getUploadedFileLabel() { const uploadedFile = this.getUploadedFile(); + const uploadingFiles = this.getUploadingFiles(); - if(uploadedFile) { + if(uploadingFiles.length) { + return ( + + {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} + [{getLangText('cancel upload')}] + + ); + } else if(uploadedFile) { return ( @@ -111,8 +152,11 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, render() { - let { multiple, - allowedExtensions } = this.props; + const { + multiple, + allowedExtensions } = this.props; + const { disabled } = this.state; + /* * We do not want a button that submits here. @@ -122,14 +166,19 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = */ return (
- + disabled={disabled}> {this.getButtonLabel()} - + {this.getUploadedFileLabel()}
); diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index a9dd1039..b11a877f 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -47,7 +47,6 @@ const ReactS3FineUploader = React.createClass({ handleChangedFile: func, // for when a file is dropped or selected, TODO: rename to onChangedFile submitFile: func, // for when a file has been successfully uploaded, TODO: rename to onSubmitFile - onInactive: func, // for when the user does something while the uploader's inactive // Handle form validation setIsUploadReady: func, //TODO: rename to setIsUploaderValidated @@ -317,7 +316,7 @@ const ReactS3FineUploader = React.createClass({ // Cancel uploads and clear previously selected files on the input element cancelUploads(id) { - !!id ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); + typeof id !== 'undefined' ? 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. @@ -425,11 +424,13 @@ const ReactS3FineUploader = React.createClass({ if(fileId < filesToUpload.length) { const changeSet = { $set: url }; - const newFilesToUpload = React.addons.update(filesToUpload, { [fileId]: { thumbnailUrl: changeSet } }); + const newFilesToUpload = React.addons.update(filesToUpload, { + [fileId]: { thumbnailUrl: changeSet } + }); this.setState({ filesToUpload: newFilesToUpload }); } else { - throw new Error("You're accessing an index out of range of filesToUpload"); + throw new Error('Accessing an index out of range of filesToUpload'); } }, @@ -1052,13 +1053,12 @@ const ReactS3FineUploader = React.createClass({ render() { const { errorState: { errorClass }, filesToUpload, uploadInProgress } = this.state; const { - multiple, areAssetsDownloadable, areAssetsEditable, - onInactive, enableLocalHashing, fileClassToUpload, fileInputElement: FileInputElement, + multiple, showErrorPrompt, uploadMethod } = this.props; @@ -1069,7 +1069,6 @@ const ReactS3FineUploader = React.createClass({ multiple, areAssetsDownloadable, areAssetsEditable, - onInactive, enableLocalHashing, uploadMethod, fileClassToUpload, diff --git a/js/components/contract_notification.js b/js/components/contract_notification.js deleted file mode 100644 index cd6ceb53..00000000 --- a/js/components/contract_notification.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/js/components/error_not_found_page.js b/js/components/error_not_found_page.js index 61f83196..0e111ce7 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -6,6 +6,16 @@ import { getLangText } from '../utils/lang_utils'; let ErrorNotFoundPage = React.createClass({ + propTypes: { + message: React.PropTypes.string + }, + + getDefaultProps() { + return { + message: getLangText("Oops, the page you are looking for doesn't exist.") + }; + }, + render() { return (
@@ -13,7 +23,7 @@ let ErrorNotFoundPage = React.createClass({

404

- {getLangText('Ups, the page you are looking for does not exist.')} + {this.props.message}

diff --git a/js/components/footer.js b/js/components/footer.js index 65088ee2..31145d4b 100644 --- a/js/components/footer.js +++ b/js/components/footer.js @@ -11,6 +11,7 @@ let Footer = React.createClass({


api | + {getLangText('faq')} | {getLangText('imprint')} | {getLangText('terms of service')} | {getLangText('privacy')} diff --git a/js/components/global_notification.js b/js/components/global_notification.js index 59663b28..c1477f67 100644 --- a/js/components/global_notification.js +++ b/js/components/global_notification.js @@ -1,7 +1,9 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; +import GlobalNotificationActions from '../actions/global_notification_actions'; import GlobalNotificationStore from '../stores/global_notification_store'; import Row from 'react-bootstrap/lib/Row'; @@ -9,14 +11,18 @@ import Col from 'react-bootstrap/lib/Col'; import { mergeOptions } from '../utils/general_utils'; +const MAX_NOTIFICATION_BUBBLE_CONTAINER_WIDTH = 768; + let GlobalNotification = React.createClass({ getInitialState() { + const notificationStore = GlobalNotificationStore.getState(); + return mergeOptions( { containerWidth: 0 }, - this.extractFirstElem(GlobalNotificationStore.getState().notificationQue) + notificationStore ); }, @@ -36,35 +42,8 @@ let GlobalNotification = React.createClass({ window.removeEventListener('resize', this.handleContainerResize); }, - extractFirstElem(l) { - if(l.length > 0) { - return { - show: true, - message: l[0] - }; - } else { - return { - show: false, - message: '' - }; - } - }, - onChange(state) { - let notification = this.extractFirstElem(state.notificationQue); - - // error handling for notifications - if(notification.message && notification.type === 'danger') { - console.logGlobal(new Error(notification.message.message)); - } - - if(notification.show) { - this.setState(notification); - } else { - this.setState({ - show: false - }); - } + this.setState(state); }, handleContainerResize() { @@ -73,32 +52,31 @@ let GlobalNotification = React.createClass({ }); }, - render() { - let notificationClass = 'ascribe-global-notification'; - let textClass; + renderNotification() { + const { + notificationQueue: [notification], + notificationStatus, + notificationsPaused, + containerWidth } = this.state; - if(this.state.containerWidth > 768) { - notificationClass = 'ascribe-global-notification-bubble'; - - if(this.state.show) { - notificationClass += ' ascribe-global-notification-bubble-on'; - } else { - notificationClass += ' ascribe-global-notification-bubble-off'; - } + const notificationClasses = []; + if (this.state.containerWidth > 768) { + notificationClasses.push('ascribe-global-notification-bubble'); + notificationClasses.push(notificationStatus === 'show' ? 'ascribe-global-notification-bubble-on' + : 'ascribe-global-notification-bubble-off'); } else { - notificationClass = 'ascribe-global-notification'; - - if(this.state.show) { - notificationClass += ' ascribe-global-notification-on'; - } else { - notificationClass += ' ascribe-global-notification-off'; - } - + notificationClasses.push('ascribe-global-notification'); + notificationClasses.push(notificationStatus === 'show' ? 'ascribe-global-notification-on' + : 'ascribe-global-notification-off'); } - if(this.state.message) { - switch(this.state.message.type) { + let textClass; + let message; + if (notification && !notificationsPaused) { + message = notification.message; + + switch(notification.type) { case 'success': textClass = 'ascribe-global-notification-success'; break; @@ -106,18 +84,23 @@ let GlobalNotification = React.createClass({ textClass = 'ascribe-global-notification-danger'; break; default: - console.warn('Could not find a matching type in global_notification.js'); + console.warn('Could not find a matching notification type in global_notification.js'); } } - + return ( +

+
{message}
+
+ ); + }, + + render() { return (
-
-
{this.state.message.message}
-
+ {this.renderNotification()}
@@ -125,4 +108,4 @@ let GlobalNotification = React.createClass({ } }); -export default GlobalNotification; \ No newline at end of file +export default GlobalNotification; diff --git a/js/components/header.js b/js/components/header.js index 51f91318..b3fd543e 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -1,9 +1,10 @@ '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'; import CollapsibleNav from 'react-bootstrap/lib/CollapsibleNav'; @@ -58,11 +59,17 @@ 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() { @@ -135,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; @@ -201,8 +215,10 @@ let Header = React.createClass({ - + fixedTop={true} + ref="navbar"> + diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 3d4309f8..9424117c 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -13,6 +13,9 @@ import AccordionList from './ascribe_accordion_list/accordion_list'; import AccordionListItemWallet from './ascribe_accordion_list/accordion_list_item_wallet'; import AccordionListItemTableEditions from './ascribe_accordion_list/accordion_list_item_table_editions'; +import AclButtonList from './ascribe_buttons/acl_button_list.js'; +import DeleteButton from './ascribe_buttons/delete_button'; + import Pagination from './ascribe_pagination/pagination'; import PieceListFilterDisplay from './piece_list_filter_display'; @@ -22,7 +25,8 @@ import PieceListToolbar from './ascribe_piece_list_toolbar/piece_list_toolbar'; import AscribeSpinner from './ascribe_spinner'; -import { mergeOptions } from '../utils/general_utils'; +import { getAvailableAcls } from '../utils/acl_utils'; +import { mergeOptions, isShallowEqual } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; import { setDocumentTitle } from '../utils/dom_utils'; @@ -30,8 +34,11 @@ import { setDocumentTitle } from '../utils/dom_utils'; let PieceList = React.createClass({ propTypes: { accordionListItemType: React.PropTypes.func, + bulkModalButtonListType: React.PropTypes.func, + canLoadPieceList: React.PropTypes.bool, redirectTo: React.PropTypes.string, customSubmitButton: React.PropTypes.element, + customThumbnailPlaceholder: React.PropTypes.func, filterParams: React.PropTypes.array, orderParams: React.PropTypes.array, orderBy: React.PropTypes.string, @@ -43,6 +50,8 @@ let PieceList = React.createClass({ getDefaultProps() { return { accordionListItemType: AccordionListItemWallet, + bulkModalButtonListType: AclButtonList, + canLoadPieceList: true, orderParams: ['artist_name', 'title'], filterParams: [{ label: getLangText('Show works I can'), @@ -54,23 +63,53 @@ let PieceList = React.createClass({ }] }; }, + getInitialState() { - return mergeOptions( - PieceListStore.getState(), - EditionListStore.getState() + const pieceListStore = PieceListStore.getState(); + const stores = mergeOptions( + pieceListStore, + EditionListStore.getState(), + { + isFilterDirty: false + } ); + + // Use the default filters but use the pieceListStore's settings if they're available + stores.filterBy = Object.assign(this.getDefaultFilterBy(), pieceListStore.filterBy); + + return stores; }, componentDidMount() { - let page = this.props.location.query.page || 1; - PieceListStore.listen(this.onChange); EditionListStore.listen(this.onChange); - let orderBy = this.props.orderBy ? this.props.orderBy : this.state.orderBy; - if (this.state.pieceList.length === 0 || this.state.page !== page){ - PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, - orderBy, this.state.orderAsc, this.state.filterBy); + let page = this.props.location.query.page || 1; + if (this.props.canLoadPieceList && (this.state.pieceList.length === 0 || this.state.page !== page)) { + this.loadPieceList({ page }); + } + }, + + componentWillReceiveProps(nextProps) { + let filterBy; + let page = this.props.location.query.page || 1; + + // If the user hasn't changed the filter and the new default filter is different + // than the current filter, apply the new default filter + if (!this.state.isFilterDirty) { + const newDefaultFilterBy = this.getDefaultFilterBy(nextProps); + + // Only need to check shallowly since the filterBy shouldn't be nested + if (!isShallowEqual(this.state.filterBy, newDefaultFilterBy)) { + filterBy = newDefaultFilterBy; + page = 1; + } + } + + // Only load if we are applying a new filter or if it's the first time we can + // load the piece list + if (nextProps.canLoadPieceList && (filterBy || !this.props.canLoadPieceList)) { + this.loadPieceList({ page, filterBy }); } }, @@ -90,14 +129,29 @@ let PieceList = React.createClass({ this.setState(state); }, + getDefaultFilterBy(props = this.props) { + const { filterParams } = props; + const defaultFilterBy = {}; + + if (filterParams && typeof filterParams.forEach === 'function') { + filterParams.forEach(({ items }) => { + items.forEach((item) => { + if (typeof item === 'object' && item.defaultValue) { + defaultFilterBy[item.key] = true; + } + }); + }); + } + + return defaultFilterBy; + }, + paginationGoToPage(page) { return () => { // if the users clicks a pager of the pagination, // the site should go to the top document.body.scrollTop = document.documentElement.scrollTop = 0; - PieceListActions.fetchPieceList(page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, - this.state.filterBy); + this.loadPieceList({ page }); }; }, @@ -116,29 +170,35 @@ let PieceList = React.createClass({ }, searchFor(searchTerm) { - PieceListActions.fetchPieceList(1, this.state.pageSize, searchTerm, this.state.orderBy, - this.state.orderAsc, this.state.filterBy); - this.history.pushState(null, this.props.location.pathname, {page: 1}); + this.loadPieceList({ + page: 1, + search: searchTerm + }); + this.history.pushState(null, this.props.location.pathname, {page: 1}); }, applyFilterBy(filterBy){ - // first we need to apply the filter on the piece list - PieceListActions.fetchPieceList(1, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, filterBy) - .then(() => { - // but also, we need to filter all the open edition lists - this.state.pieceList - .forEach((piece) => { - // but only if they're actually open - if(this.state.isEditionListOpenForPieceId[piece.id].show) { - EditionListActions.refreshEditionList({ - pieceId: piece.id, - filterBy - }); - } + this.setState({ + isFilterDirty: true + }); - }); - }); + // first we need to apply the filter on the piece list + this + .loadPieceList({ page: 1, filterBy }) + .then(() => { + // but also, we need to filter all the open edition lists + this.state.pieceList + .forEach((piece) => { + // but only if they're actually open + if(this.state.isEditionListOpenForPieceId[piece.id].show) { + EditionListActions.refreshEditionList({ + pieceId: piece.id, + filterBy + }); + } + + }); + }); // we have to redirect the user always to page one as it could be that there is no page two // for filtered pieces @@ -150,35 +210,97 @@ let PieceList = React.createClass({ orderBy, this.state.orderAsc, this.state.filterBy); }, + loadPieceList({ page, filterBy = this.state.filterBy, search = this.state.search }) { + const orderBy = this.state.orderBy || this.props.orderBy; + + return PieceListActions.fetchPieceList(page, this.state.pageSize, search, + orderBy, this.state.orderAsc, filterBy); + }, + + fetchSelectedPieceEditionList() { + let filteredPieceIdList = Object.keys(this.state.editionList) + .filter((pieceId) => { + return this.state.editionList[pieceId] + .filter((edition) => edition.selected).length > 0; + }); + return filteredPieceIdList; + }, + + fetchSelectedEditionList() { + let selectedEditionList = []; + + Object + .keys(this.state.editionList) + .forEach((pieceId) => { + let filteredEditionsForPiece = this.state.editionList[pieceId].filter((edition) => edition.selected); + selectedEditionList = selectedEditionList.concat(filteredEditionsForPiece); + }); + + return selectedEditionList; + }, + + handleAclSuccess() { + PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, + this.state.orderBy, this.state.orderAsc, this.state.filterBy); + + this.fetchSelectedPieceEditionList() + .forEach((pieceId) => { + EditionListActions.refreshEditionList({pieceId}); + }); + EditionListActions.clearAllEditionSelections(); + }, + render() { - let loadingElement = ; - let AccordionListItemType = this.props.accordionListItemType; + const { + accordionListItemType: AccordionListItemType, + bulkModalButtonListType: BulkModalButtonListType, + customSubmitButton, + customThumbnailPlaceholder, + filterParams, + orderParams } = this.props; + + const loadingElement = ; + + const selectedEditions = this.fetchSelectedEditionList(); + const availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); setDocumentTitle(getLangText('Collection')); - return (
- {this.props.customSubmitButton ? - this.props.customSubmitButton : + {customSubmitButton ? + customSubmitButton : } - + + + + + + filterParams={filterParams}/>
diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 322b9934..8211e91e 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -44,11 +44,8 @@ let RegisterPiece = React.createClass( { return mergeOptions( UserStore.getState(), WhitelabelStore.getState(), - PieceListStore.getState(), - { - selectedLicense: 0, - isFineUploaderActive: false - }); + PieceListStore.getState() + ); }, componentDidMount() { @@ -66,13 +63,6 @@ let RegisterPiece = React.createClass( { onChange(state) { this.setState(state); - - if(this.state.currentUser && this.state.currentUser.email) { - // we should also make the fineuploader component editable again - this.setState({ - isFineUploaderActive: true - }); - } }, handleSuccess(response){ @@ -118,7 +108,7 @@ let RegisterPiece = React.createClass( { {this.props.children} diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js b/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js new file mode 100644 index 00000000..e69de29b diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js index a2a70a97..0fbca419 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js @@ -63,6 +63,11 @@ const PRRegisterPiece = React.createClass({

Portfolio Review

{getLangText('Submission closing on %s', ' 22 Dec 2015')}

+

For more information, visit:  + + portfolio-review.de + +

{getLangText("You're submitting as %s. ", currentUser.email)} {getLangText('Change account?')} diff --git a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js index 93ca50f3..982af7b0 100644 --- a/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_detail/prize_piece_container.js @@ -51,8 +51,7 @@ import { setDocumentTitle } from '../../../../../../utils/dom_utils'; */ let PieceContainer = React.createClass({ propTypes: { - params: React.PropTypes.object, - location: React.PropTypes.object + params: React.PropTypes.object }, getInitialState() { @@ -111,7 +110,7 @@ let PieceContainer = React.createClass({ }, render() { - if(this.state.piece && this.state.piece.title) { + if(this.state.piece && this.state.piece.id) { /* This really needs a refactor! @@ -162,7 +161,7 @@ let PieceContainer = React.createClass({ piece={this.state.piece} currentUser={this.state.currentUser}/> }> - + ); } else { @@ -292,8 +291,8 @@ let PrizePieceRatings = React.createClass({ url={ApiUrls.ownership_loans_pieces_request} email={this.props.currentUser.email} gallery={this.props.piece.prize.name} - startdate={today} - enddate={endDate} + startDate={today} + endDate={endDate} showPersonalMessage={true} showPassword={false} handleSuccess={this.handleLoanSuccess} /> @@ -426,8 +425,7 @@ let PrizePieceRatings = React.createClass({ let PrizePieceDetails = React.createClass({ propTypes: { - piece: React.PropTypes.object, - location: React.PropTypes.object + piece: React.PropTypes.object }, render() { @@ -464,8 +462,7 @@ let PrizePieceDetails = React.createClass({ overrideForm={true} pieceId={this.props.piece.id} otherData={this.props.piece.other_data} - multiple={true} - location={location}/> + multiple={true} /> ); diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js new file mode 100644 index 00000000..f360c932 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js @@ -0,0 +1,15 @@ +'use strict' + +import React from 'react'; + +let Vivi23AccordionListItemThumbnailPlaceholder = React.createClass({ + render() { + return ( + + 23 + + ); + } +}); + +export default Vivi23AccordionListItemThumbnailPlaceholder; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js new file mode 100644 index 00000000..302495a0 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js @@ -0,0 +1,78 @@ +'use strict'; + +import React from 'react'; + +import Button from 'react-bootstrap/lib/Button'; +import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; + +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; + +let Vivi23Landing = React.createClass({ + getInitialState() { + return WhitelabelStore.getState(); + }, + + componentWillMount() { + setDocumentTitle('23VIVI Marketplace'); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + return ( +

+
+
+
+ +
+ {getLangText('Artwork from the 23VIVI Marketplace is powered by') + ' '} + +
+
+
+
+

+ {getLangText('Existing ascribe user?')} +

+ + + +
+
+

+ {getLangText('Do you need an account?')} +

+ + + +
+
+
+
+
+ ); + } +}); + +export default Vivi23Landing; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js new file mode 100644 index 00000000..0bfb8aa0 --- /dev/null +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js @@ -0,0 +1,24 @@ +'use strict' + +import React from 'react'; + +import Vivi23AccordionListItemThumbnailPlaceholder from './23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder'; + +import MarketPieceList from '../market/market_piece_list'; + +let Vivi23PieceList = React.createClass({ + propTypes: { + location: React.PropTypes.object + }, + + render() { + return ( + + ); + } + +}); + +export default Vivi23PieceList; diff --git a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js index b263e517..26a186ca 100644 --- a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js +++ b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js @@ -30,7 +30,7 @@ let WalletPieceContainer = React.createClass({ render() { - if(this.props.piece && this.props.piece.title) { + if(this.props.piece && this.props.piece.id) { return ( + expanded={!disabled || !!piece.extra_data.artist_bio}> + expanded={!disabled || !!piece.extra_data.artist_contact_information}> + expanded={!disabled || !!piece.extra_data.conceptual_overview}> + expanded={!disabled || !!piece.extra_data.medium}> + expanded={!disabled || !!piece.extra_data.size_duration}> + expanded={!disabled || !!piece.extra_data.display_instructions}> + expanded={!disabled || !!piece.extra_data.additional_details}> +
-
+
diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index 470da761..42b7c1ad 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -53,8 +53,6 @@ let CylandRegisterPiece = React.createClass({ PieceStore.getState(), WhitelabelStore.getState(), { - selectedLicense: 0, - isFineUploaderActive: false, step: 0 }); }, @@ -90,13 +88,6 @@ let CylandRegisterPiece = React.createClass({ onChange(state) { this.setState(state); - - if(this.state.currentUser && this.state.currentUser.email) { - // we should also make the fineuploader component editable again - this.setState({ - isFineUploaderActive: true - }); - } }, handleRegisterSuccess(response){ @@ -167,11 +158,6 @@ let CylandRegisterPiece = React.createClass({ } }, - // basically redirects to the second slide (index: 1), when the user is not logged in - onLoggedOut() { - this.history.pushState(null, '/login'); - }, - render() { let today = new Moment(); @@ -197,9 +183,8 @@ let CylandRegisterPiece = React.createClass({ enableLocalHashing={false} headerMessage={getLangText('Submit to Cyland Archive')} submitMessage={getLangText('Submit')} - isFineUploaderActive={this.state.isFineUploaderActive} + isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - onLoggedOut={this.onLoggedOut} location={this.props.location}/> @@ -229,8 +214,8 @@ let CylandRegisterPiece = React.createClass({ url={ApiUrls.ownership_loans_pieces} email={this.state.whitelabel.user} gallery="Cyland Archive" - startdate={today} - enddate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain} + startDate={today} + endDate={datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain} showStartDate={false} showEndDate={false} showPersonalMessage={false} diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js index 4e2f6a63..df58b7c7 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js @@ -123,7 +123,7 @@ let IkonotvPieceContainer = React.createClass({ ); } - 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 ( + expanded={!this.props.disabled || !!this.props.piece.extra_data.artist_website}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.gallery_website}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_websites}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.conceptual_overview}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.medium}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.size_duration}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.courtesy_of}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.copyright_of_photography}> + expanded={!this.props.disabled || !!this.props.piece.extra_data.additional_details}> + location={this.props.location} + pageExitWarning={pageExitWarning}>
@@ -255,9 +250,8 @@ let IkonotvRegisterPiece = React.createClass({ enableLocalHashing={false} headerMessage={getLangText('Register work')} submitMessage={getLangText('Register')} - isFineUploaderActive={this.state.isFineUploaderActive} + isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - onLoggedOut={this.onLoggedOut} location={this.props.location}/> diff --git a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js new file mode 100644 index 00000000..e68b1781 --- /dev/null +++ b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js @@ -0,0 +1,84 @@ +'use strict'; + +import React from 'react'; + +import Button from 'react-bootstrap/lib/Button'; +import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; + +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; + + +let LumenusLanding = React.createClass({ + + getInitialState() { + return mergeOptions( + WhitelabelStore.getState() + ); + }, + + componentWillMount() { + setDocumentTitle('Lumenus Marketplace'); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + return ( +
+
+
+
+ +
+ {getLangText('Artwork from the Lumenus Marketplace is powered by') + ' '} + + + +
+
+
+
+

+ {getLangText('Existing ascribe user?')} +

+ + + +
+
+

+ {getLangText('Do you need an account?')} +

+ + + +
+
+
+
+
+ ); + } +}); + +export default LumenusLanding; diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js new file mode 100644 index 00000000..1dcdd4e5 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js @@ -0,0 +1,74 @@ +'use strict'; + +import React from 'react'; + +import MarketSubmitButton from './market_submit_button'; + +import DeleteButton from '../../../../../ascribe_buttons/delete_button'; +import EmailButton from '../../../../../ascribe_buttons/acls/email_button'; +import TransferButton from '../../../../../ascribe_buttons/acls/transfer_button'; +import UnconsignButton from '../../../../../ascribe_buttons/acls/unconsign_button'; + +import UserActions from '../../../../../../actions/user_actions'; +import UserStore from '../../../../../../stores/user_store'; + +let MarketAclButtonList = React.createClass({ + propTypes: { + availableAcls: React.PropTypes.object.isRequired, + className: React.PropTypes.string, + pieceOrEditions: React.PropTypes.array, + handleSuccess: React.PropTypes.func, + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) + }, + + getInitialState() { + return UserStore.getState(); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + UserActions.fetchCurrentUser(); + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + let { availableAcls, className, pieceOrEditions, handleSuccess } = this.props; + return ( +
+ + + + + {this.props.children} +
+ ); + } +}); + +export default MarketAclButtonList; diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js new file mode 100644 index 00000000..d8ef4c41 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js @@ -0,0 +1,160 @@ +'use strict'; + +import React from 'react'; +import classNames from 'classnames'; + +import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; + +import AclFormFactory from '../../../../../ascribe_forms/acl_form_factory'; +import ConsignForm from '../../../../../ascribe_forms/form_consign'; + +import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; + +import AclProxy from '../../../../../acl_proxy'; + +import PieceActions from '../../../../../../actions/piece_actions'; +import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../../stores/whitelabel_store'; + +import ApiUrls from '../../../../../../constants/api_urls'; + +import { getAclFormMessage, getAclFormDataId } from '../../../../../../utils/form_utils'; +import { getLangText } from '../../../../../../utils/lang_utils'; + +let MarketSubmitButton = React.createClass({ + propTypes: { + availableAcls: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object, + editions: React.PropTypes.array.isRequired, + handleSuccess: React.PropTypes.func.isRequired, + className: React.PropTypes.string, + }, + + getInitialState() { + return WhitelabelStore.getState(); + }, + + componentDidMount() { + WhitelabelStore.listen(this.onChange); + + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + canEditionBeSubmitted(edition) { + if (edition && edition.extra_data && edition.other_data) { + const { extra_data, other_data } = edition; + + if (extra_data.artist_bio && extra_data.work_description && + extra_data.technology_details && extra_data.display_instructions && + other_data.length > 0) { + return true; + } + } + + return false; + }, + + getFormDataId() { + return getAclFormDataId(false, this.props.editions); + }, + + getAggregateEditionDetails() { + const { editions } = this.props; + + // Currently, we only care if all the given editions are from the same parent piece + // and if they can be submitted + return editions.reduce((details, curEdition) => { + return { + solePieceId: details.solePieceId === curEdition.parent ? details.solePieceId : null, + canSubmit: details.canSubmit && this.canEditionBeSubmitted(curEdition) + }; + }, { + solePieceId: editions.length > 0 ? editions[0].parent : null, + canSubmit: this.canEditionBeSubmitted(editions[0]) + }); + }, + + handleAdditionalDataSuccess(pieceId) { + // Fetch newly updated piece to update the views + PieceActions.fetchOne(pieceId); + + this.refs.consignModal.show(); + }, + + render() { + const { availableAcls, currentUser, className, editions, handleSuccess } = this.props; + const { whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail } } = this.state; + const { solePieceId, canSubmit } = this.getAggregateEditionDetails(); + const message = getAclFormMessage({ + aclName: 'acl_consign', + entities: editions, + isPiece: false, + additionalMessage: getLangText('Suggested price:'), + senderName: currentUser.username + }); + + const triggerButton = ( + + ); + const consignForm = ( + + ); + + if (solePieceId && !canSubmit) { + return ( + + + + + + + {consignForm} + + + ); + } else { + return ( + + + {consignForm} + + + ); + } + } +}); + +export default MarketSubmitButton; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js b/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js new file mode 100644 index 00000000..97284dbc --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_edition_container.js @@ -0,0 +1,24 @@ +'use strict'; + +import React from 'react'; + +import MarketFurtherDetails from './market_further_details'; + +import MarketAclButtonList from '../market_buttons/market_acl_button_list'; + +import EditionContainer from '../../../../../ascribe_detail/edition_container'; + +let MarketEditionContainer = React.createClass({ + propTypes: EditionContainer.propTypes, + + render() { + return ( + + ); + } +}); + +export default MarketEditionContainer; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js b/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js new file mode 100644 index 00000000..4e1e3ee8 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_further_details.js @@ -0,0 +1,23 @@ +'use strict'; + +import React from 'react'; + +import MarketAdditionalDataForm from '../market_forms/market_additional_data_form' + +let MarketFurtherDetails = React.createClass({ + propTypes: { + pieceId: React.PropTypes.number, + handleSuccess: React.PropTypes.func, + }, + + render() { + return ( + + ); + } +}); + +export default MarketFurtherDetails; diff --git a/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js b/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js new file mode 100644 index 00000000..d41ade56 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_detail/market_piece_container.js @@ -0,0 +1,21 @@ +'use strict'; + +import React from 'react'; + +import MarketFurtherDetails from './market_further_details'; + +import PieceContainer from '../../../../../ascribe_detail/piece_container'; + +let MarketPieceContainer = React.createClass({ + propTypes: PieceContainer.propTypes, + + render() { + return ( + + ); + } +}); + +export default MarketPieceContainer; diff --git a/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js new file mode 100644 index 00000000..d136c9cf --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js @@ -0,0 +1,235 @@ +'use strict'; + +import React from 'react'; + +import Form from '../../../../../ascribe_forms/form'; +import Property from '../../../../../ascribe_forms/property'; + +import InputTextAreaToggable from '../../../../../ascribe_forms/input_textarea_toggable'; + +import FurtherDetailsFileuploader from '../../../../../ascribe_detail/further_details_fileuploader'; +import AscribeSpinner from '../../../../../ascribe_spinner'; + +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; + +import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_s3_fine_uploader_utils'; + +import PieceActions from '../../../../../../actions/piece_actions'; +import PieceStore from '../../../../../../stores/piece_store'; + +import ApiUrls from '../../../../../../constants/api_urls'; +import AppConstants from '../../../../../../constants/application_constants'; + +import requests from '../../../../../../utils/requests'; +import { mergeOptions } from '../../../../../../utils/general_utils'; +import { getLangText } from '../../../../../../utils/lang_utils'; + + +let MarketAdditionalDataForm = React.createClass({ + propTypes: { + pieceId: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.string + ]), + editable: React.PropTypes.bool, + isInline: React.PropTypes.bool, + showHeading: React.PropTypes.bool, + showNotification: React.PropTypes.bool, + submitLabel: React.PropTypes.string, + handleSuccess: React.PropTypes.func + }, + + getDefaultProps() { + return { + editable: true, + submitLabel: getLangText('Register work') + }; + }, + + getInitialState() { + const pieceStore = PieceStore.getState(); + + return mergeOptions( + pieceStore, + { + // Allow the form to be submitted if there's already an additional image uploaded + isUploadReady: this.isUploadReadyOnChange(pieceStore.piece), + forceUpdateKey: 0 + }); + }, + + componentDidMount() { + PieceStore.listen(this.onChange); + + if (this.props.pieceId) { + PieceActions.fetchOne(this.props.pieceId); + } + }, + + componentWillUnmount() { + PieceStore.unlisten(this.onChange); + }, + + onChange(state) { + Object.assign({}, state, { + // Allow the form to be submitted if the updated piece already has an additional image uploaded + isUploadReady: this.isUploadReadyOnChange(state.piece), + + /** + * Increment the forceUpdateKey to force the form to rerender on each change + * + * THIS IS A HACK TO MAKE SURE THE FORM ALWAYS DISPLAYS THE MOST RECENT STATE + * BECAUSE SOME OF OUR FORM ELEMENTS DON'T UPDATE FROM PROP CHANGES (ie. + * InputTextAreaToggable). + */ + forceUpdateKey: this.state.forceUpdateKey + 1 + }); + + this.setState(state); + }, + + getFormData() { + let extradata = {}; + let formRefs = this.refs.form.refs; + + // Put additional fields in extra data object + Object + .keys(formRefs) + .forEach((fieldName) => { + extradata[fieldName] = formRefs[fieldName].state.value; + }); + + return { + extradata: extradata, + piece_id: this.state.piece.id + }; + }, + + isUploadReadyOnChange(piece) { + return piece && piece.other_data && piece.other_data.length > 0; + }, + + handleSuccessWithNotification() { + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(); + } + + let notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + setIsUploadReady(isReady) { + this.setState({ + isUploadReady: isReady + }); + }, + + render() { + const { editable, isInline, handleSuccess, showHeading, showNotification, submitLabel } = this.props; + const { piece } = this.state; + let buttons, heading; + + let spinner = ; + + if (!isInline) { + buttons = ( + + ); + + spinner = ( +
+

+ {spinner} +

+
+ ); + + heading = showHeading ? ( +
+

+ {getLangText('Provide additional details')} +

+
+ ) : null; + } + + if (piece && piece.id) { + return ( +
+ {heading} + + + + + + + + + + + + + + + ); + } else { + return ( +
+ {spinner} +
+ ); + } + } +}); + +export default MarketAdditionalDataForm; diff --git a/js/components/whitelabel/wallet/components/market/market_piece_list.js b/js/components/whitelabel/wallet/components/market/market_piece_list.js new file mode 100644 index 00000000..1c74e6de --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_piece_list.js @@ -0,0 +1,90 @@ +'use strict'; + +import React from 'react'; + +import MarketAclButtonList from './market_buttons/market_acl_button_list'; + +import PieceList from '../../../../piece_list'; + +import UserActions from '../../../../../actions/user_actions'; +import UserStore from '../../../../../stores/user_store'; +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import { setDocumentTitle } from '../../../../../utils/dom_utils'; +import { mergeOptions } from '../../../../../utils/general_utils'; +import { getLangText } from '../../../../../utils/lang_utils'; + +let MarketPieceList = React.createClass({ + propTypes: { + customThumbnailPlaceholder: React.PropTypes.func, + location: React.PropTypes.object + }, + + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + componentWillMount() { + setDocumentTitle(getLangText('Collection')); + }, + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + const { customThumbnailPlaceholder, location } = this.props; + const { + currentUser: { email: userEmail }, + whitelabel: { + name: whitelabelName = 'Market', + user: whitelabelAdminEmail + } } = this.state; + + let filterParams = null; + let canLoadPieceList = false; + + if (userEmail && whitelabelAdminEmail) { + canLoadPieceList = true; + const isUserAdmin = userEmail === whitelabelAdminEmail; + + filterParams = [{ + label: getLangText('Show works I can'), + items: [{ + key: isUserAdmin ? 'acl_transfer' : 'acl_consign', + label: getLangText(isUserAdmin ? 'transfer' : 'consign to %s', whitelabelName), + defaultValue: true + }] + }]; + } + + return ( + + ); + } +}); + +export default MarketPieceList; diff --git a/js/components/whitelabel/wallet/components/market/market_register_piece.js b/js/components/whitelabel/wallet/components/market/market_register_piece.js new file mode 100644 index 00000000..387934f9 --- /dev/null +++ b/js/components/whitelabel/wallet/components/market/market_register_piece.js @@ -0,0 +1,174 @@ +'use strict'; + +import React from 'react'; +import { History } from 'react-router'; + +import Col from 'react-bootstrap/lib/Col'; +import Row from 'react-bootstrap/lib/Row'; + +import MarketAdditionalDataForm from './market_forms/market_additional_data_form'; + +import Property from '../../../../ascribe_forms/property'; +import RegisterPieceForm from '../../../../ascribe_forms/form_register_piece'; + +import PieceActions from '../../../../../actions/piece_actions'; +import PieceListStore from '../../../../../stores/piece_list_store'; +import PieceListActions from '../../../../../actions/piece_list_actions'; +import UserStore from '../../../../../stores/user_store'; +import UserActions from '../../../../../actions/user_actions'; +import WhitelabelActions from '../../../../../actions/whitelabel_actions'; +import WhitelabelStore from '../../../../../stores/whitelabel_store'; + +import SlidesContainer from '../../../../ascribe_slides_container/slides_container'; + +import { getLangText } from '../../../../../utils/lang_utils'; +import { setDocumentTitle } from '../../../../../utils/dom_utils'; +import { mergeOptions } from '../../../../../utils/general_utils'; + +let MarketRegisterPiece = React.createClass({ + propTypes: { + location: React.PropTypes.object + }, + + mixins: [History], + + getInitialState(){ + return mergeOptions( + PieceListStore.getState(), + UserStore.getState(), + WhitelabelStore.getState(), + { + step: 0 + }); + }, + + componentDidMount() { + PieceListStore.listen(this.onChange); + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + + // Reset the piece store to make sure that we don't display old data + // if the user repeatedly registers works + PieceActions.updatePiece({}); + }, + + componentWillUnmount() { + PieceListStore.unlisten(this.onChange); + UserStore.unlisten(this.onChange); + WhitelabelStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + handleRegisterSuccess(response) { + this.refreshPieceList(); + + // Use the response's piece for the next step if available + let pieceId = null; + if (response && response.piece) { + pieceId = response.piece.id; + PieceActions.updatePiece(response.piece); + } + + this.incrementStep(); + this.refs.slidesContainer.nextSlide({ piece_id: pieceId }); + }, + + handleAdditionalDataSuccess() { + this.refreshPieceList(); + + this.history.pushState(null, '/collection'); + }, + + // We need to increase the step to lock the forms that are already filled out + incrementStep() { + this.setState({ + step: this.state.step + 1 + }); + }, + + getPieceFromQueryParam() { + const queryParams = this.props.location.query; + + // Since every step of this register process is atomic, + // we may need to enter the process at step 1 or 2. + // If this is the case, we'll need the piece number to complete submission. + // It is encoded in the URL as a queryParam and we're checking for it here. + return queryParams && queryParams.piece_id; + }, + + refreshPieceList() { + PieceListActions.fetchPieceList( + this.state.page, + this.state.pageSize, + this.state.searchTerm, + this.state.orderBy, + this.state.orderAsc, + this.state.filterBy + ); + }, + + render() { + const { + step, + whitelabel: { + name: whitelabelName = 'Market' + } } = this.state; + + setDocumentTitle(getLangText('Register a new piece')); + + return ( + +
+ + + 0} + enableLocalHashing={false} + headerMessage={getLangText('Consign to %s', whitelabelName)} + submitMessage={getLangText('Proceed to additional details')} + isFineUploaderActive={true} + enableSeparateThumbnail={false} + handleSuccess={this.handleRegisterSuccess} + location={this.props.location}> + + + + + + +
+
+ + + + + +
+
+ ); + } +}); + +export default MarketRegisterPiece; diff --git a/js/components/whitelabel/wallet/constants/wallet_api_urls.js b/js/components/whitelabel/wallet/constants/wallet_api_urls.js index 2cdc0054..8ad2eb81 100644 --- a/js/components/whitelabel/wallet/constants/wallet_api_urls.js +++ b/js/components/whitelabel/wallet/constants/wallet_api_urls.js @@ -4,22 +4,30 @@ import walletConstants from './wallet_application_constants'; // gets subdomain as a parameter function getWalletApiUrls(subdomain) { - if (subdomain === 'cyland'){ + if (subdomain === 'cyland') { return { 'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/', 'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/', 'piece_extradata': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/extradata/', 'user': walletConstants.walletApiEndpoint + subdomain + '/users/' }; - } - else if (subdomain === 'ikonotv'){ + } else if (subdomain === 'ikonotv') { return { 'pieces_list': walletConstants.walletApiEndpoint + subdomain + '/pieces/', 'piece': walletConstants.walletApiEndpoint + subdomain + '/pieces/${piece_id}/', 'user': walletConstants.walletApiEndpoint + subdomain + '/users/' }; + } else if (subdomain === 'lumenus' || subdomain === '23vivi') { + return { + 'editions_list': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/editions/', + 'edition': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/editions/${bitcoin_id}/', + 'pieces_list': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/', + 'piece': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/', + 'piece_extradata': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/pieces/${piece_id}/extradata/', + 'user': walletConstants.walletApiEndpoint + 'markets/' + subdomain + '/users/' + }; } return {}; } -export default getWalletApiUrls; \ No newline at end of file +export default getWalletApiUrls; diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 5056716a..c2810fd0 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -32,7 +32,7 @@ let WalletApp = React.createClass({ // if the path of the current activeRoute is not defined, then this is the IndexRoute if ((!path || history.isActive('/login') || history.isActive('/signup') || history.isActive('/contract_notifications')) - && (['ikonotv']).indexOf(subdomain) > -1) { + && (['cyland', 'ikonotv', 'lumenus', '23vivi']).indexOf(subdomain) > -1) { header = (
); } else { header =
; diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index 8e4d5197..0a4e3a58 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -16,6 +16,8 @@ import SettingsContainer from '../../../components/ascribe_settings/settings_con import ContractSettings from '../../../components/ascribe_settings/contract_settings'; import ErrorNotFoundPage from '../../../components/error_not_found_page'; +import CCRegisterPiece from './components/cc/cc_register_piece'; + import CylandLanding from './components/cyland/cyland_landing'; import CylandPieceContainer from './components/cyland/cyland_detail/cyland_piece_container'; import CylandRegisterPiece from './components/cyland/cyland_register_piece'; @@ -23,12 +25,20 @@ import CylandPieceList from './components/cyland/cyland_piece_list'; import IkonotvLanding from './components/ikonotv/ikonotv_landing'; import IkonotvPieceList from './components/ikonotv/ikonotv_piece_list'; -import ContractAgreementForm from '../../../components/ascribe_forms/form_contract_agreement'; +import SendContractAgreementForm from '../../../components/ascribe_forms/form_send_contract_agreement'; import IkonotvRegisterPiece from './components/ikonotv/ikonotv_register_piece'; import IkonotvPieceContainer from './components/ikonotv/ikonotv_detail/ikonotv_piece_container'; import IkonotvContractNotifications from './components/ikonotv/ikonotv_contract_notifications'; -import CCRegisterPiece from './components/cc/cc_register_piece'; +import MarketPieceList from './components/market/market_piece_list'; +import MarketRegisterPiece from './components/market/market_register_piece'; +import MarketPieceContainer from './components/market/market_detail/market_piece_container'; +import MarketEditionContainer from './components/market/market_detail/market_edition_container'; + +import LumenusLanding from './components/lumenus/lumenus_landing'; + +import Vivi23Landing from './components/23vivi/23vivi_landing'; +import Vivi23PieceList from './components/23vivi/23vivi_piece_list'; import AuthProxyHandler from '../../../components/ascribe_routes/proxy_routes/auth_proxy_handler'; @@ -128,7 +138,7 @@ let ROUTES = { component={AuthProxyHandler({to: '/login', when: 'loggedOut'})(ContractSettings)}/> + ), + 'lumenus': ( + + + + + + + + + + + + + + + + ), + '23vivi': ( + + + + + + + + + + + + + + + ) }; - function getRoutes(commonRoutes, subdomain) { if(subdomain in ROUTES) { return ROUTES[subdomain]; @@ -160,5 +240,4 @@ function getRoutes(commonRoutes, subdomain) { } } - export default getRoutes; diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index a07f29b1..e7f11141 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -72,6 +72,9 @@ let ApiUrls = { 'users_username': AppConstants.apiEndpoint + 'users/username/', 'users_profile': AppConstants.apiEndpoint + 'users/profile/', 'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/', + 'webhook': AppConstants.apiEndpoint + 'webhooks/${webhook_id}/', + 'webhooks': AppConstants.apiEndpoint + 'webhooks/', + 'webhooks_events': AppConstants.apiEndpoint + 'webhooks/events/', 'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/', 'delete_s3_file': AppConstants.serverUrl + 's3/delete/', 'prize_list': AppConstants.apiEndpoint + 'prize/' diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 79d00747..74edc51b 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -51,6 +51,20 @@ const constants = { 'permissions': ['register', 'edit', 'share', 'del_from_collection'], 'type': 'wallet' }, + { + 'subdomain': 'lumenus', + 'name': 'Lumenus', + 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/lumenus/lumenus-logo.png', + 'permissions': ['register', 'edit', 'share', 'del_from_collection'], + 'type': 'wallet' + }, + { + 'subdomain': '23vivi', + 'name': '23VIVI', + 'logo': 'https://s3-us-west-2.amazonaws.com/ascribe0/whitelabel/23vivi/23vivi-logo.png', + 'permissions': ['register', 'edit', 'share', 'del_from_collection'], + 'type': 'wallet' + }, { 'subdomain': 'portfolioreview', 'name': 'Portfolio Review', @@ -124,7 +138,12 @@ const constants = { }, 'twitter': { 'sdkUrl': 'https://platform.twitter.com/widgets.js' - } + }, + + 'errorMessagesToIgnore': [ + 'Authentication credentials were not provided.', + 'Informations d\'authentification non fournies.' + ] }; export default constants; diff --git a/js/mixins/react_error.js b/js/mixins/react_error.js new file mode 100644 index 00000000..14f33a61 --- /dev/null +++ b/js/mixins/react_error.js @@ -0,0 +1,16 @@ +'use strict'; + +import invariant from 'invariant'; + +const ReactError = { + throws(err) { + if(!err.handler) { + invariant(err.handler, 'Error thrown to ReactError did not have a `handler` function'); + console.logGlobal('Error thrown to ReactError did not have a `handler` function'); + } else { + err.handler(this, err); + } + } +}; + +export default ReactError; diff --git a/js/models/errors.js b/js/models/errors.js new file mode 100644 index 00000000..4573afe4 --- /dev/null +++ b/js/models/errors.js @@ -0,0 +1,31 @@ +'use strict'; + +import React from 'react'; + +import ErrorNotFoundPage from '../components/error_not_found_page'; + + +export class ResourceNotFoundError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + this.message = message; + + // `captureStackTrace` might not be available in IE: + // - http://stackoverflow.com/a/8460753/1263876 + if(Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor.name); + } + } + + handler(component, err) { + const monkeyPatchedKey = `_${this.name}MonkeyPatched`; + + if(!component.state[monkeyPatchedKey]) { + component.render = () => ; + component.setState({ + [monkeyPatchedKey]: true + }); + } + } +} diff --git a/js/sources/webhook_source.js b/js/sources/webhook_source.js new file mode 100644 index 00000000..5351c89c --- /dev/null +++ b/js/sources/webhook_source.js @@ -0,0 +1,46 @@ +'use strict'; + +import requests from '../utils/requests'; + +import WebhookActions from '../actions/webhook_actions'; + + +const WebhookSource = { + lookupWebhooks: { + remote() { + return requests.get('webhooks'); + }, + local(state) { + return state.webhooks && !Object.keys(state.webhooks).length ? state : {}; + }, + success: WebhookActions.successFetchWebhooks, + error: WebhookActions.errorWebhooks, + shouldFetch(state) { + return state.webhookMeta.invalidateCache || state.webhooks && !Object.keys(state.webhooks).length; + } + }, + + lookupWebhookEvents: { + remote() { + return requests.get('webhooks_events'); + }, + local(state) { + return state.webhookEvents && !Object.keys(state.webhookEvents).length ? state : {}; + }, + success: WebhookActions.successFetchWebhookEvents, + error: WebhookActions.errorWebhookEvents, + shouldFetch(state) { + return state.webhookEventsMeta.invalidateCache || state.webhookEvents && !Object.keys(state.webhookEvents).length; + } + }, + + performRemoveWebhook: { + remote(state) { + return requests.delete('webhook', {'webhook_id': state.webhookMeta.idToDelete }); + }, + success: WebhookActions.successRemoveWebhook, + error: WebhookActions.errorWebhooks + } +}; + +export default WebhookSource; \ No newline at end of file diff --git a/js/stores/edition_list_store.js b/js/stores/edition_list_store.js index 4ccada4e..107f9af4 100644 --- a/js/stores/edition_list_store.js +++ b/js/stores/edition_list_store.js @@ -60,7 +60,7 @@ class EditionListStore { * We often just have to refresh the edition list for a certain pieceId, * this method provides exactly that functionality without any side effects */ - onRefreshEditionList({pieceId, filterBy}) { + onRefreshEditionList({pieceId, filterBy = {}}) { // It may happen that the user enters the site logged in already // through /editions // If he then tries to delete a piece/edition and this method is called, diff --git a/js/stores/edition_store.js b/js/stores/edition_store.js index 14ee4fee..22e78d23 100644 --- a/js/stores/edition_store.js +++ b/js/stores/edition_store.js @@ -7,11 +7,17 @@ import EditionActions from '../actions/edition_actions'; class EditionStore { constructor() { this.edition = {}; + this.editionError = null; this.bindActions(EditionActions); } onUpdateEdition(edition) { this.edition = edition; + this.editionError = null; + } + + onEditionFailed(error) { + this.editionError = error; } } diff --git a/js/stores/global_notification_store.js b/js/stores/global_notification_store.js index 5a23fe1b..7414812b 100644 --- a/js/stores/global_notification_store.js +++ b/js/stores/global_notification_store.js @@ -4,36 +4,63 @@ import { alt } from '../alt'; import GlobalNotificationActions from '../actions/global_notification_actions'; +const GLOBAL_NOTIFICATION_COOLDOWN = 400; + class GlobalNotificationStore { constructor() { - this.notificationQue = []; + this.notificationQueue = []; + this.notificationStatus = 'ready'; + this.notificationsPaused = false; this.bindActions(GlobalNotificationActions); } onAppendGlobalNotification(newNotification) { - let notificationDelay = 0; - for(let i = 0; i < this.notificationQue.length; i++) { - notificationDelay += this.notificationQue[i].dismissAfter; - } + this.notificationQueue.push(newNotification); - this.notificationQue.push(newNotification); - setTimeout(GlobalNotificationActions.emulateEmptyStore, notificationDelay + newNotification.dismissAfter); + if (!this.notificationsPaused && this.notificationStatus === 'ready') { + this.showNextNotification(); + } } - onEmulateEmptyStore() { - let actualNotificitionQue = this.notificationQue.slice(); + showNextNotification() { + this.notificationStatus = 'show'; - this.notificationQue = []; + setTimeout(GlobalNotificationActions.cooldownGlobalNotifications, this.notificationQueue[0].dismissAfter); + } - setTimeout(() => { - this.notificationQue = actualNotificitionQue.slice(); - GlobalNotificationActions.shiftGlobalNotification(); - }, 400); + onCooldownGlobalNotifications() { + // When still paused on cooldown, don't shift the queue so we can repeat the current notification. + if (!this.notificationsPaused) { + this.notificationStatus = 'cooldown'; + + // Leave some time between consecutive notifications + setTimeout(GlobalNotificationActions.shiftGlobalNotification, GLOBAL_NOTIFICATION_COOLDOWN); + } else { + this.notificationStatus = 'ready'; + } } onShiftGlobalNotification() { - this.notificationQue.shift(); + this.notificationQueue.shift(); + + if (!this.notificationsPaused && this.notificationQueue.length > 0) { + this.showNextNotification(); + } else { + this.notificationStatus = 'ready'; + } + } + + onPauseGlobalNotifications() { + this.notificationsPaused = true; + } + + onResumeGlobalNotifications() { + this.notificationsPaused = false; + + if (this.notificationStatus === 'ready' && this.notificationQueue.length > 0) { + this.showNextNotification(); + } } } diff --git a/js/stores/piece_store.js b/js/stores/piece_store.js index ccef50b1..3b04736b 100644 --- a/js/stores/piece_store.js +++ b/js/stores/piece_store.js @@ -7,11 +7,13 @@ import PieceActions from '../actions/piece_actions'; class PieceStore { constructor() { this.piece = {}; + this.pieceError = null; this.bindActions(PieceActions); } onUpdatePiece(piece) { this.piece = piece; + this.pieceError = null; } onUpdateProperty({key, value}) { @@ -21,6 +23,10 @@ class PieceStore { throw new Error('There is no piece defined in PieceStore or the piece object does not have the property you\'re looking for.'); } } + + onPieceFailed(err) { + this.pieceError = err; + } } export default alt.createStore(PieceStore, 'PieceStore'); diff --git a/js/stores/webhook_store.js b/js/stores/webhook_store.js new file mode 100644 index 00000000..7dfcc61d --- /dev/null +++ b/js/stores/webhook_store.js @@ -0,0 +1,88 @@ +'use strict'; + +import { alt } from '../alt'; +import WebhookActions from '../actions/webhook_actions'; + +import WebhookSource from '../sources/webhook_source'; + +class WebhookStore { + constructor() { + this.webhooks = []; + this.webhookEvents = []; + this.webhookMeta = { + invalidateCache: false, + err: null, + idToDelete: null + }; + this.webhookEventsMeta = { + invalidateCache: false, + err: null + }; + + this.bindActions(WebhookActions); + this.registerAsync(WebhookSource); + } + + onFetchWebhooks(invalidateCache) { + this.webhookMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhooks(); + } + + onSuccessFetchWebhooks({ webhooks }) { + this.webhookMeta.invalidateCache = false; + this.webhookMeta.err = null; + this.webhooks = webhooks; + + this.webhookEventsMeta.invalidateCache = true; + this.getInstance().lookupWebhookEvents(); + } + + onFetchWebhookEvents(invalidateCache) { + this.webhookEventsMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhookEvents(); + } + + onSuccessFetchWebhookEvents({ events }) { + this.webhookEventsMeta.invalidateCache = false; + this.webhookEventsMeta.err = null; + + // remove all events that have already been used. + const usedEvents = this.webhooks + .reduce((tempUsedEvents, webhook) => { + tempUsedEvents.push(webhook.event.split('.')[0]); + return tempUsedEvents; + }, []); + + this.webhookEvents = events.filter((event) => { + return usedEvents.indexOf(event) === -1; + }); + } + + onRemoveWebhook(id) { + this.webhookMeta.invalidateCache = true; + this.webhookMeta.idToDelete = id; + + if(!this.getInstance().isLoading()) { + this.getInstance().performRemoveWebhook(); + } + } + + onSuccessRemoveWebhook() { + this.webhookMeta.idToDelete = null; + if(!this.getInstance().isLoading()) { + this.getInstance().lookupWebhooks(); + } + } + + onErrorWebhooks(err) { + console.logGlobal(err); + this.webhookMeta.err = err; + } + + onErrorWebhookEvents(err) { + console.logGlobal(err); + this.webhookEventsMeta.err = err; + } +} + +export default alt.createStore(WebhookStore, 'WebhookStore'); diff --git a/js/utils/acl_utils.js b/js/utils/acl_utils.js index fc3987c1..dd39a380 100644 --- a/js/utils/acl_utils.js +++ b/js/utils/acl_utils.js @@ -4,7 +4,7 @@ import { sanitize, intersectLists } from './general_utils'; export function getAvailableAcls(editions, filterFn) { let availableAcls = []; - if (!editions || editions.constructor !== Array){ + if (!editions || editions.constructor !== Array) { return []; } // if you copy a javascript array of objects using slice, then @@ -33,23 +33,23 @@ export function getAvailableAcls(editions, filterFn) { }); // If no edition has been selected, availableActions is empty - // If only one edition has been selected, their actions are available - // If more than one editions have been selected, their acl properties are intersected - if(editionsCopy.length >= 1) { + // If only one edition has been selected, its actions are available + // If more than one editions have been selected, intersect all their acl properties + if (editionsCopy.length >= 1) { availableAcls = editionsCopy[0].acl; - } - if(editionsCopy.length >= 2) { - for(let i = 1; i < editionsCopy.length; i++) { - availableAcls = intersectLists(availableAcls, editionsCopy[i].acl); + + if (editionsCopy.length >= 2) { + for (let i = 1; i < editionsCopy.length; i++) { + availableAcls = intersectLists(availableAcls, editionsCopy[i].acl); + } } } // convert acls back to key-value object let availableAclsObj = {}; - for(let i = 0; i < availableAcls.length; i++) { + for (let i = 0; i < availableAcls.length; i++) { availableAclsObj[availableAcls[i]] = true; } - return availableAclsObj; -} \ No newline at end of file +} diff --git a/js/utils/error_utils.js b/js/utils/error_utils.js index e80819dc..a10f1268 100644 --- a/js/utils/error_utils.js +++ b/js/utils/error_utils.js @@ -12,7 +12,8 @@ import AppConstants from '../constants/application_constants'; * @param {boolean} ignoreSentry Defines whether or not the error should be submitted to Sentry * @param {string} comment Will also be submitted to Sentry, but will not be logged */ -function logGlobal(error, ignoreSentry, comment) { +function logGlobal(error, ignoreSentry = AppConstants.errorMessagesToIgnore.indexOf(error.message) > -1, + comment) { console.error(error); if(!ignoreSentry) { diff --git a/js/utils/file_utils.js b/js/utils/file_utils.js index f5d6ba99..9c1423b3 100644 --- a/js/utils/file_utils.js +++ b/js/utils/file_utils.js @@ -89,11 +89,16 @@ export function computeHashOfFile(file) { /** * Extracts a file extension from a string, by splitting by dot and taking * the last substring + * + * If a file without an extension is submitted (file), then + * this method just returns an empty string. * @param {string} s file's name + extension * @return {string} file extension * * Via: http://stackoverflow.com/a/190878/1263876 */ export function extractFileExtensionFromString(s) { - return s.split('.').pop(); + const explodedFileName = s.split('.'); + return explodedFileName.length > 1 ? explodedFileName.pop() + : ''; } \ No newline at end of file diff --git a/js/utils/form_utils.js b/js/utils/form_utils.js index c15eb067..8d12a8c1 100644 --- a/js/utils/form_utils.js +++ b/js/utils/form_utils.js @@ -2,6 +2,8 @@ import { getLangText } from './lang_utils'; +import AppConstants from '../constants/application_constants'; + /** * Get the data ids of the given piece or editions. * @param {boolean} isPiece Is the given entities parameter a piece? (False: array of editions) @@ -70,6 +72,10 @@ export function getAclFormMessage(options) { throw new Error('Your specified aclName did not match a an acl class.'); } + if (options.additionalMessage) { + message += '\n\n' + options.additionalMessage; + } + if (options.senderName) { message += '\n\n'; message += getLangText('Truly yours,'); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index b15a0525..a3336d80 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -1,5 +1,11 @@ 'use strict'; +/** + * Checks shallow equality + * Re-export of shallow from shallow-equals + */ +export { default as isShallowEqual } from 'shallow-equals'; + /** * Takes an object and returns a shallow copy without any keys * that fail the passed in filter function. @@ -109,7 +115,7 @@ function _doesObjectListHaveDuplicates(l) { export function mergeOptions(...l) { // If the objects submitted in the list have duplicates,in their key names, // abort the merge and tell the function's user to check his objects. - if(_doesObjectListHaveDuplicates(l)) { + if (_doesObjectListHaveDuplicates(l)) { throw new Error('The objects you submitted for merging have duplicates. Merge aborted.'); } diff --git a/js/utils/inject_utils.js b/js/utils/inject_utils.js index 174ac8b6..e9430a5e 100644 --- a/js/utils/inject_utils.js +++ b/js/utils/inject_utils.js @@ -12,16 +12,16 @@ let mapTag = { css: 'link' }; +let tags = {}; + function injectTag(tag, src) { - return Q.Promise((resolve, reject) => { - if (isPresent(tag, src)) { - resolve(); - } else { + if(!tags[src]) { + tags[src] = Q.Promise((resolve, reject) => { let attr = mapAttr[tag]; let element = document.createElement(tag); if (tag === 'script') { - element.onload = () => resolve(); - element.onerror = () => reject(); + element.onload = resolve; + element.onerror = reject; } else { resolve(); } @@ -30,14 +30,10 @@ function injectTag(tag, src) { if (tag === 'link') { element.rel = 'stylesheet'; } - } - }); -} + }); + } -function isPresent(tag, src) { - let attr = mapAttr[tag]; - let query = `head > ${tag}[${attr}="${src}"]`; - return document.querySelector(query); + return tags[src]; } function injectStylesheet(src) { @@ -65,7 +61,6 @@ export const InjectInHeadUtils = { * you don't want to embed everything inside the build file. */ - isPresent, injectStylesheet, injectScript, inject diff --git a/js/utils/regex_utils.js b/js/utils/regex_utils.js new file mode 100644 index 00000000..af948b2b --- /dev/null +++ b/js/utils/regex_utils.js @@ -0,0 +1,7 @@ +'use strict' + +export function isEmail(string) { + // This is a bit of a weak test for an email, but you really can't win them all + // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address + return !!string && string.match(/.*@.*\..*/); +} diff --git a/js/utils/requests.js b/js/utils/requests.js index a7300634..9195661d 100644 --- a/js/utils/requests.js +++ b/js/utils/requests.js @@ -30,6 +30,15 @@ class Requests { reject(error); } else if(body && body.detail) { reject(new Error(body.detail)); + } else if(!body.success) { + let error = new Error('Client Request Error'); + error.json = { + status: response.status, + statusText: response.statusText, + type: response.type, + url: response.url + }; + reject(error); } else { resolve(body); } @@ -100,8 +109,7 @@ class Requests { return newUrl; } - request(verb, url, options) { - options = options || {}; + request(verb, url, options = {}) { let merged = Object.assign({}, this.httpOptions, options); let csrftoken = getCookie(AppConstants.csrftoken); if (csrftoken) { @@ -129,13 +137,10 @@ class Requests { } _putOrPost(url, paramsAndBody, method) { - let paramsCopy = Object.assign({}, paramsAndBody); let params = omitFromObject(paramsAndBody, ['body']); let newUrl = this.prepareUrl(url, params); - let body = null; - if (paramsCopy && paramsCopy.body) { - body = JSON.stringify(paramsCopy.body); - } + let body = paramsAndBody && paramsAndBody.body ? JSON.stringify(paramsAndBody.body) + : null; return this.request(method, newUrl, { body }); } diff --git a/package.json b/package.json index 63c6d1e0..be5c1202 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "gulp-uglify": "^1.2.0", "gulp-util": "^3.0.4", "harmonize": "^1.4.2", - "history": "^1.11.1", + "history": "^1.13.1", "invariant": "^2.1.1", "isomorphic-fetch": "^2.0.2", "jest-cli": "^0.4.0", @@ -80,11 +80,12 @@ "react": "0.13.2", "react-bootstrap": "0.25.1", "react-datepicker": "^0.12.0", - "react-router": "^1.0.0-rc3", + "react-router": "1.0.0", "react-router-bootstrap": "^0.19.0", "react-star-rating": "~1.3.2", "react-textarea-autosize": "^2.5.2", "reactify": "^1.1.0", + "shallow-equals": "0.0.0", "shmui": "^0.1.0", "spark-md5": "~1.0.0", "uglifyjs": "^2.4.10", diff --git a/sass/ascribe-fonts/ascribe-fonts.scss b/sass/ascribe-fonts/ascribe-fonts.scss index 11b42851..6f95a616 100644 --- a/sass/ascribe-fonts/ascribe-fonts.scss +++ b/sass/ascribe-fonts/ascribe-fonts.scss @@ -1,24 +1,10 @@ -[class^="icon-ascribe-"], [class*=" icon-ascribe-"] { - font-family: 'ascribe-logo'; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - /* These glyphs are generated using: https://icomoon.io Even though it seems radically complicated, check out the site, its fairly straight forward. If someone wants you to add a new glyph go to the site, - drop in the regular ascribe-logo font and select all icons. + drop in the regular ascribe-font font and select all icons. Then also add the new glyph, name and address it correctly and download the font again. @@ -26,18 +12,19 @@ */ @font-face { - font-family: 'ascribe-logo'; - src:url('#{$BASE_URL}static/fonts/ascribe-logo.eot?q6qoae'); - src:url('#{$BASE_URL}static/fonts/ascribe-logo.eot?q6qoae#iefix') format('embedded-opentype'), - url('#{$BASE_URL}static/fonts/ascribe-logo.ttf?q6qoae') format('truetype'), - url('#{$BASE_URL}static/fonts/ascribe-logo.woff?q6qoae') format('woff'), - url('#{$BASE_URL}static/fonts/ascribe-logo.svg?q6qoae#ascribe-logo') format('svg'); + font-family: 'ascribe-font'; + src:url('#{$BASE_URL}static/fonts/ascribe-font.eot?q6qoae'); + src:url('#{$BASE_URL}static/fonts/ascribe-font.eot?q6qoae#iefix') format('embedded-opentype'), + url('#{$BASE_URL}static/fonts/ascribe-font.ttf?q6qoae') format('truetype'), + url('#{$BASE_URL}static/fonts/ascribe-font.woff?q6qoae') format('woff'), + url('#{$BASE_URL}static/fonts/ascribe-font.svg?q6qoae#ascribe-font') format('svg'); font-weight: normal; font-style: normal; } [class^="icon-ascribe-"], [class*=" icon-ascribe-"] { - font-family: 'ascribe-logo'; + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'ascribe-font' !important; speak: none; font-style: normal; font-weight: normal; @@ -80,35 +67,15 @@ .icon-ascribe-logo:before { content: "\e808"; } - .icon-ascribe-ok:before { content: "\e809"; font-size: .7em; } +.icon-ascribe-thin-cross:before { + content: "\e810"; +} .btn-glyph-ascribe { font-size: 18px; padding: 4px 12px 0 10px } - -.ascribe-logo-circle { - border: 6px solid #F6F6F6; - border-radius: 10em; - position: relative; - top: 10%; - left: 10%; - - display: block; - width: 80%; - height: 80%; - - > span { - color: #F6F6F6; - position: absolute; - top: -.29em; - left: .16em; - - font-size: 5em; - font-weight: normal; - } -} \ No newline at end of file diff --git a/sass/ascribe_accordion_list.scss b/sass/ascribe_accordion_list.scss index c0b81096..791743fc 100644 --- a/sass/ascribe_accordion_list.scss +++ b/sass/ascribe_accordion_list.scss @@ -60,6 +60,34 @@ $ascribe-accordion-list-item-height: 100px; background-size: cover; } + .ascribe-logo-circle { + border: 6px solid #F6F6F6; + border-radius: 10em; + position: relative; + top: 10%; + left: 10%; + + display: block; + width: 80%; + height: 80%; + + > span { + color: #F6F6F6; + position: absolute; + top: -.29em; + left: .16em; + + font-size: 5em; + font-weight: normal; + } + } + + .ascribe-thumbnail-placeholder { + color: #F6F6F6; + font-size: 5em; + font-weight: normal; + } + //&::before { // content: ' '; // display: inline-block; @@ -211,10 +239,6 @@ $ascribe-accordion-list-item-height: 100px; -ms-user-select: none; -webkit-user-select: none; - &:hover { - color: $ascribe-dark-blue; - } - .glyphicon { font-size: .8em; top: 1px !important; diff --git a/sass/ascribe_acl_information.scss b/sass/ascribe_acl_information.scss index 063c8ae6..5a4708f0 100644 --- a/sass/ascribe_acl_information.scss +++ b/sass/ascribe_acl_information.scss @@ -22,4 +22,4 @@ .example { color: #616161; } -} \ No newline at end of file +} diff --git a/sass/ascribe_custom_style.scss b/sass/ascribe_custom_style.scss index 98cce937..96b97783 100644 --- a/sass/ascribe_custom_style.scss +++ b/sass/ascribe_custom_style.scss @@ -68,10 +68,15 @@ hr { .dropdown-menu { background-color: $ascribe--nav-bg-color; } + .navbar-nav > li > .dropdown-menu { + padding: 0; + } .dropdown-menu > li > a { color: $ascribe--nav-fg-prim-color; font-weight: $ascribe--font-weight-light; + padding-bottom: 9px; + padding-top: 9px; } .dropdown-menu > li > a:hover, @@ -79,6 +84,10 @@ hr { background-color: rgba($ascribe--button-default-color, .05); } + .dropdown-menu > .divider { + margin: 0; + } + .notification-menu { .dropdown-menu { background-color: white; @@ -257,6 +266,24 @@ hr { font-weight: $ascribe--font-weight-light; } +.btn-default { + background-color: $ascribe--button-default-color; + border-color: $ascribe--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: lighten($ascribe--button-default-color, 20%); + border-color: lighten($ascribe--button-default-color, 20%); + } +} + // disabled buttons .btn-default.disabled, .btn-default.disabled:hover, @@ -280,9 +307,10 @@ fieldset[disabled] .btn-default.active { border-color: darken($ascribe--button-default-color, 20%); } -.btn-default { - background-color: $ascribe--button-default-color; - border-color: $ascribe--button-default-color; +.btn-secondary { + background-color: $ascribe--button-secondary-bg-color; + border-color: $ascribe--button-secondary-fg-color; + color: $ascribe--button-secondary-fg-color; &:hover, &:active, @@ -293,8 +321,9 @@ fieldset[disabled] .btn-default.active { &.active:hover, &.active:focus, &.active.focus { - background-color: lighten($ascribe--button-default-color, 20%); - border-color: lighten($ascribe--button-default-color, 20%); + background-color: $ascribe--button-secondary-fg-color; + border-color: $ascribe--button-secondary-bg-color; + color: $ascribe--button-secondary-bg-color; } } @@ -322,26 +351,6 @@ fieldset[disabled] .btn-secondary.active { color: darken($ascribe--button-secondary-fg-color, 20%); } -.btn-secondary { - background-color: $ascribe--button-secondary-bg-color; - border-color: $ascribe--button-secondary-fg-color; - color: $ascribe--button-secondary-fg-color; - - &:hover, - &:active, - &:focus, - &:active:hover, - &:active:focus, - &:active.focus, - &.active:hover, - &.active:focus, - &.active.focus { - background-color: $ascribe--button-secondary-fg-color; - border-color: $ascribe--button-secondary-bg-color; - color: $ascribe--button-secondary-bg-color; - } -} - .btn-tertiary { background-color: transparent; border-color: transparent; @@ -580,11 +589,6 @@ fieldset[disabled] .btn-secondary.active { background-color: lighten($ascribe--button-default-color, 20%); } -// uploader -.ascribe-progress-bar > .progress-bar { - background-color: lighten($ascribe--button-default-color, 20%); -} - .action-file { text-shadow: -1px 0 black, 0 1px black, diff --git a/sass/ascribe_notification_list.scss b/sass/ascribe_notification_list.scss index a09f7049..b5f46a4c 100644 --- a/sass/ascribe_notification_list.scss +++ b/sass/ascribe_notification_list.scss @@ -2,8 +2,9 @@ $break-small: 764px; $break-medium: 991px; $break-medium: 1200px; -.notification-header,.notification-wrapper { - width: 350px; +.notification-header, .notification-wrapper { + min-width: 350px; + width: 100%; } .notification-header { @@ -81,4 +82,4 @@ $break-medium: 1200px; border: 1px solid #cccccc; background-color: white; margin-top: 1px; -} \ No newline at end of file +} diff --git a/sass/ascribe_notification_page.scss b/sass/ascribe_notification_page.scss index 955609d2..7bb37446 100644 --- a/sass/ascribe_notification_page.scss +++ b/sass/ascribe_notification_page.scss @@ -31,16 +31,11 @@ margin-top: .5em; margin-bottom: 1em; - .loan-form { - margin-top: .5em; + &.embed-form { height: 45vh; } } - .loan-form { - height: 40vh; - } - .notification-contract-pdf-download { text-align: left; margin-left: 1em; @@ -69,4 +64,8 @@ padding-left: 0; width: 100%; } +} + +.ascribe-property.contract-appendix-form { + padding-left: 0; } \ No newline at end of file diff --git a/sass/ascribe_panel.scss b/sass/ascribe_panel.scss index 0f675605..f4b70a80 100644 --- a/sass/ascribe_panel.scss +++ b/sass/ascribe_panel.scss @@ -31,7 +31,7 @@ vertical-align: middle; &:first-child { - word-break: break-all; + word-break: break-word; font-size: .9em; } } diff --git a/sass/ascribe_piece_list_toolbar.scss b/sass/ascribe_piece_list_toolbar.scss index f033ee81..06cbd1a7 100644 --- a/sass/ascribe_piece_list_toolbar.scss +++ b/sass/ascribe_piece_list_toolbar.scss @@ -81,4 +81,8 @@ top: 2px; } } + + .dropdown-menu { + min-width: 170px; + } } diff --git a/sass/ascribe_spinner.scss b/sass/ascribe_spinner.scss index 7f02a383..133cc6b8 100644 --- a/sass/ascribe_spinner.scss +++ b/sass/ascribe_spinner.scss @@ -52,6 +52,13 @@ $ascribe--spinner-size-sm: 15px; } } +.spinner-wrapper-white { + color: $ascribe-white; + .spinner-circle { + border-color: $ascribe-white; + } +} + .spinner-wrapper-lg { width: $ascribe--spinner-size-lg; height: $ascribe--spinner-size-lg; @@ -107,17 +114,20 @@ $ascribe--spinner-size-sm: 15px; } .spinner-wrapper-lg .spinner-inner { font-size: $ascribe--spinner-size-lg; - top: -64px; + line-height: $ascribe--spinner-size-lg; + top: -50px; } .spinner-wrapper-md .spinner-inner { font-size: $ascribe--spinner-size-md; - top: -38px; + line-height: $ascribe--spinner-size-md; + top: -30px; } .spinner-wrapper-sm .spinner-inner { font-size: $ascribe--spinner-size-sm; - top: -19px; + line-height: $ascribe--spinner-size-sm; + top: -15px; } @-webkit-keyframes spin { @@ -146,4 +156,4 @@ $ascribe--spinner-size-sm: 15px; 40% { color: $ascribe-blue; } 60% { color: $ascribe-light-blue; } 80% { color: $ascribe-pink; } -} \ No newline at end of file +} diff --git a/sass/ascribe_uploader.scss b/sass/ascribe_uploader.scss index fa353ecd..ea02eb07 100644 --- a/sass/ascribe_uploader.scss +++ b/sass/ascribe_uploader.scss @@ -120,7 +120,7 @@ &.icon-ascribe-ok, &.icon-ascribe-ok:hover { cursor: default; - color: lighten($ascribe--button-default-color, 20%); + color: $ascribe-dark-blue; font-size: 4.2em; top: .2em; } @@ -328,9 +328,12 @@ } span { - font-size: 1.25em; + font-size: 1.2em; color: white; - text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; + text-shadow: -1px 0 $ascribe--button-default-color, + 0 1px $ascribe--button-default-color, + 1px 0 $ascribe--button-default-color, + 0 -1px $ascribe--button-default-color; } } diff --git a/sass/main.scss b/sass/main.scss index 8a732e96..5cc91e9a 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -350,7 +350,7 @@ hr { > span { font-size: 1.1em; - font-weight: 600; + font-weight: normal; color: #616161; padding-left: .3em; diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss new file mode 100644 index 00000000..a5026272 --- /dev/null +++ b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss @@ -0,0 +1,377 @@ +/** Sass cannot use a number as the first character of a variable, so we'll have to settle with vivi23 **/ +$vivi23--fg-color: black; +$vivi23--bg-color: white; +$vivi23--nav-fg-prim-color: $vivi23--fg-color; +$vivi23--nav-fg-sec-color: #3a3a3a; +$vivi23--nav-bg-color: $vivi23--bg-color; +$vivi23--nav-highlight-color: #f8f8f8; +$vivi23--button-default-color: $vivi23--fg-color; +$vivi23--highlight-color: #de2600; + + +.client--23vivi { + /** Landing page **/ + .route--landing { + display: table; + + > .container { + display: table-cell; + padding-bottom: 100px; + vertical-align: middle; + } + + .vivi23-landing { + font-weight: normal; + text-align: center; + } + + .vivi23-landing--header { + background-color: $vivi23--fg-color; + border: 1px solid $vivi23--fg-color; + color: $vivi23--bg-color; + padding: 2em; + + .vivi23-landing--header-logo { + margin-top: 1em; + margin-bottom: 2em; + height: 75px; + } + } + + .vivi23-landing--content { + border: 1px solid darken($vivi23--bg-color, 20%); + border-top: none; + padding: 2em; + } + } + + /** Navbar **/ + .navbar-default { + background-color: $vivi23--nav-fg-prim-color; + + .navbar-brand .icon-ascribe-logo { + color: $vivi23--bg-color; + &:hover { + color: darken($vivi23--bg-color, 20%); + } + } + + } + + .navbar-nav > li > a, + .navbar-nav > li > a:focus, + .navbar-nav > li > .active a, + .navbar-nav > li > .active a:focus { + color: $vivi23--nav-bg-color; + } + + .navbar-nav > li > a:hover { + color: darken($vivi23--nav-bg-color, 20%); + } + + .navbar-nav > .active a, + .navbar-nav > .active a:hover, + .navbar-nav > .active a:focus { + background-color: $vivi23--nav-fg-prim-color; + border-bottom-color: $vivi23--nav-bg-color; + color: $vivi23--nav-bg-color; + } + + .navbar-nav > .open > a, + .dropdown-menu > .active > a, + .dropdown-menu > li > a { + color: $vivi23--nav-fg-prim-color; + background-color: $vivi23--nav-bg-color; + } + + .navbar-nav > .open > a:hover, + .navbar-nav > .open > a:focus, + .dropdown-menu > .active > a:hover, + .dropdown-menu > .active > a:focus, + .dropdown-menu > li > a:hover, + .dropdown-menu > li > a:focus { + color: lighten($vivi23--nav-fg-prim-color, 20%); + background-color: $vivi23--nav-highlight-color; + } + + .navbar-collapse.collapsing, + .navbar-collapse.collapse.in { + background-color: $vivi23--nav-bg-color; + + .navbar-nav > .open > a, + .navbar-nav > .active > a { + background-color: $vivi23--nav-highlight-color; + } + } + + .navbar-collapse.collapsing li a, + .navbar-collapse.collapse.in li a { + color: $vivi23--nav-fg-prim-color; + } + .navbar-collapse.collapse.in li a:not(.ascribe-powered-by):hover { + color: lighten($vivi23--nav-fg-prim-color, 20%); + background-color: $vivi23--nav-highlight-color; + } + + .navbar-toggle { + border-color: $vivi23--highlight-color; + + .icon-bar { + background-color: $vivi23--highlight-color; + } + } + + .navbar-toggle:hover, + .navbar-toggle:focus { + border-color: lighten($vivi23--highlight-color, 10%); + background-color: $vivi23--nav-fg-prim-color; + + .icon-bar { + background-color: lighten($vivi23--highlight-color, 10%); + } + } + + .notification-menu { + .dropdown-menu { + background-color: $vivi23--nav-bg-color; + } + + .notification-header { + background-color: $vivi23--nav-fg-sec-color; + border-top-width: 0; + color: $vivi23--nav-bg-color; + } + + .notification-action { + color: $vivi23--highlight-color; + } + } + + /** Buttons **/ + // reset disabled button styling for btn-default + .btn-default.disabled, + .btn-default.disabled:hover, + .btn-default.disabled:focus, + .btn-default.disabled.focus, + .btn-default.disabled:active, + .btn-default.disabled.active, + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus, + .btn-default[disabled].focus, + .btn-default[disabled]:active, + .btn-default[disabled].active, + fieldset[disabled] .btn-default, + fieldset[disabled] .btn-default:hover, + fieldset[disabled] .btn-default:focus, + fieldset[disabled] .btn-default.focus, + fieldset[disabled] .btn-default:active, + fieldset[disabled] .btn-default.active { + background-color: lighten($vivi23--button-default-color, 30%); + border-color: lighten($vivi23--button-default-color, 30%); + } + + .btn-default { + background-color: $vivi23--button-default-color; + border-color: $vivi23--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: lighten($vivi23--button-default-color, 30%); + border-color: lighten($vivi23--button-default-color, 30%); + } + } + + // disabled buttons + .btn-secondary.disabled, + .btn-secondary.disabled:hover, + .btn-secondary.disabled:focus, + .btn-secondary.disabled.focus, + .btn-secondary.disabled:active, + .btn-secondary.disabled.active, + .btn-secondary[disabled], + .btn-secondary[disabled]:hover, + .btn-secondary[disabled]:focus, + .btn-secondary[disabled].focus, + .btn-secondary[disabled]:active, + .btn-secondary[disabled].active, + fieldset[disabled] .btn-secondary, + fieldset[disabled] .btn-secondary:hover, + fieldset[disabled] .btn-secondary:focus, + fieldset[disabled] .btn-secondary.focus, + fieldset[disabled] .btn-secondary:active, + fieldset[disabled] .btn-secondary.active { + background-color: darken($vivi23--bg-color, 20%); + border-color: $vivi23--button-default-color; + } + + .btn-secondary { + border-color: $vivi23--button-default-color; + color: $vivi23--button-default-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $vivi23--button-default-color; + border-color: $vivi23--button-default-color; + color: $vivi23--bg-color; + } + } + + .btn-tertiary { + &:hover, + &:active, + &ctive:hover, + &.active:hover{ + background-color: $vivi23--highlight-color; + border-color: $vivi23--highlight-color; + color: $vivi23--highlight-color; + } + } + + /** Other components **/ + .ascribe-piece-list-toolbar .btn-ascribe-add { + display: none; + } + + .ascribe-footer { + display: none; + } + + .ascribe-accordion-list-table-toggle:hover { + color: $vivi23--fg-color; + } + + .request-action-badge { + color: $vivi23--fg-color; + } + + .acl-information-dropdown-list .title { + color: $vivi23--fg-color; + } + + // filter widget + .ascribe-piece-list-toolbar-filter-widget button { + background-color: transparent !important; + border-color: transparent !important; + color: $vivi23--button-default-color !important; + + &:hover, + &:active { + background-color: $vivi23--button-default-color !important; + border-color: $vivi23--button-default-color !important; + color: $vivi23--bg-color !important; + } + } + + .icon-ascribe-search { + color: $vivi23--fg-color; + } + + // forms + .ascribe-property-wrapper:hover { + border-left-color: rgba($vivi23--fg-color, 0.5); + } + + .ascribe-property textarea, + .ascribe-property input, + .search-bar > .form-group > .input-group input { + &::-webkit-input-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &::-moz-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &:-ms-input-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + &:-moz-placeholder { + color: rgba($vivi23--fg-color, 0.5); + } + } + + .ascribe-property { + > div, + > input, + > pre, + > select, + > span:not(.glyphicon), + > p, + > p > span, + > textarea { + color: $vivi23--fg-color; + } + } + + // global notification + .ascribe-global-notification-success { + background-color: lighten($vivi23--fg-color, 20%); + } + + // uploader progress + .ascribe-progress-bar > .progress-bar { + background-color: lighten($vivi23--fg-color, 20%); + } + + .ascribe-progress-bar span { + text-shadow: -1px 0 lighten($vivi23--fg-color, 20%), + 0 1px lighten($vivi23--fg-color, 20%), + 1px 0 lighten($vivi23--fg-color, 20%), + 0 -1px lighten($vivi23--fg-color, 20%); + } + + .action-file.icon-ascribe-ok, + .action-file.icon-ascribe-ok:hover { + color: lighten($vivi23--fg-color, 20%); + } + + // spinner + .spinner-circle { + border-color: $vivi23--fg-color; + } + .spinner-inner { + display: none; + } + .btn-secondary .spinner-circle { + border-color: $vivi23--bg-color; + } + + // video player + .ascribe-media-player .vjs-default-skin { + .vjs-play-progress, + .vjs-volume-level { + background-color: $vivi23--highlight-color; + } + } + + // pager + .pager li > a, + .pager li > span { + background-color: $vivi23--fg-color; + border-color: $vivi23--fg-color; + } + .pager .disabled > a, + .pager .disabled > span { + background-color: $vivi23--fg-color !important; + border-color: $vivi23--fg-color; + } + + // intercom + #intercom-container .intercom-launcher-button { + background-color: $vivi23--button-default-color !important; + border-color: $vivi23--button-default-color !important; + } +} diff --git a/sass/whitelabel/wallet/cc/cc_custom_style.scss b/sass/whitelabel/wallet/cc/cc_custom_style.scss index 44cb0dd1..774f5b27 100644 --- a/sass/whitelabel/wallet/cc/cc_custom_style.scss +++ b/sass/whitelabel/wallet/cc/cc_custom_style.scss @@ -207,4 +207,20 @@ $cc--button-color: $cc--nav-fg-prim-color; .client--cc .acl-information-dropdown-list .title { color: $cc--button-color; +} + +.client--cc .action-file.icon-ascribe-ok, +.client--cc .action-file.icon-ascribe-ok:hover { + color: $cc--button-color; +} + +.client--cc .ascribe-progress-bar span { + text-shadow: -1px 0 $cc--button-color, + 0 1px $cc--button-color, + 1px 0 $cc--button-color, + 0 -1px $cc--button-color; +} + +.client--cc .upload-button-wrapper > span { + color: $cc--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss index eaf45621..6c4223ac 100644 --- a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss +++ b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss @@ -59,40 +59,14 @@ $cyland--button-color: $cyland--nav-fg-prim-color; display: none; } - -.client--cyland .icon-ascribe-search{ +.client--cyland .icon-ascribe-search { color: $cyland--button-color; } -.client--cyland .ascribe-piece-list-toolbar .btn-ascribe-add{ +.client--cyland .ascribe-piece-list-toolbar .btn-ascribe-add { display: none; } -// disabled buttons -.client--cyland { - .btn-default.disabled, - .btn-default.disabled:hover, - .btn-default.disabled:focus, - .btn-default.disabled.focus, - .btn-default.disabled:active, - .btn-default.disabled.active, - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus, - .btn-default[disabled].focus, - .btn-default[disabled]:active, - .btn-default[disabled].active, - fieldset[disabled] .btn-default, - fieldset[disabled] .btn-default:hover, - fieldset[disabled] .btn-default:focus, - fieldset[disabled] .btn-default.focus, - fieldset[disabled] .btn-default:active, - fieldset[disabled] .btn-default.active { - background-color: darken($cyland--button-color, 20%); - border-color: darken($cyland--button-color, 20%); - } -} - // buttons! // thought of the day: // "every great atrocity is the result of people just following orders" @@ -129,6 +103,26 @@ $cyland--button-color: $cyland--nav-fg-prim-color; } } + .btn-secondary { + background-color: white; + border-color: $cyland--button-color; + color: $cyland--button-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $cyland--button-color; + border-color: $cyland--button-color; + color: white; + } + } + .open > .btn-default.dropdown-toggle:hover, .open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus, @@ -148,6 +142,48 @@ $cyland--button-color: $cyland--nav-fg-prim-color; } } +.client--ikonotv { + .btn-default.disabled, + .btn-default.disabled:hover, + .btn-default.disabled:focus, + .btn-default.disabled.focus, + .btn-default.disabled:active, + .btn-default.disabled.active, + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus, + .btn-default[disabled].focus, + .btn-default[disabled]:active, + .btn-default[disabled].active, + fieldset[disabled] .btn-default, + fieldset[disabled] .btn-default:hover, + fieldset[disabled] .btn-default:focus, + fieldset[disabled] .btn-default.focus, + fieldset[disabled] .btn-default:active, + fieldset[disabled] .btn-default.active { + background-color: darken($cyland--button-color, 20%); + border-color: darken($cyland--button-color, 20%); + } +} + +// landing page +.client--cyland { + .route--landing { + display: table; + + > .container { + display: table-cell; + padding-bottom: 100px; + vertical-align: middle; + } + + .cyland-landing { + font-weight: normal; + text-align: center; + } + } +} + // spinner! .client--cyland { .btn-spinner { @@ -182,4 +218,20 @@ $cyland--button-color: $cyland--nav-fg-prim-color; .client--cyland .acl-information-dropdown-list .title { color: $cyland--button-color; +} + +.client--cyland .action-file.icon-ascribe-ok, +.client--cyland .action-file.icon-ascribe-ok:hover { + color: $cyland--nav-fg-prim-color; +} + +.client--cyland .ascribe-progress-bar span { + text-shadow: -1px 0 $cyland--nav-fg-prim-color, + 0 1px $cyland--nav-fg-prim-color, + 1px 0 $cyland--nav-fg-prim-color, + 0 -1px $cyland--nav-fg-prim-color; +} + +.client--cyland .upload-button-wrapper > span { + color: $cyland--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss index 70a5cd18..8f330911 100644 --- a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss +++ b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss @@ -411,6 +411,26 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; } } + .btn-secondary { + background-color: white; + border-color: $ikono--button-color; + color: $ikono--button-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: $ikono--button-color; + border-color: $ikono--button-color; + color: white; + } + } + .open > .btn-default.dropdown-toggle:hover, .open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus, @@ -524,4 +544,20 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; .client--ikonotv .acl-information-dropdown-list .title { color: $ikono--button-color; +} + +.client--ikonotv .action-file.icon-ascribe-ok, +.client--ikonotv .action-file.icon-ascribe-ok:hover { + color: $ikono--button-color; +} + +.client--ikonotv .ascribe-progress-bar span { + text-shadow: -1px 0 $ikono--button-color, + 0 1px $ikono--button-color, + 1px 0 $ikono--button-color, + 0 -1px $ikono--button-color; +} + +.client--ikonotv .upload-button-wrapper > span { + color: $ikono--button-color; } \ No newline at end of file diff --git a/sass/whitelabel/wallet/index.scss b/sass/whitelabel/wallet/index.scss index 024fb3cc..01c374d9 100644 --- a/sass/whitelabel/wallet/index.scss +++ b/sass/whitelabel/wallet/index.scss @@ -1,6 +1,7 @@ @import 'cc/cc_custom_style'; @import 'cyland/cyland_custom_style'; @import 'ikonotv/ikonotv_custom_style'; +@import '23vivi/23vivi_custom_style'; .ascribe-wallet-app { border-radius: 0;