From 2a39c230fc13976d95ca57b1cd822e3de665d8a8 Mon Sep 17 00:00:00 2001 From: Ilya Asiyuk Date: Mon, 22 Apr 2024 16:18:53 +0400 Subject: [PATCH] #4427 - Macro: Store custom presets in browser local cache --- .../RNA-Builder/rna-library.spec.ts | 20 ++++ ...lay-after-page-reload-1-chromium-linux.png | Bin 0 -> 14862 bytes ...lay-after-page-reload-2-chromium-linux.png | Bin 0 -> 14862 bytes .../src/application/editor/tools/Tool.ts | 12 ++- .../src/domain/entities/Command.ts | 4 + .../domain/entities/DrawingEntitiesManager.ts | 61 ++++++++++- .../ketcher-macromolecules/src/Editor.tsx | 19 +--- .../contextMenu/RNAContextMenu.test.tsx | 9 +- .../components/modal/Delete/Delete.test.tsx | 29 +++-- .../monomerLibrary/MonomerLibrary.tsx | 9 +- .../monomerLibrary/RnaBuilder/RnaBuilder.tsx | 6 +- .../RnaBuilder/RnaEditor/RnaEditor.tsx | 2 +- .../RnaEditorExpanded.test.tsx | 27 ++--- .../RnaEditorExpanded/RnaEditorExpanded.tsx | 14 ++- .../RnaEditorExpanded.test.tsx.snap | 5 +- .../monomerLibrary/RnaBuilder/types.ts | 2 +- .../RnaPresetGroup/RnaPresetGroup.tsx | 2 +- .../RnaPresetItem/RnaPresetItem.test.tsx | 2 +- .../monomerLibraryList/MonomerList.test.tsx | 3 +- .../ketcher-macromolecules/src/constants.ts | 1 + .../src/helpers/localStorage.ts | 4 + .../src/helpers/manipulateCachedRnaPresets.ts | 81 ++++++++++++++ .../src/hooks/useSetRnaPresets.ts | 72 +++++++++++++ .../src/state/rna-builder/rnaBuilderSlice.ts | 100 ++++++++++++------ 24 files changed, 390 insertions(+), 94 deletions(-) create mode 100644 ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png create mode 100644 ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png create mode 100644 packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts create mode 100644 packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts index c098e0f15f..dcb78a94a5 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts @@ -291,6 +291,26 @@ test.describe('RNA Library', () => { await takePresetsScreenshot(page); }); + test('Add Custom preset to Presets section and display after page reload', async ({ + page, + }) => { + await expandCollapseRnaBuilder(page); + await selectMonomer(page, Sugars.TwelveddR); + await selectMonomer(page, Bases.Adenine); + await selectMonomer(page, Phosphates.Test6Ph); + await page.getByTestId('add-to-presets-btn').click(); + await page.getByTestId('12ddR(A)Test-6-Ph_A_12ddR_Test-6-Ph').click(); + await expandCollapseRnaBuilder(page); + await takePresetsScreenshot(page); + await page.reload(); + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + await page.getByTestId('RNA-TAB').click(); + await page.getByTestId('12ddR(A)Test-6-Ph_A_12ddR_Test-6-Ph').click(); + await expandCollapseRnaBuilder(page); + await takePresetsScreenshot(page); + }); + test('Add Custom preset to Canvas', async ({ page }) => { /* Test case: #2507 - Add RNA monomers to canvas diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..c052487bf201520fe45ff3c86b38879e944bd44a GIT binary patch literal 14862 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 PiI9FOC!Q;&^X`8D&f%V` literal 0 HcmV?d00001 diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..c052487bf201520fe45ff3c86b38879e944bd44a GIT binary patch literal 14862 zcmeHuby$_{y5~X#K0rZHQbI*Ry1NvSP(nfJ?v(CSQc^$`ASECoQX<_YUDC}WrMr9X zwdb6fJ!hYN=FItXuJc{rb+KHq*89Fs-1o1Z_b=omui=v8A`pmc(o*6I@besjz*zYU z3tnv;-|2)O7iX{Z%4nkV|@hj(qwFwuMkA^1~TL($?Kl&Fsx}9pmlX3!7yJWvrc@zGFtd*Pr#ob{gSSUk~a*I$rj@PLOm(`!7uC zO}`f({{0uxDw~}~rBqR`6rxDPZasc~1!cgo)2vOxZ$HOj+}%uUHa<6JpjK$2>mqcr zTClr15;alb6eF%!aI}<>94qYRn9>|C;vrCCg~HOgI6vE6?73`ZW%X4iPRz`V0TD$f zUwq0eZpyenbvx8(pSrRoG`Xl34DzGw?N5{kzVmVgalcmi1RX$nH-QC@W zx1qYvw62i$+Tj5#u7NM)d95 zx3My`#H?B}F)W&kz3F(*V^|)nEMTt;=FFzYXw{F6#lOEvr#4u6s97l~T^me8I%b_xSi?G4Y6XxX9x;jqyc9MC=!TzikXASs3n0_B?(cqveX@y*5_T zm+}O=va-_J(a~ryTNQaQT5S1TO6o7#sMhc0xGxiegb$0%l_c`IMMiMt=D*D>>+EqI ztc^)NdxrZuO9@j424Q!7B7R8AEj08t@AjBgO}<_u?xo9@aq#htP}Lsel`c`;A}1l; zq8AA;j1eC`)DGsTvy)y8xy>6EqwN_tHKhZWY%etJGwJ<;Ln9gX#e66?gi;Xs;|E(9 zwP@Gz-im6O?E~|{?CF`t;E~bjq$D|U@hjYB{fMcKIL^gASAPO(xo|3Bm0}CB+}vEm z@aU*Q`ZJ=g1b%%7O5acCyj~c|tcTk9a*Ep7ikB(c#%2cUeNY#b-mM13LPJZqB_UgT zdr#APbl6DQ;I=3Q2MJT*7I(JWSubCwpy>Fa+hA>HcUh;_8)J2}C>ZM+Il}tv*uiWt zyEXM`@NW|?-+%zc6fsOKkGh0@$1)oyjmn5}^Z&ns#fpND{YvhBT&n=X{w~UWRg>ma~fs2Akfm zzy5T_8{ODc z7wd&yx*EsH%UfcRQXVgnmVdf7GgBXUgT|yM6%$eNX5!w12Yztz@$qq$p9VPe^z`oM zM++UvqRC0ZZoQu$Uxwwy+{b74=W3DJz|8JaA95&HtLj+*KIP`Y0UZwy5;n$9-3Bbz zgY`u7_$=i-Pi;X50nOqC+mp*LuV2L1)DpVpDLE=KJ8_0-=rHDVIerPB}B=67SK{`agJ%{9Wzg)_!7RV4*huGBUE=4OB9<=OIRpQtn6M^sM3Y%ddq+ zp(|Lu^YPiCe6ulGWxqbID#qG&CF>k7OK9C0WgcFk;+1r+`)7W>alSo9`dQnF6VBbc zcX!vu4&D|$53E%2h?!W7y`L*SF*5E|KS8D(vyJ%m;EU~Y(sO$UwQJoxCFg(DTu*gw zT`Pi4wrY)=atC@GZsZi+KYP)u9rW?`lfP2PtJ3t3df)h5pTUTd^|*WMejSO}V?y`; z$_u}BW?uUt{Qb3~X!DmZAN1Y3EB?0Zbxx4em}B9_rd~=D9@{Lb*n*5x8UTA4}~S{eBMYvI8*Z5``n3kwUr)1k*| zC6>VzZbgL2$tZ6`(vB-t!AQsa^O(aKuboVVbcSoUxfyznB($p9sJ&KWreZJ%PdR4N z*Yh@a!n4SP9mT>ntj5ayF4tW6*O^Q$8I$*|gjQc+)^1XCSm`a7RVO5ziQ}*lN^1_w z@bvN5sI-0?{TRh|cHt4bBE(I8jax{_NT!TwF{cc+7iy@J!6r zl{}iUg3V-+ZP)d7wN~MWxv`QXl<~!M;?~XQKR(?P{TZ%}1^cmbteEER-M4980jLAd z!-$4c7ah?Jxx^+(>h8O6J;#8x)Hi3xr?+jWybFxGUKA)<-s?)fJau|h(4VDT^Js@R zPIl4W>72-3jZ#|reaG2`yM?8tYPA7-r+T45Q})O%HQR^AU}8Nka{gl{zW499-1$~+ ziX1!R%gGUyx-Z>BS7DPQ4lI1CdbnhYZkojDk8SnUwY8;@m1|~huFn?R5jdV?Iy~_` zCS1F&X{@3brC2_7AJ)zqyeMZ>^OS^?s)f{Ykb{<^gr0|N^1`oU0(YF=d7ZDSjviOm z9@#m)qqbS6a9CNObcYE|4<>=>e+*?He}0cvHa?}tBrvSHYtMYhWa29`g4=u`RWJQn zf}k4~9UWcjJsN(SQ&KFSw@Fp5J70_`*eDjE5Outj)}ylOaC0z9 z_KagLvwZKHQk6i8pwp&vGy`%d$_iyP6t;%UDv+VQ85R~+T3Q~d+j8e<-}2tQuC47X z;o)j#(x_H(@n0^Jy96yZA}Gw9sS_(7#J5+O9MySyzY4k(kq49%l$;CkA$2-aKBkIH z8|fM8Q6E2w?+`ODjZ_c_iWTR(o8FO)=Sg!y@k-0d(axN@e3kbY;t0!0q2yf%`?z%<(+10O++R-@>1y^gKGlx^~2S z?;W6K4C`r>mukVX8^T_IlD?Hbw)4-u64_g!*<0I#?+y-Ct6+a_sm;t_J@x_Em${A?=8Wq_8vn{mRJ-0`w}P z6%h%`jB4Us5Zwt*_ox^66pvGaS9wUuZ^0IPDJI$?*!Q}h(y&F_*MD2RKZ{|aa?D=C z(tDoEEfy#i92D{^$810>RPo!CTZv^e6i}%{zlSvN2P37^B$Z=@jQ!4j0*Tny%k8jF z_S>K_Jj5ISuGCl|TOl@~OJe?x)z|s3wNCiIQtAIQ{XuCD8L!CO%;4aW-D85gt0wm< zk5ZxL9{sbmb)|1p#zeywJnB^6`%Jh-aFfSk?i97h^>@2s=4i^{Qk+)cz46i)WKwi(}b`W7RzGqITK0>siUK?W0#af z?ky>oGRK^EEVZ^x2j8c=qWI0nO=@cg+5Q#TpX>d_bC7Ya=o+R#sS|I!=Qift^0SZg z-3irKGBriucNH0qjkebEe{GpW{PXVqA2j~|%&!>sr}8u_ki^Vt)@!3hGfBHNYx`?s z9iO;^fjRunRHPF*+f@va&^P5>Nx4Bq6$bbU&F@wQLs@H16$GYRp5$YD4drU~=V>Pc z@F6+)LT$=v)KS~m=m&I*$71-AjLcPFRP<{`!nas7N^60AkqSC8>FVlcs}%*c$FS~9 zIH95)``WVgu6b%`P+Xj!ur>q`&~b4U^3k11RXDDZqiH9#2!*;jx+##shw<_8(H#%&|4w;g3RoU`Jd|srt^{e|XU{534VCh-ybjhU3e5(Bkj(0~$GfJ`LtiIEc^RDT zbc=GC{Jx`3_t(Jp*R#>eTJ!^s!c20B0#AXKVDNdK@&SKJRWEr1#4)!pUdf2aeDleZ zCtF)vbzNOyUuEOBSMq9z7v`u{%N#sDCMBKMj%~B`uIw%DISKfg^?$oDR${dPg!3*p zchTQIuD%%+8OdkAcpbh!4LA^mprZ`W0bWaAU(`)HIRkZsMyd5%Z*Ok}%b|vPcvJNJ zkY>zWGMtLJXkX}R5c8qi{_1quLXv(vlG$RiGOoyc$o6nkH!?CZ$ecJl8(7@Vubc9l z^D)|y5fP8o)X0I8rvigO57IlBF*KhS5~A>CRL;FlN*budXq;uIXc3Kb?OHmTku3mc z`wC=jb*v<>)M!g8p5KnPr>6&?YaErZ>(uh{GmtZ$SEv&Bed0Ffa9Dn2)Sc9c$68qn zd?-%Hh2!=2SM+ONqgBxrZfEsfzX69XM_fxw%N-hF%JAja)EX;P93*+-y87n9z!?oK zX^I&6GL!e#b2L5G1%EQ%_kDN&Q0$=;l&SyyV*A?pL`=k`SQ(e3D=!AC9H8|pRER+p zV9gkv&!{zb50$t~er<`&mdsSHm$Ub7fS1x2N6dS$$+VzKr10zkqO!qxKUlaHK@#&J z)_D3~wtda${CyOjbB7TpZ4^#gk4f2w`mRB&1>CUVkE`%Mjt+ecu*Kv)D4 ziL2zoC!3nK8QBx%jOi=;S&S7#SBSni9${*0e~3fGarUa><_wszFNYw;5_8m|Hae;4oT=%E{y?!~Su5|MQ}>Ikfz+WD7=+kDwZg9*tWQ$v)eVUGR>~JE zdGg`r(B<(ux;(z~I#8fBe;--25w7=qoatb2Zhdt83{z^&mKMh5v3X34Sk6}2%IWtJ z3nWSq^*X#_C901NetjSPYqOa6OV}5aI4HrJ(a1`dGdp4fifHqI{yeGdOiek1e0tw^ zxJ6CfYCnw7|WXV1k^5L4<*ur)REmr1aXh@`qz)LF?<@ z?Yu-Ng|?5c0}Qe8iH{kY+Kl%5>@6M1cl5|xITZU>RdX3CE+zoFJTlKgtpNpwrI z@Jsso?6Nhrpy_3gSp<(84lGW3X}}{$on^-;u5BxP+c~twePHD>AZZpO3i|_BJz0!$ zTyY!whF`@0ytOP6X$|5H!3J|2F`Ssiupg&#Q)`Q!v7|pLG4gY zvYIH5M(_WXmFF*B{Pt*QhUGk{ZZ&amygE{VLqM=F*BS-NOb;{(0f%Mr<}k`QVYlLW z+Zm8aiJ4S%$hr+UddlpKjk->2+D&*ZN5!3;opIbpQzNE=1GGTa0Bw*SB$MJwnp!g9|QFUp1onh97i=In?X2H~Oes+K= zEiL`ww4oIeR};N4T4Y}A7)?azzV*AHs3;v&L#Xq$+oSN|#nB>0B1WaT{AQ}O$39pZ z)$VcMzC8qizZ*y-nh*kJTi!TJu;=W4Yo9zlz4@W)O8lVq!@0^YexF zhxJD$QB`idAZDh61mf%Gr>bT3`!fcpPYfyYR+Ko^&R=rWOTyUoo7g;#?KLzs_*}N{ zS65df(Au+inq+t{3_k@01&E4iFc`?Ka0-6Ck>-u@GS1f4){l(JVJ$6E49d9{Wb21p zvwQ*qGwbVJ&>iJfRAT2-T0CN=J0N+%Gb?}y32-}c{><%U zuHDwD#vP_=J}a;KU5Aegs5}Y6Zu`}!$^d_VLL#E4(1!2sls14=X43!d9^;}f!LFZ5m6!#c;UMWd3?FEodl2S&B?DU^bFQ-m`W>UqY^FyJf*;{@;6hLPh z$=8=mYPf=pz0-8tO6Ug<(MO8$oAuAah4)EeGk4m>X8U*;%I4>6p}a0UV7vN;hZBIL zSUGq_lu%IaUtna*h z`4SCRaBk9md={Vc+QR%adnF2{xE*##EW5shjjUQAAqg2-D=*5u;rr{XQV{+C5l&TX zR99G)VqC^zGibSvK(c7Iftrl-*Iy6)h)F_MXYnv7_^iFV5(Q@$7XvdgjEkvJb4}np z1e35{`T$ZsEKrxd0p%mm%Bdd(2TQzTy>cByZ-7Fe#5Q+#hXcr&=dT>Gpk-pZ3fr$m zDE(m4Z6n{Plf3xNgxa5q(?}mTF$9Qq;*ydD2Camtsj1EjiB2k2uH3N0F?9|%r#~F& zCsBKy-T*@0`H4G@+ng9yo6y;AAI$xuKUUZw5^3`#2*Z~nmT`KZK7t6b-5r^f6qcB% zY0CSY4C>1l%?iiik&%}LMzXN)K#}b%PWA%n(&FUMspkBLhw{%%Z3!;3{y^Bz7#|`d z>;13YcK#D*{H#K?ZF75D57rLpRV)_mY8h}ny1Kg3C$b4?%uhM=misf^w_E9$)Qhhs z%1MGx;|zlVn5=DccXtl_r>`KB!-co%u&FEn|ZGBMjbmxcWPj%v!2Nhz0 zm!kfGMzrxB9wha$#kK9H2d2+YmXEtpmn8V#xX8G($7p4Kxwb6!HbJ4K;cF$zpUaWy zcQWqcRW=QoaDDFikQDJo_BOT464}y+22nAY^5;$Z1 zAPX<%WOKJ#8}tw^@@up?#5wdk?JvG<631b7m9q%s$uebX;-k5t_?oTa=P6Xx$5ype zgSC^iVM5;aD(s=s z{J-D%d#3W=qw)VX8Zz%@PZ0>RN^}=$c&ac-8{&Ib+bFylx|OIL!JGNL62f`(#-T<5+TnAuPK zuckonIXpVD*e&XQb)(;Jlpq*x(H{&fKWSl>*MMXr^LAPE4cM*&&bQGX9 zicX&=ex%U!rl8Zh!N^a;E0~x!$jRFvNPzg)^g^aPONu`S$?4*viJqPwDF6*3hF54A z3DE7-`nsHg0%6U?xnP<^=+CPc&D35hNzILoXlofTcf+@rX|Y_Ugz-FleULv4hxrD(`XlQ6^ zj+8qvf|SNlbAHTd@aCP>$WK2&jbP_KRL)H>e2e(ux@RVGICYtv$D#!uI|Sq$fW2H+ zlL_G0j`Jpiiw{nz<;Ys*>nviJy03tPM*N@4%S+~Udy+ndu*ZB`^vPraFpAZ#zY8q9 zzSh{l47LwVK=|D8c|?Pxl$6xNULm+^2LVcuA9i)&aSppgXpdI_OL?}5LhL=`Uz%1t%6+K{Q@8NheNIp3^N#zar`zqFD;39t- z2t+Hbv_REL2dVAI6It%C5*ZuY0_vQUoZKIPhcpZfLA$&5YDH#^AZdYfF}*$C?tZdX zVqjpP(v+i^fe$d;?|Y&%m&Ii1`}4Eot>(9)>uwvh8z)PsJZg0wOO{@tY=K1ONVuY- zqa%2#G1VR?dTgY_z@j#)4yd#tIFUO$0YMSl`8LBb1|UX09kW=l1wfL!Dk&)mE)7H? zYOMIYM+%H@l9Eb+IRyyIV6G*C%Y2Y&tP+9@kaEziPHvo>OoAa|H}0YUIZPI4>elY= z7coEFLMv1fu-aZo@gyW9n2Z%O)5#_L2w7QOm4W~6GSE>Got;kJhoOIae$GUITb~t8 zg23LV)_L&=q2Cmiz7c;G#1#%Z0XWnqkg*+*^~C;swE=#O-a)`y>L(_I$2L``AxJ~Z z&mZ^Y%l+qvdD=DjwUFnLl9yiyp!TeVy94$RfkYa``75~@H#F<8U8kn*hH3%u#A2*C z)CuJglPKskIn?zoVFJJvtalas{`8EDKH!W(!ouM0=)qIV#BntCHo;UG#aE9rJl3R< zsD3ITApl_u9SB`uJ;7?gBGBy^&JQ6yQi{mX{=>5u&u!k6shEMz$N)^80Zwi)TKFC$ z-M&f}I~Wmkm;x3fI;{f&01n4Na8*^+(M&L_6kHJ=GC{-x#224B9oYPsZf}_ zf=f$F2c7B}ee5G~5O4i1ed6zZphCoD4HKvy`5?hUFJT8#WuSwI(YZ;Z=_=O-J%_Ky{{`ZgdaCVIN* zW5_EWII}BtrfdnUTif#plS6*`skr!N2WR)K6L&DuogqNOZ@>5=U;ehv{|Z1n>!8_7 z<~HrUUteFpIGj%d{mAfmcWHPex`V(66c^Z1Pu|N2-oNh)8y4A8=vVtm_H3+mX?4O@`d1$lKfZg!85h9!mk zbQJ&@rO>3OejB*k3@i|)e05clevrk>JWh(|T|kM?gSzg~-~mO)hbvt;Ek_HdeF#Jz7FEyvT}IHTAny0?7qs$8?%K+YFu6J)8~|$!v8q1u zhsJLMzCmwqZ!DYcCD3UshJU>M^-CN=P8f?rdDPnOTTjt_IQJg*#c->+#`}=VoS$3W zW2c`PP&CVF6}Xl|=R(Uwkp6-`1ReqMvj_0d%EOspXE!%aScdQ`#No+C4YQ`!$;pY` zbUh{lY>HSOOLDM{(3wf7YLASZhM|w6bA{+w9@s_;!^nRP3UUb63psDz0k8bvS_$l9 zAjtj!0oYVjROqLG-hh4rC?jb8YvbTx*z)17ufP8#bRccS;twFDdTa0jgSITJv!6FWjhuxo!GOJ0bQR3p_ zFVZBgwX9Y-!Mt9*&9hpq+nvDQ09|FNFM|N`m%3o4yvkOI0AXWhX~~pq9r8|d(C?th zA%3Sm#q)$#dT&Tth@6m+kk9r|DtCjL-+it+r?dWNd0&SGWbx`1fV`7Fp*7zKjKR(z!l9rOv z=}r=M-|dmSfB*g_2rMs)%w8)fC_L1zt~4youIcOTga(vz`(zZI%3~~CI>@G$Hw*nRGZ!VtW5AhG?sO#YR%Jh8I09 zpKC-R<+o|(VC-kFbAf?{d4uipSq2m_5V z1qon2K0a%R07`_A)&YuYhYT($j|HjhC$4E&T7_SkkeDClqs+M$?FFF0sML5?&F_KO zM*`P=E?+?&`n4ZlBj~5;ua*)}gN=WCW1vBaPK(a))ZGv=LF5e@6vOsz&S6OmaeeB%gM;z*_xI5fo5zV`Z(kpbl}rL( z1W4EryP{XlrGS9a@bEAie2ZM1+CgoAm37Mp>)OxNSufDn0VIRu>w}Ie^0=WqA!Up7 z2{=M?30MHM0s^1Ph-N!HG%3nQe;j7F~V$(vR>OC*Y7~qQCWFNY(P@z zSfXYJ|49IWF(50S%UB#{9kF^i!cWUC%dduxQ3$z{DT<_-bCav_xLt~Ao8O^ed2VaV z0e1>aDg9Zr^K2+DsgN@pI-wfJ;TIcA3Y%1Np(`G#*1^<#Q^~(~8SCn2(CbG2BqEU#$s!&q zC2ur~hgh|%J76#Y>{QGb0S-pHtWaKTATGT*RrfbZA5at;Cjdd(+}}593LygxAK!bU z`UJmVL^jX~l=bRA zHPz3&E$S(>QMrQ;#q|NlS-yTV$D8p|!uaaz2MoPSm2J^fX|JCX6po%a?H8$C%TaR` z+D*Y;oT%WftE+p!v38qxU1NqRg@iyMf^{l$(Lz28Nwrw`N$}*<==Tzr(1nF8gTNSx zJmd4Ym>7~6R&82(`pn^sDzgCw38GorgzK0uEu2O|<)!9p>w-VORbm#lwq;VLh$0gE z=&e^(<>+7MD{+#k1e={ za_O!+2*2$!P91TY(hf^5_ovUuwcq)WkRSt|J5#uEvIi0j1-0@YTBX)^LBoIetY9Y^ zW*K}fT;WOJmB0|`iyhJNXkcy#bbS_ZS`R5H;RO-8$Q}d#2jByULI;694@;enp8ox9 z-Zz`m0o26m>c7$&m#5-ojP}>Vr?#V=TUKS;ExOzd@a?v?nc=;M_JXg zIkX@E>=C-(!f?KR`q@iyG6RQA`NXoWwK@9^qnif%A@L7ZtT?R&`4jt$)9{2n{=Jh1 zEvt2ozXER@9v-sU&Plz2%VfxA$W3o-$OB)84DdI@Lm-JzaQ$EufC}3d=LFV4avhV8^~$^!9Ct%w0S`1E_aq#ylFU0wLzK=n7<81pvg zqU6=eF$8Vra79(O%SwIYdq&`}*aNS>(wzg&iehf!B$RLvlH-f$gv^oM$1!oPK zIo80OVh8i*xm9tp$37X_xX`F{jsOV9?y`;cR}r;9t>AP6z;!?P+kE40D(Jd?fXe_F z>cZEc0VmL(e>^Aw6yE0a&<2)DH*^mO?}r*&+#-Fj&)?PG&Rs=#*WmQ!ipUpk!+(o+&$G|@ZzKK5H0;UT(i3$SE zZ4wd^$cfunTi2pc$q>*7t5+NGyt@L*GulB#9-*gxA=xVlo`6%g{)*%3u&#?0csS5p zrq-clo}hN$!~n=@Qlz%)>QS8I%LJ7RFxgdJUS7!Qpb5?}G!Fx+CxKyMO%oG| zA3j`%_aiJd(2-2>Umg>0oL~?=PWHD(&GSHrx{63hOhn%)G-gm~lyWs%7kkn+K5C_< zKr9hHa_jqks=eiZq)vRNcH7}m3#COR*%p{^IS~`*Vy?22py3D~KR;wqt!-@bcP$Av z4`;(s+-rjB6oA)C%gPMhCOSLOt|z+7gK@S6rdM6vTUczpV0$6}<1x>^%2A6B3AqYA z6@4~fWQ2cAsAaYH7`6-=CA`X1@CykEp&0XmBPB+ipZv7EGJZ*q}y-#zm3LSkc1547QVUp^sVdw9p2UB9^(px@I^ z0k7*y{?MID=ubD<*x40a$-4ticB$jqr};8K^kCz@%GHPiMTG-SfJ}NEXo3sc04HV8 zXLt}`Mj~y%c=VUcNOM0LCwWm|)R_tj*zU>@L5@;86g)f2?tgRK11-+LNhHwz-a{H` z=GS{P3!1EQGroPX?$-$v?R|K-alJ1N0*yt?&4Y7tn3Z!ibjM3=Wa4?Qv9ji!jKVx+ zA6JVnk+W|zb8z&M!O0GQer%*1q&&`lfrw)WU@Az+BkSG9oJwx3xf@6Le#|ca8JyL) zdHZ%dxT`MHek3Xtj?8j$a{b?52TMoMqGPY6HnT8ua6n7n+#IhKoj6!Zk8xhk%s5;v z=r)1ES^RqfBJT=0YTOVq0x^t2*!3eIg_f(TaKy~u4H6p*O94 zf-3YP*D4Wp^`Ec4y?FcanT#J^*L14IUd9 z+()3T=_1JGX%sWx-`XTS>k=frDgn?|hl|wNc)S$w)F4*Pi@Q9ik#5$dsWEUMQ5WYi z@*+pRurL$=*Py|xQkG#yEIa(~1EInf`Fc;`9Mv0su}RqZ=mrQ)s2MG6xNTI$EVu)z zX#g2R^@k1y6v(UOT!CnX1j2+#E`P{i=mM&$gaMEX4CQHq?MndG9cUpCdVtz$^M9NS z`2sv<0FW&h-Qp#R`vWsBOZ6ZWQZ7`(lwU<5Lo?98}Xv&_*yu2$Je>`T-4}y?vD&%RaQ9Q PiI9FOC!Q;&^X`8D&f%V` literal 0 HcmV?d00001 diff --git a/packages/ketcher-core/src/application/editor/tools/Tool.ts b/packages/ketcher-core/src/application/editor/tools/Tool.ts index 855560fbf2..fa3fe01c14 100644 --- a/packages/ketcher-core/src/application/editor/tools/Tool.ts +++ b/packages/ketcher-core/src/application/editor/tools/Tool.ts @@ -70,10 +70,20 @@ interface ToolEventHandler { export interface IRnaPreset { name?: string; + nameInList?: string; base?: MonomerItemType; sugar?: MonomerItemType; phosphate?: MonomerItemType; - presetInList?: IRnaPreset; + default?: boolean; + favorite?: boolean; + editedName?: boolean; +} + +export interface IRnaLabeledPreset + extends Omit { + base?: string; + sugar?: string; + phosphate?: string; } export type LabeledNucleotideWithPositionInSequence = { diff --git a/packages/ketcher-core/src/domain/entities/Command.ts b/packages/ketcher-core/src/domain/entities/Command.ts index ee944694ed..8deaa875d0 100644 --- a/packages/ketcher-core/src/domain/entities/Command.ts +++ b/packages/ketcher-core/src/domain/entities/Command.ts @@ -32,4 +32,8 @@ export class Command { ); renderersManagers.runPostRenderMethods(); } + + public clear() { + this.operations = []; + } } diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index 6f4612e322..014ad454f6 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -1,3 +1,5 @@ +import pick from 'lodash/pick'; +import omit from 'lodash/omit'; import { AttachmentPointName, MonomerItemType } from 'domain/types'; import { Vec2 } from 'domain/entities/vec2'; import { Command } from 'domain/entities/Command'; @@ -53,7 +55,9 @@ import { ChainsCollection } from 'domain/entities/monomer-chains/ChainsCollectio import { SequenceRenderer } from 'application/render/renderers/sequence/SequenceRenderer'; import { Nucleoside } from './Nucleoside'; import { Nucleotide } from './Nucleotide'; -import { SequenceMode } from 'application/editor'; +import { IRnaPreset, SequenceMode } from 'application/editor'; +import { IRnaLabeledPreset } from 'application/editor/tools'; +import { getRnaPartLibraryItem } from 'domain/helpers/rna'; const HORIZONTAL_DISTANCE_FROM_MONOMER = 25; const VERTICAL_DISTANCE_FROM_MONOMER = 30; @@ -890,7 +894,9 @@ export class DrawingEntitiesManager { if (rnaBase && rnaBasePosition) { monomersToAdd.push([rnaBase, rnaBasePosition]); } - monomersToAdd.push([sugar, sugarPosition]); + if (sugar && sugarPosition) { + monomersToAdd.push([sugar, sugarPosition]); + } if (phosphate && phosphatePosition) { monomersToAdd.push([phosphate, phosphatePosition]); } @@ -912,6 +918,10 @@ export class DrawingEntitiesManager { const attPointEnd = monomer.getValidSourcePoint(previousMonomer); assert(attPointStart); + if (!attPointEnd) { + command.clear(); + return; + } assert(attPointEnd); const operation = new PolymerBondFinishCreationOperation( @@ -932,6 +942,53 @@ export class DrawingEntitiesManager { return { command, monomers }; } + public createRnaPresetFromLabeledRnaPreset(preset: IRnaLabeledPreset) { + const editor = CoreEditor.provideEditorInstance(); + const modifiedPreset: Partial = omit(preset, [ + 'sugar', + 'base', + 'phosphate', + ]); + const position = Coordinates.canvasToModel(new Vec2(0, 0)); + let rnaBaseLibraryItem; + let phosphateLibraryItem; + let sugarLibraryItem; + + if (preset.base) { + rnaBaseLibraryItem = getRnaPartLibraryItem(editor, preset.base); + rnaBaseLibraryItem && assert(rnaBaseLibraryItem); + } + if (preset.phosphate) { + phosphateLibraryItem = getRnaPartLibraryItem(editor, preset.phosphate); + phosphateLibraryItem && assert(phosphateLibraryItem); + } + if (preset.sugar) { + sugarLibraryItem = getRnaPartLibraryItem(editor, preset.sugar); + sugarLibraryItem && assert(sugarLibraryItem); + } + + const { monomers } = editor.drawingEntitiesManager.addRnaPreset({ + sugar: sugarLibraryItem, + sugarPosition: position, + rnaBase: rnaBaseLibraryItem, + rnaBasePosition: position, + phosphate: phosphateLibraryItem, + phosphatePosition: position, + }); + for (const monomer of monomers) { + const props = pick(monomer.monomerItem, ['label', 'props', 'struct']); + if (monomer instanceof RNABase) { + modifiedPreset.base = { ...props }; + } else if (monomer instanceof Sugar) { + modifiedPreset.sugar = { ...props }; + } else if (monomer instanceof Phosphate) { + modifiedPreset.phosphate = { ...props }; + } + } + + return modifiedPreset; + } + private findChainByMonomer( monomer: BaseMonomer, monomerChain: BaseMonomer[] = [], diff --git a/packages/ketcher-macromolecules/src/Editor.tsx b/packages/ketcher-macromolecules/src/Editor.tsx index 17c13408ce..e5bd076cc8 100644 --- a/packages/ketcher-macromolecules/src/Editor.tsx +++ b/packages/ketcher-macromolecules/src/Editor.tsx @@ -91,15 +91,10 @@ import { calculatePreviewPosition } from 'helpers'; import { ErrorModal } from 'components/modal/Error'; import { EditorWrapper, TogglerComponentWrapper } from './styledComponents'; import { useLoading } from './hooks/useLoading'; +import useSetRnaPresets from './hooks/useSetRnaPresets'; import { Loader } from 'components/Loader'; import { FullscreenButton } from 'components/FullscreenButton'; -import { getDefaultPresets } from 'src/helpers/getDefaultPreset'; -import { - setDefaultPresets, - setFavoritePresetsFromLocalStorage, - clearFavorites, -} from 'state/rna-builder'; -import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; +import { clearFavorites } from 'state/rna-builder'; import { LayoutModeButton } from 'components/LayoutModeButton'; import { useContextMenu } from 'react-contexify'; import { CONTEXT_MENU_ID } from 'components/contextMenu/types'; @@ -191,15 +186,7 @@ function Editor({ theme, togglerComponent }: EditorProps) { } }, [editor, monomers]); - useEffect(() => { - const defaultPresets: IRnaPreset[] = getDefaultPresets(monomers); - dispatch(setDefaultPresets(defaultPresets)); - dispatch(setFavoritePresetsFromLocalStorage()); - - return () => { - dispatch(clearFavorites()); - }; - }, [dispatch, monomers]); + useSetRnaPresets(); const dispatchShowPreview = useCallback( (payload) => dispatch(showPreview(payload)), diff --git a/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx b/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx index 422869d659..ec2b89d0e4 100644 --- a/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx +++ b/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx @@ -85,7 +85,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), @@ -112,7 +113,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), @@ -141,7 +143,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), diff --git a/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx b/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx index 569041c99a..e5db6fa7e9 100644 --- a/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx +++ b/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx @@ -37,13 +37,28 @@ describe('Delete component', () => { }, name: 'A', }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const presetCustom: any = { + base: { + label: '25A', + }, + phosphate: { + label: 'P', + }, + sugar: { + label: 'R', + }, + name: 'MyRna', + nameInList: 'MyRna', + }; it('should render correctly', () => { expect( render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [], }, }), ), @@ -53,8 +68,9 @@ describe('Delete component', () => { render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [], }, }), ); @@ -67,8 +83,9 @@ describe('Delete component', () => { render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [presetCustom], }, editor: { editor: { diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx index 6878ae00a9..a29487afda 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx @@ -26,7 +26,7 @@ import { setSearchFilter } from 'state/library'; import { Icon } from 'ketcher-react'; import { IRnaPreset } from './RnaBuilder/types'; import { - selectPresets, + selectAllPresets, setActivePreset, setIsEditMode, setUniqueNameError, @@ -50,7 +50,7 @@ const MonomerLibrary = React.memo(() => { const isDisabledTabsPanels = isSequenceMode && !isSequenceEditInRNABuilderMode; - useAppSelector(selectPresets, (presets) => { + useAppSelector(selectAllPresets, (presets) => { presetsRef.current = presets; return true; }); @@ -68,10 +68,11 @@ const MonomerLibrary = React.memo(() => { return; } + const nameToSet = presetWithSameName ? `${name}_Copy` : name; const duplicatedPreset = { ...preset, - presetInList: undefined, - name: presetWithSameName ? `${name}_Copy` : name, + name: nameToSet, + nameInList: nameToSet, default: false, favorite: false, }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx index 163ca38402..807ed57442 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx @@ -42,8 +42,10 @@ export const RnaBuilder = ({ libraryName, duplicatePreset, editPreset }) => { onClose={closeErrorModal} > - Preset with name "{uniqueNameError}" already exists. Please choose - another name. +
+ Preset with name "{uniqueNameError}" already exists. Please choose + another name. +
Close diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx index 53e4a9a44e..ab2f73f33c 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx @@ -69,7 +69,7 @@ export const RnaEditor = ({ duplicatePreset }) => { const expandEditor = () => { setExpanded(!expanded); - if (!activePreset?.presetInList) { + if (!activePreset?.nameInList) { dispatch(setIsEditMode(true)); } }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx index 1f417cff30..7889cf8733 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx @@ -19,22 +19,11 @@ describe('Test Rna Editor Expanded component', () => { { rnaBuilder: { activePreset: { - name: 'MyRna', - sugar: { - props: { - MonomerName: '', - }, - }, - phosphate: { - props: { - MonomerName: '', - }, - }, - base: { - props: { - MonomerName: '', - }, - }, + name: '', + nameInList: '', + sugar: undefined, + phosphate: undefined, + base: undefined, }, }, }, @@ -74,6 +63,8 @@ describe('Test Rna Editor Expanded component', () => { nodeIndexOverall: 1, }, ], + presetsDefault: [], + presetsCustom: [], }, }, ), @@ -112,8 +103,10 @@ describe('Test Rna Editor Expanded component', () => { MonomerName: '', }, }, - presetInList: {}, + nameInList: 'MyRna', }, + presetsDefault: [], + presetsCustom: [], }, }, ), diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx index cd7f6923f5..32826e2e95 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx @@ -39,7 +39,7 @@ import { selectActivePresetMonomerGroup, selectActiveRnaBuilderItem, selectIsPresetReadyToSave, - selectPresets, + selectAllPresets, setActivePreset, setActiveRnaBuilderItem, setIsEditMode, @@ -48,6 +48,7 @@ import { setUniqueNameError, setSequenceSelection, setSequenceSelectionName, + selectIsActivePresetNewAndEmpty, } from 'state/rna-builder'; import { useAppSelector, useSequenceEditInRNABuilderMode } from 'hooks'; import { @@ -93,9 +94,10 @@ export const RnaEditorExpanded = ({ const dispatch = useDispatch(); const activePreset = useAppSelector(selectActivePreset); + const isActivePresetEmpty = useAppSelector(selectIsActivePresetNewAndEmpty); const activeMonomerGroup = useAppSelector(selectActiveRnaBuilderItem); const editor = useAppSelector(selectEditor); - const presets = useAppSelector(selectPresets); + const presets = useAppSelector(selectAllPresets); const activePresetMonomerGroup = useAppSelector( selectActivePresetMonomerGroup, ); @@ -233,13 +235,13 @@ export const RnaEditorExpanded = ({ ); if ( presetWithSameName && - activePreset.presetInList !== presetWithSameName + activePreset.nameInList !== presetWithSameName.name ) { dispatch(setUniqueNameError(newPreset.name)); return; } - dispatch(setActivePreset(newPreset)); dispatch(savePreset(newPreset)); + dispatch(setActivePreset(newPreset)); editor.events.selectPreset.dispatch(newPreset); setTimeout(() => { scrollToSelectedPreset(newPreset.name); @@ -253,6 +255,8 @@ export const RnaEditorExpanded = ({ resetAfterSequenceUpdate(); } else { setNewPreset(activePreset); + dispatch(setIsEditMode(false)); + dispatch(setActivePresetMonomerGroup(null)); } }; @@ -281,7 +285,7 @@ export const RnaEditorExpanded = ({ let mainButton; - if (!activePreset.presetInList && !isSequenceEditInRNABuilderMode) { + if (isActivePresetEmpty && !isSequenceEditInRNABuilderMode) { mainButton = ( diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts index 3431c304fd..80e51c2673 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts @@ -21,10 +21,10 @@ export interface IExpandIconProps { export interface IRnaPreset { name?: string; + nameInList?: string; base?: MonomerItemType; sugar?: MonomerItemType; phosphate?: MonomerItemType; - presetInList?: IRnaPreset; default?: boolean; favorite?: boolean; editedName?: boolean; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx index 1355646d1a..7140209b01 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx @@ -45,7 +45,7 @@ export const RnaPresetGroup = ({ presets, duplicatePreset, editPreset }) => { const selectPreset = (preset: IRnaPreset) => () => { dispatch(setActivePreset(preset)); editor.events.selectPreset.dispatch(preset); - if (preset === activePreset?.presetInList) return; + if (preset.name === activePreset.name) return; dispatch(setIsEditMode(false)); }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx index 48dd80bee4..ea4993fdaa 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx @@ -9,7 +9,7 @@ describe('Test Rna Preset Item component', () => { base: undefined, name: 'MyRna', phosphate: undefined, - presetInList: undefined, + nameInList: undefined, sugar: undefined, }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx index 41347b96be..5cdd205ebb 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx @@ -56,7 +56,8 @@ describe('Monomer List', () => { ], }, rnaBuilder: { - presets: [preset], + presetsDefault: [preset], + presetsCustom: [], }, }; diff --git a/packages/ketcher-macromolecules/src/constants.ts b/packages/ketcher-macromolecules/src/constants.ts index 193ac74ca1..989461c35d 100644 --- a/packages/ketcher-macromolecules/src/constants.ts +++ b/packages/ketcher-macromolecules/src/constants.ts @@ -62,3 +62,4 @@ export const MonomerCodeToGroup: Record = { } as const; export const FAVORITE_ITEMS_UNIQUE_KEYS = 'favoriteItemsUniqueKeys'; +export const CUSTOM_PRESETS = 'ketcher_custom_presets'; diff --git a/packages/ketcher-macromolecules/src/helpers/localStorage.ts b/packages/ketcher-macromolecules/src/helpers/localStorage.ts index 42a3d7bdd9..e9bf787750 100644 --- a/packages/ketcher-macromolecules/src/helpers/localStorage.ts +++ b/packages/ketcher-macromolecules/src/helpers/localStorage.ts @@ -18,6 +18,10 @@ class LocalStorageWrapper { setItem(key: string, item: unknown) { this.localStorage.setItem(key, JSON.stringify(item)); } + + removeItem(key: string) { + this.localStorage.removeItem(key); + } } export const localStorageWrapper = new LocalStorageWrapper(); diff --git a/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts b/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts new file mode 100644 index 0000000000..426e9282d3 --- /dev/null +++ b/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts @@ -0,0 +1,81 @@ +import { IRnaLabeledPreset, IRnaPreset } from 'ketcher-core'; +import { localStorageWrapper } from './localStorage'; +import { CUSTOM_PRESETS } from '../constants'; +import omit from 'lodash/omit'; + +// Get custom presets from LocalStorage +export const getCachedCustomRnaPresets = (): IRnaLabeledPreset[] | undefined => + localStorageWrapper.getItem(CUSTOM_PRESETS); + +const getPresetIndexInList = (name?: string): number => { + const presets = getCachedCustomRnaPresets(); + return presets?.findIndex((cachedPreset) => cachedPreset.name === name) ?? -1; +}; + +// Save or update custom preset in LocalStorage +export const setCachedCustomRnaPreset = ( + preset: IRnaPreset | IRnaLabeledPreset, +) => { + const presetToSet = { ...preset }; + const cachedPresets = getCachedCustomRnaPresets() || []; + const isLabeledPreset = + typeof presetToSet.sugar === 'string' || + typeof presetToSet.base === 'string' || + typeof presetToSet.phosphate === 'string'; + const fieldsToLabel = ['sugar', 'base', 'phosphate']; + const newLabeledPreset = isLabeledPreset + ? (presetToSet as IRnaLabeledPreset) + : (omit(presetToSet, fieldsToLabel) as Partial); + + if (!isLabeledPreset) { + for (const monomerName of fieldsToLabel) { + newLabeledPreset[monomerName] = presetToSet[monomerName]?.label; + } + } + + const presetIndexInCachedList = getPresetIndexInList( + newLabeledPreset.nameInList, + ); + + newLabeledPreset.nameInList = newLabeledPreset.name; + + if (presetIndexInCachedList > -1) { + cachedPresets.splice(presetIndexInCachedList, 1, newLabeledPreset); + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + } else { + localStorageWrapper.setItem(CUSTOM_PRESETS, [ + ...cachedPresets, + newLabeledPreset, + ]); + } +}; + +// Delete custom preset from LocalStorage +export const deleteCachedCustomRnaPreset = (presetName?: string) => { + if (!presetName) return; + + const cachedPresets = getCachedCustomRnaPresets(); + const presetIndexInCachedList = getPresetIndexInList(presetName); + + if (cachedPresets) { + cachedPresets.splice(presetIndexInCachedList, 1); + + if (cachedPresets.length) + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + else localStorageWrapper.removeItem(CUSTOM_PRESETS); + } +}; + +// Toggle 'favorite' field in custom preset from LocalStorage +export const toggleCachedCustomRnaPresetFavorites = (presetName?: string) => { + if (!presetName) return; + + const cachedPresets = getCachedCustomRnaPresets(); + const presetIndexInCachedList = getPresetIndexInList(presetName); + + if (cachedPresets && presetIndexInCachedList > -1) { + cachedPresets[presetIndexInCachedList].favorite = + !cachedPresets[presetIndexInCachedList].favorite; + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + } +}; diff --git a/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts b/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts new file mode 100644 index 0000000000..9da4f9be75 --- /dev/null +++ b/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts @@ -0,0 +1,72 @@ +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from './stateHooks'; +import { selectEditor } from 'state/common'; +import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; +import { getDefaultPresets } from 'helpers'; +import { + getCachedCustomRnaPresets, + setCachedCustomRnaPreset, +} from 'helpers/manipulateCachedRnaPresets'; +import { + clearFavorites, + setCustomPresets, + setDefaultPresets, + setFavoritePresetsFromLocalStorage, +} from 'state/rna-builder'; +import { selectMonomers } from 'state/library'; + +function useSetRnaPresets() { + const dispatch = useAppDispatch(); + const editor = useAppSelector(selectEditor); + const monomers = useAppSelector(selectMonomers); + + useEffect(() => { + if (!editor) return; + + const defaultPresets: IRnaPreset[] = getDefaultPresets(monomers); + let customLabeledPresets = getCachedCustomRnaPresets(); + const customPresets: IRnaPreset[] = []; + const presetsDefaultNames = defaultPresets.map((preset) => preset.name); + + if (customLabeledPresets) { + // If preset with the same name already exists: + // add '_Copy' (again and again) to custom preset, and update in LocalStorage + for (const customLabeledPreset of customLabeledPresets) { + let i = 0; + let presetUniqName = customLabeledPreset.name; + + while (presetsDefaultNames.includes(presetUniqName)) { + i++; + presetUniqName = `${customLabeledPreset.name}${'_Copy'.repeat(i)}`; + } + + if (presetUniqName !== customLabeledPreset.name) { + setCachedCustomRnaPreset({ + ...customLabeledPreset, + name: presetUniqName, + }); + } + } + + // Transform IRnaLabeledPreset[] to IRnaPreset[] + customLabeledPresets = getCachedCustomRnaPresets()!; + for (const preset of customLabeledPresets) { + const rnaPreset = + editor.drawingEntitiesManager.createRnaPresetFromLabeledRnaPreset( + preset, + ); + if (rnaPreset) customPresets.push(rnaPreset); + } + } + + dispatch(setDefaultPresets(defaultPresets)); + customLabeledPresets && dispatch(setCustomPresets(customPresets)); + dispatch(setFavoritePresetsFromLocalStorage()); + + return () => { + dispatch(clearFavorites()); + }; + }, [dispatch, monomers]); +} + +export default useSetRnaPresets; diff --git a/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts b/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts index 060f8f9585..fe07631f88 100644 --- a/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts +++ b/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts @@ -17,14 +17,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; import { RootState } from 'state'; -import { MonomerGroups } from '../../constants'; import { MonomerItemType, MONOMER_CONST, LabeledNucleotideWithPositionInSequence, } from 'ketcher-core'; import { localStorageWrapper } from 'helpers/localStorage'; -import { FAVORITE_ITEMS_UNIQUE_KEYS } from 'src/constants'; +import { FAVORITE_ITEMS_UNIQUE_KEYS, MonomerGroups } from 'src/constants'; +import { + deleteCachedCustomRnaPreset, + setCachedCustomRnaPreset, + toggleCachedCustomRnaPresetFavorites, +} from 'helpers/manipulateCachedRnaPresets'; export enum RnaBuilderPresetsItem { Presets = 'Presets', @@ -41,7 +45,8 @@ interface IRnaBuilderState { groupName: MonomerGroups; groupItem: MonomerItemType; } | null; - presets: IRnaPreset[]; + presetsDefault: IRnaPreset[]; + presetsCustom: IRnaPreset[]; activeRnaBuilderItem?: RnaBuilderItem | null; isEditMode: boolean; uniqueNameError: string; @@ -54,7 +59,8 @@ const initialState: IRnaBuilderState = { sequenceSelectionName: undefined, isSequenceFirstsOnlyNucleotidesSelected: undefined, activePresetMonomerGroup: null, - presets: [], + presetsDefault: [], + presetsCustom: [], activeRnaBuilderItem: null, isEditMode: false, uniqueNameError: '', @@ -76,12 +82,13 @@ export const rnaBuilderSlice = createSlice({ sugar: undefined, phosphate: undefined, name: '', + nameInList: '', }; }, setActivePreset: (state, action: PayloadAction) => { state.activePreset = { ...action.payload, - presetInList: action.payload, + nameInList: action.payload.name, }; }, setSequenceSelection: ( @@ -127,28 +134,36 @@ export const rnaBuilderSlice = createSlice({ const preset = action.payload; const newPreset = { ...preset }; - if (preset.presetInList) { - const presetIndexInList = state.presets.findIndex( - (presetInList) => presetInList.name === preset.presetInList?.name, + setCachedCustomRnaPreset(newPreset); + + // Save or update preset in Store + if (newPreset.nameInList) { + const presetIndexInList = state.presetsCustom.findIndex( + (presetInList) => presetInList.name === newPreset.nameInList, ); + newPreset.nameInList = newPreset.name; presetIndexInList === -1 - ? state.presets.push(newPreset) - : state.presets.splice(presetIndexInList, 1, newPreset); + ? state.presetsCustom.push(newPreset) + : state.presetsCustom.splice(presetIndexInList, 1, newPreset); } else { - state.presets.push(newPreset); + state.presetsCustom.push(newPreset); } + if (!state.activePreset) return; - state.activePreset.presetInList = newPreset; + state.activePreset.nameInList = newPreset.name; }, deletePreset: (state, action: PayloadAction) => { const preset = action.payload; - const presetIndexInList = state.presets.findIndex( + deleteCachedCustomRnaPreset(preset.name); + + // Delete preset from Store + const presetIndexInList = state.presetsCustom.findIndex( (presetInList) => presetInList.name === preset.name, ); - state.presets.splice(presetIndexInList, 1); + state.presetsCustom.splice(presetIndexInList, 1); - if (preset.presetInList) { + if (preset.nameInList) { state.activePreset = null; } }, @@ -166,13 +181,19 @@ export const rnaBuilderSlice = createSlice({ if (!defaultNucleotide) { return; } - const presetExists = state.presets.find( + const presetExists = state.presetsDefault.find( (item: IRnaPreset) => item.name === defaultNucleotide.name, ); if (presetExists) { return; } - state.presets = action.payload; + state.presetsDefault = action.payload; + }, + setCustomPresets: ( + state: RootState, + action: PayloadAction, + ) => { + state.presetsCustom = action.payload; }, setFavoritePresetsFromLocalStorage: (state: RootState) => { @@ -183,7 +204,7 @@ export const rnaBuilderSlice = createSlice({ return; } - state.presets = state.presets.map((preset) => { + state.presetsDefault = state.presetsDefault.map((preset) => { const uniqueKey = `${preset.name}_${MONOMER_CONST.RNA}`; const favoriteItem = favoritesInLocalStorage.find( @@ -202,21 +223,34 @@ export const rnaBuilderSlice = createSlice({ }, clearFavorites: (state: RootState) => { - state.presets = []; + state.presetsDefault = []; }, togglePresetFavorites: (state, action: PayloadAction) => { - const presetIndex = state.presets.findIndex( + // Find preset to update in default presets + const presetIndex = state.presetsDefault.findIndex( + (presetInList) => presetInList.name === action.payload.name, + ); + // Find preset to update in custom presets + const presetCustomIndex = state.presetsCustom.findIndex( (presetInList) => presetInList.name === action.payload.name, ); - const uniquePresetKey = `${action.payload.name}_${MONOMER_CONST.RNA}`; - + // If updating default preset if (presetIndex >= 0) { - const favorite = state.presets[presetIndex].favorite; - state.presets[presetIndex].favorite = !favorite; + const favorite = state.presetsDefault[presetIndex].favorite; + state.presetsDefault[presetIndex].favorite = !favorite; + // If updating custom preset + } else if (presetCustomIndex >= 0) { + toggleCachedCustomRnaPresetFavorites( + state.presetsCustom[presetCustomIndex].name, + ); + const favorite = state.presetsCustom[presetCustomIndex].favorite; + state.presetsCustom[presetCustomIndex].favorite = !favorite; + return; } + const uniquePresetKey = `${action.payload.name}_${MONOMER_CONST.RNA}`; const favoriteItemsUniqueKeys = (localStorageWrapper.getItem( FAVORITE_ITEMS_UNIQUE_KEYS, ) || []) as string[]; @@ -261,10 +295,6 @@ export const selectIsSequenceFirstsOnlyNucleotidesSelected = ( state: RootState, ): boolean => state.rnaBuilder.isSequenceFirstsOnlyNucleotidesSelected; -export const selectPresets = (state: RootState): IRnaPreset[] => { - return state.rnaBuilder.presets; -}; - export const selectCurrentMonomerGroup = ( preset: IRnaPreset, groupName: MonomerGroups | string, @@ -315,7 +345,7 @@ export const selectIsActivePresetNewAndEmpty = (state: RootState): boolean => { const activePreset = state.rnaBuilder.activePreset; return ( activePreset && - !activePreset.presetInList && + !activePreset.nameInList && !activePreset.name && !activePreset.sugar && !activePreset.base && @@ -330,12 +360,19 @@ export const selectActivePresetForContextMenu = (state: RootState) => { export const selectPresetsInFavorites = (items: IRnaPreset[]) => items.filter((item) => item.favorite); +// Return custom and default presets +export const selectAllPresets = ( + state, +): Array => { + const { presetsDefault = [], presetsCustom = [] } = state.rnaBuilder; + return [...presetsDefault, ...presetsCustom]; +}; export const selectFilteredPresets = ( state, ): Array => { const { searchFilter } = state.library; - const { presets } = state.rnaBuilder; - return presets.filter((item: IRnaPreset) => { + const presetsAll = selectAllPresets(state); + return presetsAll.filter((item: IRnaPreset) => { const name = item.name?.toLowerCase(); const sugarName = item.sugar?.label?.toLowerCase(); const phosphateName = item.phosphate?.label?.toLowerCase(); @@ -364,6 +401,7 @@ export const { setIsEditMode, setUniqueNameError, setDefaultPresets, + setCustomPresets, setActivePresetForContextMenu, togglePresetFavorites, setFavoritePresetsFromLocalStorage,