From 246eb172493dc67e71d429f9ad64b0d3e9b1f108 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Tue, 28 Apr 2020 19:46:55 +0500 Subject: [PATCH] Add dropzone for attachment, paste attachment from clipboard --- client/package-lock.json | 23 + client/package.json | 1 + client/public/favicon.ico | Bin 16958 -> 105538 bytes client/public/logo192.png | Bin 3407 -> 2696 bytes client/public/logo512.png | Bin 5496 -> 5294 bytes .../components/CardModal/AddAttachment.jsx | 23 + .../AddAttachmentZone/AddAttachmentZone.jsx | 108 ++++ .../AddAttachmentZone.module.css | 13 + .../AddAttachmentZone/AddTextFileModal.jsx | 83 +++ .../AddTextFileModal.module.css | 3 + .../CardModal/AddAttachmentZone/index.js | 3 + .../CardModal/Attachments/Item.module.css | 3 + client/src/components/CardModal/CardModal.jsx | 489 +++++++++--------- client/src/components/Login/Login.jsx | 2 +- client/src/hooks/index.js | 3 +- client/src/hooks/use-modal.js | 15 + client/src/hooks/use-steps.js | 4 +- client/src/lib/custom-ui/index.css | 4 +- client/src/locales/en/app.js | 4 + client/src/locales/ru/app.js | 4 + server/api/controllers/attachments/create.js | 2 +- .../api/helpers/create-attachment-receiver.js | 10 +- server/package-lock.json | 31 ++ server/package.json | 1 + 24 files changed, 576 insertions(+), 253 deletions(-) mode change 100644 => 100755 client/public/logo192.png mode change 100644 => 100755 client/public/logo512.png create mode 100644 client/src/components/CardModal/AddAttachment.jsx create mode 100644 client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.jsx create mode 100644 client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.module.css create mode 100644 client/src/components/CardModal/AddAttachmentZone/AddTextFileModal.jsx create mode 100644 client/src/components/CardModal/AddAttachmentZone/AddTextFileModal.module.css create mode 100644 client/src/components/CardModal/AddAttachmentZone/index.js create mode 100644 client/src/hooks/use-modal.js diff --git a/client/package-lock.json b/client/package-lock.json index 5c956949..36b6938f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2198,6 +2198,11 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "attr-accept": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz", + "integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==" + }, "autoprefixer": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.5.tgz", @@ -5566,6 +5571,14 @@ "schema-utils": "^2.5.0" } }, + "file-selector": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", + "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -11207,6 +11220,16 @@ "scheduler": "^0.19.1" } }, + "react-dropzone": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.0.1.tgz", + "integrity": "sha512-x/6wqRHaR8jsrNiu/boVMIPYuoxb83Vyfv77hO7/3ZRn8Pr+KH5onsCsB8MLBa3zdJl410C5FXPUINbu16XIzw==", + "requires": { + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" + } + }, "react-error-overlay": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", diff --git a/client/package.json b/client/package.json index 23d9de50..c8bac967 100755 --- a/client/package.json +++ b/client/package.json @@ -60,6 +60,7 @@ "react-beautiful-dnd": "^13.0.0", "react-datepicker": "^2.14.1", "react-dom": "^16.13.1", + "react-dropzone": "^11.0.1", "react-i18next": "^11.3.4", "react-input-mask": "^2.0.4", "react-markdown": "^4.3.1", diff --git a/client/public/favicon.ico b/client/public/favicon.ico index 366beb9daf3e788abf3fe1b449ffcbfa7ae66c1d..a04182faea00c35b632496f6283a2c2e87b8499f 100644 GIT binary patch literal 105538 zcmeHQ30zIfA3wKHuarGP>g7E<*&gltUW!(wO+{JSRN57xtd&Zk1))euDAHzmLKH$? zyN6O~p`vK{|0bRO*L82*(&FXZ&*yvR%$zwhzxn=VKXYa`TE;ZM%F@5~= zBL!4rikzKX1&qbgp#d$1lyr z*uqii%Gj=?nU(2U*h7YSc}5vwn>TEXw`WqED9e$)o#}a#EK{ur#!4FbJTx-={CEYu zNtT;em)Xk&YF?RnE3%c@D5P4UVSDbdkIQdn9!`xBk{4VUi`~!6Y)K`_jFNgewNOXx z_=?Q}iwZQ-?o_eQ*;ElrvYrxOdF2VoFGp+HbG{rd>BRkCQY!=R&rN>jyIc$#sV;iO zRLedlI;~*BU-osrZdTi{SsrStS{mVwan(FYoSN8#zpkufsn}R3ptgCZ;n`v#E)4^9 z?UNqY{oJftv&YYRb6s_IU`(CT-c`eoAK$k3otuRFk(@ajH;%GjoX(^-!YH`9q~-ki z^aCKE2}1=TMJkDF^lyo7dbkoO)pNyeC|O69Rs(Yv#Q(9>ct&reO6ISto9s zEM2E0X1!S4sF>9#0$5Uf6c=xEZ(owhTT>0v&M863q#Jy8W%3u9rBf^IR$jH);5C2C z@9~KX#l~FZd&VWX{MGZjm!7+iXAe}F%CalM&P?HNJB$3VahAFj6coqV250zRRvxmtJnW%`r=Z$Az0MVbU@cw&l;(7;H7Z z;@Y@A&hF;o(@z&+vTwsQxE)n)i|(>&RXuW5w$)uj>p6#Fte`5%mZUs<<&Fm}y5ku~ zlT3HbcZ|DsvDMJ+THP)7W3Tw;z0cNE%o_G|3?@sG{}Rc__;!R5$(+Ma=H;Ewnn{fR zFTAJsKQ<#_M)w0|r$*dgpPk258FKM@gl(L+xd7{NS4G9$91h%z>T31vZ4VY7@qg7g z(n499my3(bYDJi^fZyH+e)6Wl*=5lT*52OZZ`|MvNGx(1#g;u^D${vxE{71`1Sb;D zw3@vRpX&KjuPXfJOxnEbX7nD%lTOdh?XK2XSFL)7BYi_;sQ*lX3NQWlu{CygjHT?$ znvxG4I>GW{gTi&Ue&gDr|a;T|#SzE{HCUntnxb^$Q3&E2QSUU4g^HF^i`X=+;a?6pi z*FG%X&na^MVXZ#zVzEghMV|*9nU^W3Kf8g)LvrRT>0v&?36mPw`08JcQM#5mZ1M8s z^V?FYc`P}#u^@wO51$@x@GFgtjpZ%WIkZY+wRffa2gbxdd3d+TES!`a%@;4t>zCsu zT3lT6FUylJt<4LMYMH(XO`MSUHQNqj}>bY zo3Ae6P^$E4`4VsQYW-5-vF$yazEOe~D;1gLgYlEi1 zRqNNng})a-*$q&e!?904Lda_k8{bli+YNK{{bgQDCwk7$oP0K5O9O*p%l+sVSE5BG zc**l|bG$uRvLNC2ZFYqX83~CF<14dXK2Na7A9JyQ&oDAT@Zl{n_o;eCjdi7KYU*pY zD{x<$GpE$_fp%v0Bz4RpKS4D&^hWgC5vr{9Q%l#r``wbsvfwG78E=lC;hWFwPq}W+ zHeorQk}~~O&6k=Nh2`rS7bY?Ws!0_btlIzg=Q`h06W&{g*eyDPN&hk3{dA!TyTNW3 zo#wwAF4hGL&F7 z_@lbGgoNmf{d^7a`^pO0mH!yaSCSL?PH>r9-qTi*h2Q7qcF>nyCm~d-lZp0g_F!}1sBYm$<$_BeWb;8 zr=7pqu_-~K5kkAB$WBOaR$6dyns9k~-H4Hsd>2J)jJ*5s?`NzF{xrs+N}=KDufBnFX%KPX&%W;=Z;yfVt( z&hD!2+r^qJtgO}zJDT_#d2*E=iagHAv2AU9|4Jn0&yP-r*J;i^J&keUlkyjlVP`Y` z%?RIZVOH=@Jl~Uf#_l2oSL@sCm~1ldLgHMiTXLQT88nquJ!U($(b?JAE~AN0KtLcW z*nVVb=%R^Id-dioy)Gfm5xXX_TJ@4yc6FWDio@&Jm}+7EIPIgN z#J%G;JdOQlO!JKrOqlxj-v^7d8F&`(j=?em)iN2ji>D>({xO#GZC%rSZ0fM|$2r?$ z-pV%km<4^#-Bx>|qM-0deC`QxY3Wmm%l)7>QxN#bVmf!&g};{1cT->7utcn*R_46d z$W6Kzs&i@Mh>s)Vay~DegEhWttqwA9{@sL+RAVJO(c|1XwKu5}njRr1kDnn;+WR1d zf6fANR*%4h(8kJhXLhT8-ct7FFMCf{-#U{0438t&UVGIS+vj6Pe|L1u93fnHgGp)e z$7U15B?;J^bCvf4ZgQ7Yoj6i{Zd`g*;WA6T!ppbc2fDbNi;9g_XZeJ^Q2liKbNb{l zP3x~-EDFxxaOa%Pdi-S4*6nH)DvmA&1~VLGJp7?MbhUNqN*#5F;T}SM*TVOIJS`<% zx_e6HF*~Nm53e#rosUhy6h(|473A7A%$Q?f@OZR?VBE>1kDCwZc$CE%j8HQ50yLPJ;e9xhC6c~3QJQ}2&;~py;S^bH;FfIN|)8o zHx(48I>vHN+hN2*2*%sdKyIOD@Y38As4*EXS7aU6c~VzXUH*@UYWQ;V9Z}VoAb5Wp z_*c?&^FKdin=W?~NIuV)UYRsK^6s2MlHKAn*v+{W%P~g<=)(=$7iz(`&rVKd_#)FV z!`qMwY5&TE4ZV`}W*T z3>D4V_Uyx-T)bDA#U(!t`>b%x+ig$vXw)mkc#I91@9u*hxO~w3EnBZ%Y_(YKdFz1E zWa-zu{QRE2zb7b2RzMFtx03zjnglUUGd`_Mxx(%5$8mcaT~^yD81GPZy@7X&v#m)> zU3;&mUe>ye@o0eBI&61kSb*7%bqRl{Y%oX-@EIZXF}2ZOO-J8ld_dCVbOmg$U0FFJ zLy5ikH2D`F-J*|97W}d?LylWN$X{qutvGCtK;>}@iExF%*;yWT@2SNN~e0IZ#x*a`cLd%Ocy)9*}{S?J9tq%}=Px$V-S3 z%5q<&_|ls*$0ke1%;+_6ZxoSE1+O0Ju>DiSBmRY^H`ZD{yqqc5%#@p5!@`TMXxMTz7qPNvwl$c=klut)g>WZG@+ZWj?gIH$%3N)jH>b zViWT|jS5;X$*yN>eXS}dw$)mEmLeF=ub&Tn`Ozcw|xe9-` zTXGI7a{W>`MNr6u(Z0<3TuKC^sz}Q1%mo~}hyS{L56g^S&lnsD)&J(TN=izl(6ibO zO=||1!^nbb(Jgv~0tLE{o5wmk>$z{?JQ*OEe-AtTl%dd8W&4hhkPxf4?G-@W8~@Om z8O9ft`7N%fUR*tr`@*L8@%vu+7uG)0Hf&(zm-rN-eoCvo2zxw#{``H?62~h}nH+q# zX8w8oW$nvbpQN@Ha2*+$n-;sC5xM}MsQ-2 z;>&6s6@2{FQ)h%uTlQJRsQ&dH<&19oC8%NuhLKOehgANs^`dXeS@dV_+j;b|;z0%5 z!hKMM37g7?i_BCF+Oflx?^fcU*H3uAdnA%Oe({D_7w8$u`b=v$S3S~T*A5cjtz!AD zx2mhF4=gM@@Ur4@`9Cmn66c*ZP4Keg_3BBM2c|B%S$>J(W4&GclvbA$51fIkF%dz` z%gt=#IoE1Bi2Ee=U#Aq+y}_S}|Al6q^ZOv2KCXHvO# zEh_3QW1&DuUfNRcqvuQt*%M_hHwD&I6SDJ71n32S$}!kldT#qa&p@(@^w==np{OW7rCykIUgUfRW*Fs zU)6TWYI8>L9FqUTVD9|+RnJ+E_VhFN3nOK8Vj0}~Pw!Z??#pX8oYvu~0zx>HTBe*yLkc#_wM#6yLiZ=}}x`p8eEBKhrt!mXv6C z_Iry*ZK+Hp2BtNUw|P%Lbz2~&vyLr-g=cc1wbae5Rie3O z%@fuy)k;t1?)}`-`n4PK;qoKnGfU23&<}2gnkY=l>O#;hso#}a_)#!!VV7~y-ZGRp0mMS_L9k^^(${y7*Hyx63|X}aA0yTMG7 zQw8d*^{k8jvGhvZb5~)zc>29j9&KZohgFdDcD$Rry(+;Xd_2o8U6k>bh>dcw%bf52 zAuZ)&VE!_TJlAy&s;auvJPQi+LPS6P@j#?WI#G4F?uPu2yH2dUo1=IVGt$vItyUq5 zjkD(t5{=W7UI4X8n2*vj`OJy#=bGQ`Hsq==y1r?m_g$U*#F|Nuqn)0WgoI4pKSgLn z&M2Yr8Je0&0#%8e>5AB>0{6BSHr9y?-d$36dThQ_TROG%nwRIn5xyT)neIfEHBE1< z&ux{tVPt*8d$t*W+R?|uYn>d5-5gZR%(CUhFDFOUefaP$K7P88lA@xXew=CkR@dgn z_cmPr?CGdv`p$Tg*T#5<*(chpF=j8!R$~e>D$=o%MmuJ~wFh#5scbaY{q;w}rZ2$S z(cwTC1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E z1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1Oo&E1OvTd!2jSuCcgs*M)ejk=gy5RDJfy5 zg`%P&_$r6^{Y(sm?cGb#(^9ikQN zzyAt!#{u{m4So**UISVI)WZ7_w|WTkDuBN`S%&ic)oLq3enjWAp55=gt1I1n2HsU@ z@e23G0-gi93Zt*?eSy3NdyV{hKNABTd0P7G3-^@kNnPov5_o5&#yaTz=m6;+m#+rm z8%0f)q3ouqs(7+5^h_%ZuO03EMY`H~TIQ8&Q)NAj(qlx3*f)s*B?Xz8vm z-0iI`Xz3kyx1!`2^v^K}x<~RjQ_3>b?P^N0sePemT4C5(ZN~LX%e1EC81z4Jko50L zDa%l|6Z-$Ae@W>72gZKX>Hwnr|J*eQQT~bYPp#bcl^aq1fAaFLJm~#@OG<`_{y!xn z{bz^qe|TS&b!y>w@7zwS-;cYiQl_rBxR??8|AR69*Q1nWsM}$#Uh+rh{Hf_3cS}!A z!SzhdECW6NMvY@|4*^hnroY^3VJ>nEHCcwbo36U5Kd!I7G!Grz*Ojgsfp1w_+=A}M z1O6GHJkSjBzz%=E?oVG^-B?{&{;8_GREx5Vcw29wsvzCA!PKZZCi?6b;FKEBJbN4f zc82fz2l)fN13CnF3+O5@_4ZyPgqsWy?phM!?qD;JmzT!~YYK+-B_2L}hz=JZegp#q z1Hi!ET|3!qHkt8nFfkPFE$pl|31;5Aw;1Z~h139b?H^Ev>0SdwB9Sl%XF5D53J~jK zEQPR{es$&V^l)R*TB#JEA}9H|FZzG06lL0c_wQ*dE-IqcF8>R9)bEaW@Us~306;68 zzHmqV5!=U)AODhVfxnj*NmE53xi9oi8HS^+RU4GAu5`4SRvrMI%maMrXWf&_=1EJI zq3mw3T4N2R-uluWe<7BfW6J9W$RBV(&G$dhd*T4<9?6B)fbo1U$}XDaU)1_V4C^p#2{=Nc}&DpRkDbt4J{K8;I+wKg@%By(#rgX_ui)AbKwc z`bTR+DE;=gU71prp>8)@tIr4T`dj)#_bX&2+V0=G+m+68p{&En@8ADG_s#>X|9@cJ z+3@cr8Tu}|8mgNh5484W9zg3yB58jEj`Bn~=pIf$qW*(9z+vDgdVumkDabOPx+K4b zE35+)hV?&Zp+EQn`UEd~3r!WpLaUAIb5fJ9=Ya00Ap_jKf$xdkG5g}h3)Bv%1I_|& z0dDs(k{~=ckY!wVvJm$P1_%ZS28JpF3X-Di(EhInpz*-5e!@`*_aH!3PC|&09xfFn zML58}F~AkDt&d@=C@H#(9yx}x9|bAV<-ltxpx68U^y1mu3X;N9-~HoULzwFT4ZR4v zr|F1S5Eo&`a7;}n*UT#?1mR= zdh0VcXm^%U;}~RmGzfD2MNO8W>;@di3&P3=-FP z9{6tm7wC^n93+{3x&1!~{eRQvCGC2 zo56cVkuKL;fgcM%;{eE&1RRVWD*EhAIwT}W7`zKw3S652HuNL(fNLIlWrVpvls7j4 zb^>+m7pw*K6|6m*Y|S8L%~{J*B#?&P)Hu$~We)i2+FKzVF?ka?e~-a7R84_fEn zm5ksoik9v#UyQ?bGf?I`s0W7BT5&l04aQ-edA!egMEiiz(qD%=%{^eDewYk?T4+6Q zv)R1Q<)wp*@7`B-m4AQsd;R_Wr^A+RzyGwL#S!RThL-M;JM3LF82S&TCGUUH{b&6C z8|M*ptk{+Q;d`aQ(EmwXzM)_e`tNbuN9ex?{e|9~|ALn*qWt4b^q)zTf1>=;(dX@T zJ4=-R{wuqE5i*p2H1E}G{dV`Py>-v`f5g+(ZT-JuQ2Ku-aQTLU3E%6V?@ot5a(}z4 zQ+MNk9a=oX_@BS4{y*$p+;91|i!}StlJ~#puA{D^*F{=?nBNOA(Ov1k9&}7|J`L`T z0R7WE`(^KL&mWer$7z=or8!^qU(~;Yr6t2E#r`=18qR{~ZvXcU-Eju`*8#Lrs|TJv z&1r-DJCJ$c!qTNwKUeH789(*?&E_VoD-~seVb7i~JuVMa`VJ~`Qf;Vg78VxLD*MHt z&(-vjg8Txg?g#ep!Gku~Z>GQV06G^V0M^Ga(<}3T(cebXwWHx2FhT8=%A&o6k*=0- z($xfX_RBmP;Cc4!8TF68yC4nT6QOs&e?geC069to1^CHnuToZm_tz_WiWRcr@~{?7 zjw&QY#o?XL*e*mPZV?O+3=jIXqCeM(t|x*gtm3J(I$ zc->zK@6=hTaSSqr4X|9bQ16VPCd*KF1D?^D47g4P%0zSSDzvzTd^rK`=MRuv%@B_^ zEm?-TJKFyQ(!Umv1L*H}0ZkA`GVJRj&>gP|5~Ab4Zy=x&&>#5{<-s(lgT%U%TpALrV{w;us6abBeJ`Hb!fMz z|73PD`6%UY1yRY<*PaS+Wa>!YcLw<9hD0)U+`#!E=Yt8w*KvPH^79=j|95ETlY$>6 z$Uool@iX&-^ymInK%_sW68P(|x91nN2!qn(LN-uCj)o<8@l9DwB9DA9Bh)XvWku5a4;AxmGI2WS=?Y1TJi zStS1HVPF>IGjRZLijgoL_=lf3hD~4a743H7k6?gcfM9@NfM9@NfM9@NfMB2p4CrgC zO;uHpc7n5+y=XyIUTU}Z&h0y4zkM7Ya4kX^4Y&&nx={MBT#O1}!tGK+7_I*E)eo;J}fi#Kq%l)YS6LEe0 zp#HzBOYv~~)6`X01r#pQ$auj^(0{{qiV!NVD7Q%zaUZH25vy^^$8 z8!f;&NNpR}t!>H9dWg=RNB9g#fHP-0&w&9uI*%qC&;-Ew>CSuyeoY2C&HWi)US1xl zq@;v_9z1;bus=niD{MF~Y}P788K0F3(xJ2f_e0^_!eBUaCS#=ayj@Li9T0fPhcZ8R?vDoP5CAj-DE)M2FNSh7DCcNttWeOU zH|@xuiM|eHTH< zX#vLnH5TTkO^0$EtzRPNKus}LT}K$;+LkNSU9($_iv1NZh(xleEU2W9?; zNWZA|59kc(_A;Jd;d;*xgf&?Ce+tO_Bl~K1p49~5DD*P@=$y4ddj|kAqx0@g1JJpT z^x`7;-3)sYe{*gzxETSi18&lZSn#(5&qIVc@C>Y8wPFLz`xL`@=QXqd^FP%#o6Req z=HygDe-+^^;1qfP7fB9%;9=m7-UIvtD0=uHv&d|%ei4i@ifKVnN~{R-0FDBF`$+doQwA>`rq*ENiN41jCOxM|wE8vi00 zA?*?5=~tDPrWsZ@@n-i|n#sfM%lV&<^q(eAKa3}PoBl`Sa`xBtubzJO74kjm{~GJ9 z?kw9KdhVS6?9l&la{aGTlI^YjAM}^b$-~uASDgs;GCF^nE;$2y4^pOIk32j``zrGE zd$>B$6)u(d-N?i3&ue)9H5=xC{NNo}7;Q*~xoovP4DWOTA$Bz7@sQoAPP}V;pt|5MOwzNH9XAj^3e!r`;z-fkwJbh%MeIgbD zi~ybcMEpVQ69IG(KY{^*0fK=6XJGdZHj)+Sc)9?3hOs{r1#1_M!aNpwkMrGp9=tPcbF{T?gY_=xeJJ`K?kJuHxOM`d z@8c-nm6F@hx6{`E{qmk~gNY#<$k;$lc5=7utSnrnMv#RK(!&7eO91ZPojIpJbg*`f z_Gpm*6S=I^u3>!w0diWx+Ptll&{R>dznXAm3&?=55ik*d4z5Ra zxH*7H3$}1?2L4Q6o9@mI43K{w;dzCcsjH#dR#;Gg=Z{Aq&oEeDf%4B40PfzESq1+3 zW!)@%=h&x;oRW2dO)d)#e{#fG+*B_+6gr$OfSJ`k9w-a&l|{2ndy2GUnwmt8zO*z1H* zM@?le)I)z!0@}AF@GTr4$cQkDTn@M|-S z^3O9mpF`^(I{Qt1k#ESufUwc`G^v1$Zjc7|Rq$uwS{!g2fXY5Cpt5a^mwEizzBFwu zH!&LPY0ov%)mlP{P3w&pKYRLg5!8VQv&rjEBm=4&0e=8yUb=W;CX7XAsmMuv1K77~ zCZqw!n7))f`s7Ir`kr03Uywe0{EOPBGXT8(`VFo#0BsL4@3*a1ThUz^M)&Ig->oO< zZJIWi8lmxTxAhNY|Dl8XI9T87f6?L2UbIy**S+t9#(I!A>1i|t$=Oa0Qu=`hv0e~z^0pLklgcnD_eKZK|9I- z;iI;P9=#(!&w#&Svcke>J>FG1pF{f=^~?N8Z~H%rk|O`<{@YI^KMUx#p3Z!L@=qgx z4dh4p?%y6f*DvE=R6an4KmgVGa|-#8pJRCa0oQ2#C>jrRRd!I{GZ~e$?#g0s?<-0Q zp}sH|V0t&;Lj7hu9ytjil=jKpJi`NPkNa)xj>|%r1Oo&E1Oo&EUm5ti+z9Jfq16Mk zgyb=*4g8m29!$tgM+f|LVRriOc&*D~=>0J(0KL;mqXP8pcxQjHr-_ERW1w&OUEla*cqsm!NEgDF0^oLPX0PY{ zA^r>0;y)1C*aP6hu2dR=6zUTpByE~2dX&CO$=AS9Ii5i+qsV|TrUHxq=FwBbHN-;Fg5-! z#6;t1l!iDL0uUd#8@c%j+5rh6{-`sj@URi*0$s$9-mUg} zzfs6*lVM$n9qjGmhy(T!+M1u2XAj{Tqq%%ohlJ+vwE;F%AluVCNA4fet_wh8d~|LH zC33(G#+IUE0q7k_F%?_{w_cAu;o0+m@9G~iw>5ArLuRMuw{yOZJZ#F2R0pz5d=8)?H0rsUTwub&vxNcwewJKR+L>V=}?tCBZd% ze`I+yD8LHpN^3G$flLTSfF8?1tNfiMD}P9j5ch3Bn}&)4HT~mmYv|EAxgUD!pN{xZ zJ>LfY@wlj&=Hy}1b4^G6E7cKyGKwGe*r66L?xsdhoaBCBUY`YIPzI>tfVCQ^{ZWGW z_;J5*jrt)g&qSP91^(1=03Rs-t^!Enb8h%E^qBD6M!;tPnuDW6CAiH3p!u)YRPH1H z=)Fp>+h2H=YX1k^aZdH%T9(X?yOW!}Zhs)$=x)MB^|N$0etO;if^a1Psel_)fcj?R z0jz*XD)-Sd0p#fU(FJB^mLxFeWwjJFs^x)ct5}$vl8j2Q{ zyU@88@KzfO)R&F{oCM(gYGl%U-w0>uO@lB_10o@QH1<^otN=uHz#mMS?`xoaLhVv} z9qnXigW`$=gaYvRQNO)&zkSM7tdNuBgFa3Yq(6D5hg%Z(wE&m`u66*Whvu3?h+6{i zfeij|y&F)|fmdKMQ^t$i;hq=(wSQ+wJcr{FTu;K|BsbwXYW$fT3l1c2_s}&T`E&R+aK{BW4Tv;Yt#KOq6zVXqA#G-C z5D9KkfKYgLCiEHbN4_!Po)&;SF4Rt+q;R8|f56`u0P2&j?Z6^1=abnrq22l>&cU{{ Wp}AH-3G`-Y0!CUTI6C~{&;1XoUZ!6F literal 16958 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 diff --git a/client/public/logo512.png b/client/public/logo512.png old mode 100644 new mode 100755 index 9193a2a39c57e0e6d5995c4511a74a2ee2bbf162..693046e7b6a6797a0b29baa95e64e9c69df44eda GIT binary patch literal 5294 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 diff --git a/client/src/components/CardModal/AddAttachment.jsx b/client/src/components/CardModal/AddAttachment.jsx new file mode 100644 index 00000000..b71926da --- /dev/null +++ b/client/src/components/CardModal/AddAttachment.jsx @@ -0,0 +1,23 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { FilePicker } from '../../lib/custom-ui'; + +const AddAttachment = React.memo(({ children, onCreate }) => { + const handleFileSelect = useCallback( + (file) => { + onCreate({ + file, + }); + }, + [onCreate], + ); + + return {children}; +}); + +AddAttachment.propTypes = { + children: PropTypes.element.isRequired, + onCreate: PropTypes.func.isRequired, +}; + +export default AddAttachment; diff --git a/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.jsx b/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.jsx new file mode 100644 index 00000000..de469268 --- /dev/null +++ b/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.jsx @@ -0,0 +1,108 @@ +import React, { useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDropzone } from 'react-dropzone'; +import { useTranslation } from 'react-i18next'; +import { closePopup } from '../../../lib/popup'; + +import { useModal } from '../../../hooks'; +import AddTextFileModal from './AddTextFileModal'; + +import styles from './AddAttachmentZone.module.css'; + +const AddAttachmentZone = React.memo(({ children, onCreate }) => { + const [t] = useTranslation(); + const [modal, openModal, handleModalClose] = useModal(); + + const submit = useCallback( + (file) => { + onCreate({ + file, + }); + }, + [onCreate], + ); + + const handleDropAccepted = useCallback( + (files) => { + submit(files[0]); + }, + [submit], + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + multiple: false, + noClick: true, + noKeyboard: true, + onDropAccepted: handleDropAccepted, + }); + + const handleFileCreate = useCallback( + (file) => { + submit(file); + }, + [submit], + ); + + useEffect(() => { + const handlePaste = (event) => { + const item = event.clipboardData && event.clipboardData.items[0]; + + if (!item) { + return; + } + + if (item.kind === 'file') { + submit(item.getAsFile()); + return; + } + + if ( + ['input', 'textarea'].includes(event.target.tagName.toLowerCase()) && + event.target === document.activeElement + ) { + return; + } + + closePopup(); + event.preventDefault(); + + item.getAsString((content) => { + openModal({ + content, + }); + }); + }; + + window.addEventListener('paste', handlePaste); + + return () => { + window.removeEventListener('paste', handlePaste); + }; + }, [openModal, submit]); + + return ( + <> + {/* eslint-disable-next-line react/jsx-props-no-spreading */} +
+ {isDragActive &&
{t('common.dropFileToUpload')}
} + {children} +
+ {modal && ( + + )} + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ); +}); + +AddAttachmentZone.propTypes = { + children: PropTypes.element.isRequired, + onCreate: PropTypes.func.isRequired, +}; + +export default AddAttachmentZone; diff --git a/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.module.css b/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.module.css new file mode 100644 index 00000000..6b995385 --- /dev/null +++ b/client/src/components/CardModal/AddAttachmentZone/AddAttachmentZone.module.css @@ -0,0 +1,13 @@ +.dropzone { + background: white; + font-size: 20px; + font-weight: 700; + height: 100%; + line-height: 30px; + opacity: 0.7; + padding: 200px 50px; + position: absolute; + text-align: center; + width: 100%; + z-index: 1; +} diff --git a/client/src/components/CardModal/AddAttachmentZone/AddTextFileModal.jsx b/client/src/components/CardModal/AddAttachmentZone/AddTextFileModal.jsx new file mode 100644 index 00000000..b7aaacb6 --- /dev/null +++ b/client/src/components/CardModal/AddAttachmentZone/AddTextFileModal.jsx @@ -0,0 +1,83 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { Button, Form, Header, Modal } from 'semantic-ui-react'; +import { Input } from '../../../lib/custom-ui'; + +import { useForm } from '../../../hooks'; + +import styles from './AddTextFileModal.module.css'; + +const AddTextFileModal = React.memo(({ content, onCreate, onClose }) => { + const [t] = useTranslation(); + + const [data, handleFieldChange] = useForm(() => ({ + name: '', + })); + + const nameField = useRef(null); + + const handleSubmit = useCallback(() => { + const cleanData = { + ...data, + name: data.name.trim(), + }; + + if (!cleanData.name) { + nameField.current.select(); + return; + } + + const file = new File([content], `${cleanData.name}.txt`, { + type: 'plain/text', + }); + + onCreate(file); + onClose(); + }, [content, onCreate, onClose, data]); + + useEffect(() => { + nameField.current.select(); + }, []); + + return ( + + +
+ {t('common.createTextFile', { + context: 'title', + })} +
+

{t('common.enterFilename')}

+
+ +
- - - )} - {labels.length > 0 && ( -
-
- {t('common.labels', { - context: 'title', - })} -
- {labels.map((label) => ( - - - - - ))} - - - -
- )} - {dueDate && ( -
-
- {t('common.dueDate', { - context: 'title', - })} + + +
- - - - - -
- )} - {timer && ( -
-
- {t('common.timer', { - context: 'title', - })} -
- - - - - -
- )} - - )} -
-
- -
{t('common.description')}
- - {description ? ( - - ) : ( - )} - -
-
-
-
- -
{t('common.tasks')}
- -
-
- {attachments.length > 0 && ( + {labels.length > 0 && ( +
+
+ {t('common.labels', { + context: 'title', + })} +
+ {labels.map((label) => ( + + + + + ))} + + + +
+ )} + {dueDate && ( +
+
+ {t('common.dueDate', { + context: 'title', + })} +
+ + + + + +
+ )} + {timer && ( +
+
+ {t('common.timer', { + context: 'title', + })} +
+ + + + + +
+ )} + + )}
- -
{t('common.attachments')}
- +
{t('common.description')}
+ + {description ? ( + + ) : ( + + )} + +
+
+
+
+ +
{t('common.tasks')}
+
- )} - - - -
- {t('action.addToCard')} - - + + + + + + + + + + + + + +
+
+ {t('common.actions')} + - - - - - - - - - - - - - -
-
- {t('common.actions')} - - - - -
-
- - + + + + + + + +
); }, diff --git a/client/src/components/Login/Login.jsx b/client/src/components/Login/Login.jsx index bd787afa..acff2dd8 100755 --- a/client/src/components/Login/Login.jsx +++ b/client/src/components/Login/Login.jsx @@ -1,8 +1,8 @@ +import isEmail from 'validator/lib/isEmail'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import isEmail from 'validator/lib/isEmail'; import { Form, Grid, Header, Message } from 'semantic-ui-react'; import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { Input } from '../../lib/custom-ui'; diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js index b30bb981..77aee264 100644 --- a/client/src/hooks/index.js +++ b/client/src/hooks/index.js @@ -1,6 +1,7 @@ import useField from './use-field'; import useForm from './use-form'; import useSteps from './use-steps'; +import useModal from './use-modal'; import useClosableForm from './use-closable-form'; -export { useField, useForm, useSteps, useClosableForm }; +export { useField, useForm, useSteps, useModal, useClosableForm }; diff --git a/client/src/hooks/use-modal.js b/client/src/hooks/use-modal.js new file mode 100644 index 00000000..08067034 --- /dev/null +++ b/client/src/hooks/use-modal.js @@ -0,0 +1,15 @@ +import { useCallback, useState } from 'react'; + +export default (initialParams) => { + const [modal, setModal] = useState(() => initialParams); + + const open = useCallback((params) => { + setModal(params); + }, []); + + const handleClose = useCallback(() => { + setModal(null); + }, []); + + return [modal, open, handleClose]; +}; diff --git a/client/src/hooks/use-steps.js b/client/src/hooks/use-steps.js index b0b2505c..a1b60562 100644 --- a/client/src/hooks/use-steps.js +++ b/client/src/hooks/use-steps.js @@ -14,7 +14,7 @@ const createStep = (type, params = {}) => { export default (initialType, initialParams) => { const [step, setStep] = useState(() => createStep(initialType, initialParams)); - const openStep = useCallback((type, params) => { + const open = useCallback((type, params) => { setStep(createStep(type, params)); }, []); @@ -22,5 +22,5 @@ export default (initialType, initialParams) => { setStep(null); }, []); - return [step, openStep, handleBack]; + return [step, open, handleBack]; }; diff --git a/client/src/lib/custom-ui/index.css b/client/src/lib/custom-ui/index.css index e42ac12d..e50747be 100644 --- a/client/src/lib/custom-ui/index.css +++ b/client/src/lib/custom-ui/index.css @@ -34814,11 +34814,11 @@ select.ui.dropdown { overflow: hidden; } -.scrolling.dimmable > .dimmer { +/* .scrolling.dimmable > .dimmer { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; -} +} */ .scrolling.dimmable.dimmed > .dimmer { overflow: auto; diff --git a/client/src/locales/en/app.js b/client/src/locales/en/app.js index d2074906..eb2d68be 100644 --- a/client/src/locales/en/app.js +++ b/client/src/locales/en/app.js @@ -42,6 +42,7 @@ export default { createLabel_title: 'Create Label', createNewOneOrSelectExistingOne: 'Create a new one or select
an existing one', createProject_title: 'Create Project', + createTextFile_title: 'Create Text File', currentPassword: 'Current password', date: 'Date', dueDate: 'Due date', @@ -55,6 +56,7 @@ export default { deleteTask_title: 'Delete Task', deleteUser_title: 'Delete User', description: 'Description', + dropFileToUpload: 'Drop file to upload', editAttachment_title: 'Edit Attachment', editAvatar_title: 'Edit Avatar', editBoard_title: 'Edit Board', @@ -69,6 +71,7 @@ export default { emailAlreadyInUse: 'E-mail already in use', enterCardTitle: 'Enter card title...', enterDescription: 'Enter description...', + enterFilename: 'Enter filename', enterListTitle: 'Enter list title...', enterProjectTitle: 'Enter project title', enterTaskDescription: 'Enter task description...', @@ -129,6 +132,7 @@ export default { addToCard: 'Add to card', addUser: 'Add user', createBoard: 'Create board', + createFile: 'Create file', createLabel: 'Create label', createNewLabel: 'Create new label', createProject: 'Create project', diff --git a/client/src/locales/ru/app.js b/client/src/locales/ru/app.js index 725e2597..8219f827 100644 --- a/client/src/locales/ru/app.js +++ b/client/src/locales/ru/app.js @@ -46,6 +46,7 @@ export default { createLabel: 'Создание метки', createNewOneOrSelectExistingOne: 'Создайте новую или выберите
уже существующую', createProject: 'Создание проекта', + createTextFile: 'Создание текстового файла', currentPassword: 'Текущий пароль', date: 'Дата', dueDate: 'Срок', @@ -59,6 +60,7 @@ export default { deleteTask: 'Удаление задачи', deleteUser: 'Удаление пользователя', description: 'Описание', + dropFileToUpload: 'Перетяните файл, чтобы загрузить', editAttachment: 'Изменение вложения', editAvatar: 'Изменение аватара', editBoard: 'Изменение доски', @@ -73,6 +75,7 @@ export default { emailAlreadyInUse: 'E-mail уже занят', enterCardTitle: 'Введите заголовок для этой карточки...', enterDescription: 'Введите описание...', + enterFilename: 'Введите название файла', enterListTitle: 'Введите заголовок списка...', enterProjectTitle: 'Введите название проекта', enterTaskDescription: 'Введите описание задачи...', @@ -133,6 +136,7 @@ export default { addToCard: 'Добавить на карточку', addUser: 'Добавить пользователя', createBoard: 'Создать доску', + createFile: 'Создать файл', createLabel: 'Создать метку', createNewLabel: 'Создать новую метку', createProject: 'Создать проект', diff --git a/server/api/controllers/attachments/create.js b/server/api/controllers/attachments/create.js index 6ab255a0..9f3d77ad 100644 --- a/server/api/controllers/attachments/create.js +++ b/server/api/controllers/attachments/create.js @@ -60,7 +60,7 @@ module.exports = { dirname: file.extra.dirname, filename: file.filename, isImage: file.extra.isImage, - name: file.filename, + name: file.extra.name, }, inputs.requestId, this.req, diff --git a/server/api/helpers/create-attachment-receiver.js b/server/api/helpers/create-attachment-receiver.js index 2fcc9156..ecdb4de8 100644 --- a/server/api/helpers/create-attachment-receiver.js +++ b/server/api/helpers/create-attachment-receiver.js @@ -3,6 +3,7 @@ const path = require('path'); const util = require('util'); const stream = require('stream'); const streamToArray = require('stream-to-array'); +const filenamify = require('filenamify'); const { v4: uuid } = require('uuid'); const sharp = require('sharp'); @@ -33,10 +34,13 @@ module.exports = { try { const dirname = uuid(); + // FIXME: https://github.com/sindresorhus/filenamify/issues/13 + const filename = filenamify(file.filename); + const rootPath = path.join(sails.config.custom.attachmentsPath, dirname); fs.mkdirSync(rootPath); - await writeFile(path.join(rootPath, file.filename), buffer); + await writeFile(path.join(rootPath, filename), buffer); const image = sharp(buffer); let imageMetadata; @@ -68,8 +72,12 @@ module.exports = { file.extra = { dirname, isImage: !!imageMetadata, + name: file.filename, }; + // eslint-disable-next-line no-param-reassign + file.filename = filename; + return done(); } catch (error) { return done(error); diff --git a/server/package-lock.json b/server/package-lock.json index fdb80057..8c402120 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2386,6 +2386,21 @@ "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=" + }, + "filenamify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.1.0.tgz", + "integrity": "sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg==", + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -6450,6 +6465,14 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -6674,6 +6697,14 @@ } } }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", diff --git a/server/package.json b/server/package.json index c2924540..9e778d50 100644 --- a/server/package.json +++ b/server/package.json @@ -42,6 +42,7 @@ "bcrypt": "^4.0.1", "dotenv": "^8.2.0", "dotenv-cli": "^3.1.0", + "filenamify": "^4.1.0", "jsonwebtoken": "^8.5.1", "knex": "^0.20.13", "lodash": "^4.17.15",