From 2c09fd1d1cc1fe0c7f5c00de8c51d3e7cd29ae61 Mon Sep 17 00:00:00 2001 From: Jon Robson Date: Thu, 19 Oct 2023 14:12:09 -0700 Subject: [PATCH] Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6 --- nyc.config.js | 2 +- package.json | 2 +- resources/dist/index.js | Bin 47873 -> 48872 bytes resources/dist/index.js.map.json | Bin 217273 -> 220979 bytes resources/ext.popups/index.js | 11 ++- src/actionTypes.js | 1 + src/actions.js | 17 +++- src/changeListeners/settings.js | 9 ++- src/changeListeners/syncUserSettings.js | 18 ++--- src/index.js | 21 ++--- src/integrations/mwpopups.js | 23 +++++- src/isPagePreviewsEnabled.js | 3 +- src/isReferencePreviewsEnabled.js | 4 +- src/reducers/preview.js | 7 +- src/reducers/settings.js | 12 +++ src/ui/settingsDialog.js | 33 ++++---- src/ui/settingsDialogRenderer.js | 54 +++++++++---- src/userSettings.js | 76 ++++++++---------- tests/node-qunit/actions.test.js | 16 ++++ .../changeListeners/settings.test.js | 7 +- .../changeListeners/syncUserSettings.test.js | 15 ++-- .../node-qunit/isPagePreviewsEnabled.test.js | 2 +- .../isReferencePreviewsEnabled.test.js | 4 +- tests/node-qunit/reducers/settings.test.js | 57 ++++++++++--- .../ui/settingsDialogRenderer.test.js | 3 +- tests/node-qunit/userSettings.test.js | 32 +------- webpack.config.js | 4 +- 27 files changed, 270 insertions(+), 163 deletions(-) diff --git a/nyc.config.js b/nyc.config.js index 9e0b9e5a8..13abbd038 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -16,7 +16,7 @@ module.exports = { // Set the coverage percentage by category thresholds. statements: 80, - branches: 70, + branches: 69, functions: 80, lines: 90, diff --git a/package.json b/package.json index 72ccdb869..ef2b4b467 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "bundlesize": [ { "path": "resources/dist/index.js", - "maxSize": "14.8kB" + "maxSize": "15.1kB" } ] } diff --git a/resources/dist/index.js b/resources/dist/index.js index d1aa5f72fedaf19ec135af558f61bbb5f85fb9bc..2f3d6f62ff34fc3fc6de68f48a9ec9b52a0328c6 100644 GIT binary patch delta 6288 zcma)AeQ*@Vm9LR35ex_j0ttiAYQd`+(;BZN9MFnE=wl_VC9PK46+&PkM!UVcgJx$| zGqYM0X%Uz>PPl{^9!`LOZ36KJ*bsYh>{RT#3w3{_E^!sfRpsKURP3v(y1P_eUCz0y z`=~m{_j#}_xtMiey>mbN7+ApTK0o7=jJ||5(J`&A>JT%gGLn7qK2PZ zmZC-AC(iZhGUu`~sIRVt=2aVDsm1ZIENP;~D*`d$R?7)40nM%ToCzbXb@*3r-O2?4 zS_e24K5Dh?jb|3C0trbAQCG_oPLg!BI9pRBoEbaWxV6QZY7Zy0prz=VEyhz~2@7+w zVbi1pbnFOj3X92hCCn=hhrT-`MjZ|+MyV2sTDwE={*Lfckf|f)etj#VaZ$Lv(~E!0cRk}kEd`>O0a;aD%Hm=S zzrC_y=tdauImG zt^TpdK{_c0Rb8WAJw*8ir;sYRW4hEGbMV`?TN~#YiD+3L84|7#t@$`5Y<6?{j;p;4 zkN)HC5&Y|uMrxC6Ua?dv5F^E`;+7hk$x#UoU9aC<^m~987P1DixiicB@bA(b=YyF& zXRC^5@QqJt@Qb}QX?w@-xiUWwvyvTF?OnA}AW2>mEqc`A1sLhKfwQwBZFVjz=ltn~ zo*m^}0DjP?b4S6}e+R$+zP|y#D?ES39ZjF{+~v3#_(|1c@Qs0*rm0NK&Im=>I!U)-QNnREfOjb+6`zKn_L~L4<6_qe)h$d+XRc(+Yt0aW8HoazZ*dNqXTc zhb?Xl{`>GxIS<^CcW}M%pK_hER}4(}+*5sSuY1(f?Fz~!wdH6%9)IUhm#1&6GvFEa z=BA)$dMmO~@0x@I1R+$OPUv9$xhQ{-SBd;n&k8(a4Xv5LoAv_!ZuI3AhhwDcb$)f}h8d+|mP6+0d_ zqIhxqlJX*Fr1u_qi~FLEY}pH*w-Y?14siyIs_mQtr`1H2T)HzmnC1S*W6ijo95c2g zGSk_38lFBvgO(_prV`Qkg%nAoKZ)Hg+b}|cx;94*%O^)@Kp(=)gLx6ks>TDDVlWX) zN%%(M)|Mo5swT&%3)2slIcf$CCC)0D%jig^C+1$OtYn4=QTQylzANv!Vo>GmGehT1 zB#JRP&hv#;SO|0XM+vzw=i)i=$+3;___1Xx>>?U4C|f81EI-z?vp6R9rV=^qc!L$Q zJ4%ByG~}WOt;rOY{M=2p#X&jt(F&_D*5K@tv?*(h91Jk|PQ#Gr#jv5rcn#KC>y{~6 zJYm@d_^)SeP=CC>iXe(03%G3tOu%=KH?KwWDj}NR0&N8QkGC}5M=hyMVOmI-u=KT> zmAUAni;Fy_SNaGXZ>xZrCC3t#uu5r|jqyx_h7D>)U8N>t_BOD*#$|(Q7OQFPR5WNz zpTiSKjvL)(FeD+z<5UX;blwnBBtIx_hhrO;C3qC%% z9-cc<4XIN#@Z%Hfk@AZZ)m#ilo{pBg_$dB1Wkws|v8T6Hu*+}3pFdp#zuwijEGIIQ zpWM5_I;c%DL#FV*yjr#x+&tL@r%zRu;brii9D;v6S%n4mvy*--;uzfAT>7hD*3!V%(r}3QY#Ef}3tlo>pl{5LK#0 ztSDyuQYQU#F*zI>+B)MDzQ9rU;B!brvm{9dtohakIDOhyPYMhVT%W#qTI1?UMbh%( zi!>`bG}G!fEYl5n!(~%hN#&+I!!GUA;h4sBfS;(PjtGs{g%liquBI!0tqg(Oa}n`Q zSh_)dMSJDUg~-{;_$%Ww;-(oS@!@mp+K$o;lyD>T2~f%2H9Q=!6F+_pdb|TuJ>%}d zK|Ar`c+}nJ@dwND$?rf{OOtJB*v?45FZd+ek$&)Gfb=^n6?K^*tG z1Ks^o{y^s_ZhfRc$gCeO47-Pie1q+Eyy z^SH;S28a9l7}d+Dqgh%`_Sq7Wzkhh#PWnqJZnxJ?hLDm|%RiM})Kg08_w=A#6R`Gd zbzLu;2y_Pg`|_t23-K6qoh4i!=x29vf%L0qTe-$@=D}i)WGK|-L!YvAOi3;zu&CUH zxrfInzmQol2)=V7R)nQFE(z{=;lSEwY&@56`?CIzfS>)^9Q<%Xk} zJn*JgtiOgt8#eBhdxG}b@@Uk*AE|E7(_ToF4l2F z@Z`lh@Zd()#jV@~oWIzI_4MP51I0V97>-zx(b8!s^ybcHAR% zsTQYre7x zHeFedXtVi>lbc9SUuopvmCLJ^9>nzkp&zj9|51RLQ9YrC{82sGtLxZb4=UP>3-if+ zeaj~JGdtUPZJo~+wwg(*Qx=%JsR5Zm>ut}WHr6&aFfB2r7u}w9YSL?7MoVj`! z5%rm?HL(1eh!)*-?Fj_#XRba5=dZcoSC`koOIMe{@2)*slTV{I#2M%mgj@#Ei2|42 z>_$la@tck%84GQu{WDoZOH~AWxweCSGPv#Y(g=pKM{+`#yso)8)USNiT@`AD~f%uO`}O0JoDCWPKG;g zxwp&2Byp_jyi5{^H}EG&!U8EN1$i`OgNMJqnSrRK0%$^vDbl0(`eE`yikF3sSW;B= zh?tZOjkh24>G4F|oV0m$Tds22!n$EAu*;;;mVZ5=A@M2Ar&89J1PzK-u*IlpBFwcV z6)S2(sAsdsGaJIA$JP!s3i*lI?*UXl4yNLaVGj5S@@efn7>YVTF_;PfpQ-j1C8kU2jG-`|NeW zlXau?IV7<<3g*@Xm%^|kVs^U|n{4bALU5+C7Vf^W8h&&AEPFS+(K~Rz4fy{ni{N64 zrC78iOlpugDT$k~#5xFk1H>lfk2l112m`a$uv3LPu{MEgKzX8n9d5;QWl{QzwsytH>+Cjzi=hdfa&4^;=r++n^3QdH^n708~k%* zn#_{`3;#?~@G~@t5E(DSw4%{b8cGDIVe$%gCMMFc#Zc#mlu$^;EQ%{-5pTIb{I~(G z4|QgqOS{=tI&v8#Achn(E?dEuD@nhxEn<0}0EOQhiZ(uO5rA<59w zJQc%*nuN^_+9F3*u}B5jyB&K1$`iABoFStE2@{lM&LOH+N{RDYa11@<&$SuY8<%h( zv6nR;$i$ks{N{1I=ByWCbksG)2Z$eC940;zB}r6WoJLU@NGKQ_8_nK#4cb`$kZoSE52Ok>X|Eu zI4@hxTwao|Wv+u+blD37x3?geI&ym(zSdp2y$N@F|MvcsYLRIO8s0X#+?YA=CDh4u zu?#sl!yhFJa>l7Hd)^ZEOdBYWiCjk}bny79h;Pb43wz?FiVfU;_~*AbatEODhZG;) z)gOKk@{7!KJxQvnXh(`gP$vnrGwb@vbcVAK9KwQMl&57aq-$6)aDM*Yc}zVek&ZWr zy@oj)ez^S3PAnR)zf<=GtY(S09Up&Re`jk2J_?X&Muv`eE3p@4U#m|o!}q|Mcm3;i ZlElE(lOl`tqk(8bn~{|C-@Gf9{Vy5Om1_V1 delta 5273 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 aNYa>xYMQrF_gFA)Dl>wf{r)Y!>VE-wgD|fE diff --git a/resources/dist/index.js.map.json b/resources/dist/index.js.map.json index d4c41dea7346057c983e41c79e6e9b2b3b0746e3..5dda57e117594e99c39411ae40740ee820b7f843 100644 GIT binary patch delta 17668 zcmbV!32wpg1k_nLnNbry>+wXmN06`E0K@fr<%ZWkY5%)oy zBpof1*-kP$^=^~A*}UwuP2Ad>c$2nCt0at*ZBA#Kc$+%eZYP`C%_h6s&2(q7sk5EQ zbT-}W@BjZENQk2Bw4)gU_>TYgzpw8-z4$wKUjNdapMLgE^QF7I*FSpa>4EEif7j3M zE9NWtrZ1l=RkJ5uCu?=1oi{p*Yc->xS5iwQBZJXQZ#rE!QcWXfG@JQKwz0XZk!o)a zV!5%oF_+KuPgq00RE~^eMYV0zOQ|({s~gL8qmje69gdzeIy8~08k2Z&74^&-KT9>P9x-Xc~2! zi0dCXR~^_T=?Zt|&7Zry(R8X3%T+t}3V*fU2Ls1QPIXfC3V!4peUn=pYqXY#hxXj{ ze;C?3aI&1w*1^3s)T*RfTYC0dmkSGbB7cO0dRMY*D`?{ls zE265AY`_(wjaO)6L-Qu-XNUo;3mWsg!m4QdTtXE>#bi*O^6{4ZEKLVS81l3$FI2TC zCT4I>a8=}&TuC4@#rWL3MpqwHfl^*m#Zqy}6<5_nfUsTB>PtW?h&e)TBxUaZ=x^OO zs-+`1AL#jUWL~S&Y(^F4cb~oA-2LqC`9aWy)2b0yN)=buGeB7lM;Xh(G)+ZvF*-~w zYIIg|YT6Z5Mb5`$=2NsHR!9K0ehH9$s+oTF^gT&cjB3RN(4#M4d1!hX=RLTq5pkte zv8vCQzx(Wi3Hhs}&tP*bLhxcCoEvVlJc6?T9i9&3Z_LLO@rkQ_l8H1mOw5G^c8OV9 zsm3b_^IIQ1(G_iNFwVFU*=ll2pg}b-p@AdexhOE33j=C6K++O~t_om8(IWkgv}3M> zDw(&kjt+nJ+=~$pML`=i%=zVmd)4q*R-x8_J4$8w^43t?7FNAH}Dhq^Lq&-iJ2*F)Ir}JP-8Twn*3Q5Ks$VR{{ za<6K%X8z{CJ+)M1>lv2JDP6)cG0WBgg$_!~F4L$QXWN6-N%ilsBu-*$GG-wxgnSb| zT3*y9mWYF~Nz?hU!TsxVG#IYyM2$QxIA}igu>7xfV9YT;D|)tm!`EV8bl8EKzSkYV3p*LkU*@UX`K}usA6AiYL?bC zwK4h>@F2Q6ET+kDWQgWCnAb{I7zshzV}LHPE~X|~bE?{KvVRV7qvVrQMd@R4DnPGm zh=(kFuX)=iP9|E+5d%t0k$LqVaZY^)-&@!05iG*w3s zCNuAt|LhY7o~+$CPXSV_krs&=)<75;j!8{XK!~i8l1zujS=*TbY6M0>SY_G5GQ^aY zW&{X;f8Bij6VCbUGG`0^QMRpA+2<;1YVcB-NrdFdX$XoC2z4CWp!BgDh=Q`0#JJ6p z2{ZM{2f7>u$F)95@m5F*!Yq*;E{>2S5WZ`ghd$Lf+f|YSTnxyNV`@-{4Xp~&JT)mJ zt9NrMt!5UywAYvih@iT(z#4kVeT5@zAmK_OO(Q_DK_Xwc5pbY7sU|t&ytXhYwcCd1 ziSW|A*uAJlX_hb9xML2$W}Ws`+cbccNRqLb_kn zcs#a+w&8cfmjqn#K#+7?WGb9Mq^mK4o!L@uij`!CJ(j7#`V0+)t08=dS)Pw^d`L-$ zK=Db9>-0Byu{TXar4Yahug<7ORu~L++EY+;x5hx#guTlw{r*GUI-R42>a0(7&BK}k zikfn~6@qFZV-U<|H?erT=bYdu6`BYWRnhS|%0ooKw>CpzBDpT%{ellp*g0etN}SJOwTh-(l>gx z(*#-xRo0&?WEr7ZcG^O%AX_IFYL_V0G@mMy>`@v>Rj`+}q#v#R;Z zr*7+xGTWmv%Wp{T_CptLksxu$+x!tLPQy_Jb7GwLC38BfT~VTCTO`aLa#2DJcxwonj1`QQgNp zNY*4)jecffMQYZqc_|wT`)w5lYPxEE{nPupd9GgiT|+p}+mi(eB}=)A-joSoyhPj( z89pvjP7ro9MkZO5zr3?xlwJ{|Y_BOOa#5R`S*AkL7vke7nS=qMbd4Z^XTDIRB2c!6 zszdmFT4ifWKbKK}$#6z8(rcs&Rj`46b+ndDvH^`?%1jDj5oZ?I{ma@bz6#y#QvOhs zx`vfl;?_d=P}2nr(q!JAoTVftF~(rL2qORuI03aOW-0X)h^96Plx(?1;GI0$Dks_k zNi?C!@O?!bi_NJZ)g@hZP0PuF(3~sj8quRo4V(ue3t)kdGCJK4x8bKuFc72^X_~UC zpdbf<$c^g92a5+10X0l3=V)b=#ujKSJ`LA)iK>O9PT~0q)pTjqWuQ8D1-fb$(g38K zMxgS@H4w84+BAL?H9i=ACZgPyoe+VSfl)7Ov3UqtZ|+}-2sMx z6|oj*26t2DAN|pN^Q9Dh50q1^IcOm0ID%yv<0=QrLw>Gg@JA=ZAH$@$LUq#$1S86G zr1wd>&BNCp80eUfUq4`{n44)nian7NRiq6R&zl4g>7zlxhbjmJvBatzTr-H(K-g{m zo9pLxRZ%ul@-tz+a{X#@!~hYZAm+hj$dL#lL4ke408N_FLK@0>ewgx0oH~b!kFwwn z7j;PCEjZm~%*Ufpyetj(+w5=bJ$`JD>IM zD#|)KSu_8`XV31JeI__%pcF%2C9KY?Q|4Pf_s}i^hO_5J&9Top_vfiTM!5x?D=cTB z77R20Isfh+UrK57#m^mgBcB5Q78yBrl#q*!@JFCIN0en+WK2*|Ol36`&E~F+kv&!N z@IJ%}2(pQ>>5Yj8#u?H~%{F5?dhsS^na^)bOmk_cCWbl7il9cpsliEhWAPS$#60Xw zj3)D5v~|X>EGr{aa|ka>wXnJJnR`!_v^N)H^7GD%VZ#+Fi$-`|>u{PM8{ztn#X5Q` zv5sG1WvrN={rriEEvY8eu(X;i7Z>C})JPMAgXakk(jbVK(*v-qWZwDW;a$k((Fzo~ zh&l4&!P`BQPxy?s8F}&W-q$6H-GZt&@NPXu%W6pWuE@RL&x7(&FW_U3#WH`=TX9eKjZVJ^@S6= zdP-6^zxsv4qaAAfsEJyJR0Xk=sP!z-7;~?(1NY23zj$&FK?(Lc3xx&q(JzjS^&@j- zyD`<{pe-1x!7eMJv^d5nl^4z5`Qq6K;v3W?tF3Z(DZx2i6oW9difD02FotEFFXtct zc$<L;v2{h3^5(bd4EEZZ2369%=6It|ZO8hN2#RuOMVsE3b73 zj$Y8bZ=~l}*c+g;Lg3L%;eJb=5_-<4X>?-F)s3j3riSW++#zI}y2Kt1Sp^Gq)1&2< zF(5r!klK&r22o?*xTrqgw+IbZT9l&%5bCiBX?4pt2G#N&+xM5qwX11i`u}*O+Y>!{ zivSTPGW)ce3obFC!AxnkJ&gkrb9`9YzwI!0AKIdRMdoEKcBTm&2_{6!9CnbP1l6o4 zYPpiO2je+$B`@8348HA49_lr~wJlZUraC%i#1HB5)W`&%6VIdVN-%rVRV>5rSPtXh z!m4kLGqC77b%r1Yu9|2w43pbfYM3YuQ(qs+Gee`gH=rK2o5(wWHedPDf!$df*<9KD z>6gxS2PGoWxj#e!cYGNCDz2y=HMQTY&Ow~uljg&}KvRr#IUN#94gYJ`ZWUnC)GKlOio?BR1V=o_F~j1Sqr%) zG+yEp=1;zS$WhK~ru(I%N8BWOHSFbjFfvBEt+OCP*$5UK+h=bgrs`K{ZD=h@6J5^H z;()9#Mk&!wa^DPX59)g(T(!8v8EbNjCX%Iw8PWs9A%pC{Er-ig;sxQ1KpHpsB1^8I zuPGMM1&``nr6XwNIh08<(?|}J>XOmDC7rl^pkfv*5sMxlC0M|XVve|i_BdUcq}KW8 zQq#{IX^>pRB?K!kLl_Kq*SC8k(xFbll(&y zU^{0gWEUtWRWTcaKMh{)m_Pd3#bY-900M@@N4S9(?)acP6iUoTzJB%$Fn$9`L$)mV z(K$2n)dR;N4ApRRmbcttu~T3VNfG9K#7FbLe!aS@<>MZ!;WiU5op@@DFO#@O8@d%n z;D~LwFgM8ZMz#VT^*$%0@}owEO*FN&2-{EH{4XyZyHW})qM(cgbT;ua6*nY0Ht{&@ zJ*h6<+Qe6UJ?@Gt!#bKVs)_&r+10hG-+cTV_Z?W3vfDvr2Ug5yzH$G+nEBPOACup^ zvU;QIPc;mxfJd|r9VWNbYhZBs9cd*YEF}1Br+E$cG_aNB!M-G%G9j?TPn(l6)r&N^ z=Nq3usD@f5>`}m?m^78sx=SX@CVO*^^A z8N9Tv+r1tS<-}Oa7EDd0pjrLqA!>C%crzXiNdvKa2k#)4*zRFn^GY9ilYf8_t!gk8 zx@c@@Os*P6AEt%hYnrVN@FQ_GATPn;N2tt4ub;#&8$N9W;hUunuZH3m9o6zJM1pb( zFlxen5Bjt~KB>2?s!q`%5)^D`Xee?4BMogNz@qi5XoHj1Q=P^UMPGm+l0EKV zKu0G7GPdF7fR^CO3CxgCg+JV}k*9LRGEa4;Kx~e;nM;e2d`z_**Bsh{Yi^k7W^^rPc+!D0oIlH@TQL zKt7aq!Z*fD8Pry|ki#`LS+R~<&(1}Ia|n-|_u{gLZ){^V8NB#tbOQ?MIYV(GEVzVi=Q>wl0T@+L}FH$eRD-d*AA2Wnmr1 zbx%-TC6mOamOy74ZjYs?M)I5)2lzrlKGo3>!*mm)#`4sTSks3^ehEgaPHH8`lcCtwZ|=+@5z8A-FNE#%KEQfJ(OOSM?Pil`4L;)5bK_uMTpmK+O&LB|OGCYHF=# zgmr8Wg4Mn}pJgX$G4g}JJw|^L)MEnyUMiFzm$D`cC!>iiq%{pjK{f8;tUb;pH9FwBpqTPY;~nIpzyC6NTyx7iu58Hkyj)aPFPk5I`LSIqOK?@N?+u`HiUO;IPX8DyQ9_~^uThNyhmA&Q z83`0%`*PYE0!IPxQOFa0(LokY%#ADIg}f+2J;{g4K2?V;Bwl7sEo$v0-cjTD8W^L7 zH=2o1DDZ0r>I`tPnp{)mXct>G|K>k>x*aA9QbMm$ozz!(Nko!f2;d1D*g;@C!x5CV zC{8s8poPf0tZBmCzpF|;9(9;DQ@A@ zwX8lxkF0=(sDw!kvk?Kpw*jpa&pHCZa2rq(v4k1qR1(%3QezXoEFR;~HlGnfE67jr z5h)c8Mdy-ByA^*+qpv*IZOA_Tmfx#!=wt%aWyFw>@PvvQF7)7d$M76apW^jkjq)>mwvi9( z*%Z-|*J>0xaih0_y9d26yCzW4`17+1XnC^{qeHDPU<(Z-3`Z7u&=B z?95D&tg|r9PuY01f^>di@^es0Jdg)jB7KhpD>~<{Rn09e@TMiCRm|+Hw$Ai!w&Tai zaZy~*ai2!1Q>Uh734W3(q&5d~CV>t*M5dAN5O#Y`-3UKGh!I;S3tejN!kb+2yT|L14A z@Tz#=4EZ}<7a^d#$%@AHNS?J4ftnl|WP-omfKT4HM?FEIO(WIGiy5?#~~8jGue*vnI1^oC#c63J(9y^KNved$v{k~W7Nqh{7a&I1>7eb(E-#(u&R16Ra;K%~ zlPy34IT&;qJ#hltks&7H;rqm_mXl8^4Dv1XsBe^qw=Y7p0+GXFjqj7>Ebd5qFx)9J z6MB$<2N&4Zp^K6Zjc_~04LdremS!f z7TwRzK%pyaM_Z1Qmn07=)EYS;+j7LTeJ8kOIq+xGZTVo{IM+k5!W?uVQqb^d2W@OX z_Fxm9M*_6VQ!L&aiUp)GTr7z7u2^MaQsZ%sqjuF08b(13#;3+P-03WuIz8HoK^PRM zMVdg)kK6ejO+HrF(MDk>GJ#ZeuWv=>@-{vZh?LSQ+Fj9x0Su3V|Kayf&cb5=EJ7eS zu&@pYq}U_)gy80fW0c^=#>mTxMF@yMU=Y79;rXc^KnhGk6bk0?zufP)3zkv2YL+b? z0R+AO2r*_fAG^6(hb$xKVh~5!^xj;#F664fP+~ z2i^T*Uj!kWKE@2n$wa0ukD%v!MW#WDtcKRPfK0YBv@%rTgc?o>jaVWKkAD*>V+>Y} zvzG|v3g&OT_OBKv4Kg$3T=}bgGkuqm^rVJ9iLi;;6pBS)49)<&S2WoPrtpLzvektK1Bj#1AeTT! zV4nHPU!A?ZBaf(m@CQeCrO7&0MKfo<`v?2{BmA?5pUmwT1Sj1G_k}z>+ZOGcX2FuE zoULFMp!Q!)@P~guBuP<`j>EYdB#5l6(^PsB4k13(Yb(9FAw7vje5X; zdL{<_4CKZkcf6*p*Z=v?ez5Vg9}K*(Je{sq8cpZc`!&vs&NI%XYO9jL<$oTNE@y4) z?Udg8HqNuoi;p^=zFN83lmIf8ddP9)$eEoNU%7QD! zr`Cp+>(%lZ>jU%3e(NVe<*sASvqNdh4z>&p7S3c3jr~3n~%o|GJc27wsWB zg27#FUUO}IfBIVQ`&s$rEHAv~*^QU{%EbZe8;Y{`z|H)jv#dY-8)ct$_p}n;zatN= zuYO3mcegwR{CwK_^0ZQXglVt`FZRjhR}qSJ_ZL7zDx@6L7L0Qw(|oFwUpJi1oMDsW z?Bt9J|4y}2`4YW6Pe;NlFnLhaWWf()v$M1ICh~?F$eRir=Gb)+ID%RP{`u1-s-!<39 zQb=*{wmz&Yd#%5SE00-U`Pa(r_wHa?*8A0g`>lmb$|Kz!W_wsjt4rsqdAy$mHbs^S zM!Go!Su}QJGt8-+s+|Ej`3zvdcx}7dd&c$55aoCBM%wW(}GQ;yd31RYK zr}bDux$j=77dK5DwDw+Bc3ICRlmi1#(aM9K(@*C!*Pe2o-G0QIoN!#8a4uss()E0e z{L#**?YB-Ol%w`(^L$$Wrv8)j4Usd_MI&>5Ctvjh@#jf-2utrQJjKRfQhDE7EiGFq%F4z8L;15ite=iK zZnx&YGqB70)8 z(AS>YaTH0?0Zae6Tm#hx!?3wUptP0Ec>Ohj!H9p^{O_HgnDs7q{tc#prD9@6y|wZwNr* z(C0j$n&jy!t@4slcUG6TJe%#*oQXj_clNq zTnoSYdB%AIZns9wy3bO6@sR6RTfVXH!^-e&rxx@;Ft(^KyfdaRE(YfU=baC2AKJM1 z+sdIktUZ4>@YW*@X9~V13wJ9;+@q2xvwjv=PFnB(>w()BDyj0OYj^UGPA;F$Ir9x? zs{yx-)lU1ZPG_o;ab}Eqz74{hsYauk&Xa4uT7juM%SNMt;~GPj`@I7P9=3Im;1IKy z@v?2JZuBCvb7>8bmQ$@#^D*l^FDtHN+umgbCxMu&x88bv7@_r*mz6utZl~GSZeA{B zV;^n(=$V22R^%1s?Hlj>cgo`38+*Q}eDVRy`I@qC?{*+o>Q&{Ob=QxST~FAm3i9&V z${FX=*Ib|Oqn&eTVJV>!*|hy2N#d=^OBV6RfRw&%i+hUsE2iop=9B z)Gq5iKUMZk`KwjDdOVx26wliXYUHXdm^NNF-kNj1>s`(hHivq0sa|v(w4VK2`cDFFN+V`_`>2jNm*`HSke@X`rfIwzWIL! zPFVx-fqj-gq3k_)`e_16QEbRq#p~G(PB^Y@{Pp9G?+jQ!Rvm}l7L%Ej)xN`V&U)X= z${y<_&9U+plCbs8zT<|wHyxQ#|W9+@>joiul`MBeR)t+?Rz42b*_{gpg^P!luvSMCC^GZ+s>EjFUNnXtt$rSDV|Ak&A9X=~!cEr?FPsz#mUc;w|g)ro7 ztl|F#D6{Ob!5_RF*3Wao1_egK#zBn|;ta|M=a{o&7)AK%eA(bF{rWbv2Q_$^M_(njem^7s!Mq?ID6J#oVN(xl^z z_2hZSuHB3bPg}>&I|9eAHc9wH+yDOrUH-ogGX0vIR|C4sxtuCNv9?m4&4j0S=7O=j z?c8@#)-JT= zep&fl?4=SzSp&IfAJ8qX+hw)A0l8nLRVrEkd(u%j)Fb027j9Li92ZcK_x`^RTe{YF z-Ht(NIXhpdZd9dCb&^Ww#eZ0B#Y9isK~M;wn@rOd!? z>*7VndF$`P%AUt0UcH*MkE&mdRo33~4$tnbZR9N$oi=e4Z<@qa6UK=h$Bvyenb>PLyGa_iv72-zZKvHj-R$n{ zcDmi)_Z^TTC0UsaX9(Q$_|Er!or^19-}dY$w|()+ZC8G-e=x5 zX4z%_mw;uH^O*fuB{iR_q>72uL?uxRTxPuRe(>;sN8$v%IEIgYU!<&DyfNhdJRGS>cKQMc`X6P-`bzQvT`=xb>&Mv z+q%q04_bFy0oM%evF_(Duf5m0$Jp=qhVhS%pBw!>XN^zwY&ZUE-!He?k11*!@k;T^ zx1H~tv}BjBJRAFGN0-O=+;8`Gd9M7`Z~tsdSD*3XM<2z5FuQLb&yUR6!Meu;LW}G6h$eHQ7`6K}Es*uooTpdPQ9gUO&}6bSX^&yyFypc|)h{)@ zCK%x0*m{6odx`*7HUbQEAeMl^yFgrUYmvGD0po$jBp`bHe!?)6pCvLw^xZ?lbNw+R z_Qc2cjYUE@UaN{xBDP29v?4M*Z6rT=^j2?ObVrPBPoCUGlmtKnO=7#u2uIQ=r|&-2 zn5VH|CT!0NOerxe+&>q*cgxem_be9-4e*WY= zo0o$2geF_MAEzB?hak0XwHOmA&6C%GWMEtZ@OS|K0^V|zpieT4T3ECG(>vrH90pTI1$O@d|6100j`1^b}S{TwoEwM;TDw2p3^V-z>Ge>WY|A#Z|7xEEh#eB`e6826Sgl(**k(ULbt$E@$w8!d6yWU43GKh z9vbs3&Jd}gN(4fN&6AI)Fgcz=h~?fGVg0Hxh;_@2AAb6TzBGe1o~gQ;Y|PO9usWLb zURK1in(Cb)!s2emN|GCC35S~MGah{EfWVtWk%^ZZ=Lj8^ECL$ zS2v#c%pth%=Rb3NxJ+8;))!eB6U&e{F+c5L-@3?p9(H(XFjOQ;y?OF9Fht|-%eK9k z32A!Sy&ND^LJJHf>@X%S@7o0RB@&%R`Eme5^)%FpS|baoStN%k%4Ny0)znALAVI0c3I)fx)AL*_U{!6y)|Js_Xp58#uY2^HxO?3NO)&SZaQa*S9M zr649?d}jlhO|dz)}0_67DbuDgM>YhG!{un`WSPXrgg>#xM5qk<)<|@ zFH&@W_hXER!Nh2UDcdE!iETsglTSnDS{d?^aU_qC#F69s$Y#9~h+d3u4fC z`srhBC7e{x44c)^46yUKlodP;5;VH*P@e@hwZ%MLEWdQ)I>mr-4mynZ!Bj>z^i$!SUkAU?#?Rb`zd@lE#)q z%0q;WEE^yF&$fx~6oo`N;^1fzFSzXsLi0`!v5ABu9qu8lAiH2H7R2yIG66U*hdN9e zHg5ae$#ys88v|mUpx7QDC}5T+^PcJzgCvJ=S@g1>jwCUs#Te^wZIZp(Vi;Zr6tkwq zlu6asrV?6m(n)($69D1W<|b*KoNmpwF{pW>c8cF@_Cql`*nubobBtF=f3sq;$=Va{ zKdGw{5aJ(^T$Wq3$tW} z3qmRVDnO8w8ga;)^w=XFFX^~U<~ch{AW0(`T~%(x2hg#OX__}PMni!DMQrI;3&M!z z=IZ9n1C2erS z&c+lpN*~W{P&1qyy9LTxF~0YOW9~iQKC$n2X%+rOOl`>Lets_2h$v4|2N&>#3+KIfqd7(C|sO3 zBl~$ttf;d*vm2;^C4v5y9O5iJ27balyB8FT)^Gu8J1;!u|bF+MggVK(j{ zA5Hll)U-u_+nwYS4>)WU4NCqckX1wUpQHYw$aBu1s=1mQ^1)nO+ItaBl1_$tlU-c0 z)!e9VXqRqZz{XqkkVCkQk6+o>mZaCCl&N^#P*l$7eIu|7KP5{+8k?Xa!lSUrDN0DA zI{AVb!Zxm<3W8C2G$ajxGf!~R$j}i|GAP~1>+(cS&koY^yKU!mk4rvge0J&G*yXgi?gz|G#AB;%*ZU_8wH+X ziU9aZDRPZPph_iYMGaO{1OYWd-B|e2+06yGZYsN7#wWjYVb4;Ev@0hk!0R+pC_=sE zH>SU=AFZ5RhKUt-j$O_eg$CTcUML>4o$n&d{#jO^yz%N+kG8MM zGNuKT;zP>7i~2=cFb{fxahSh?W2K0(?%X8;8XAvm5gf#AZ?$qI%>SNyc_s^Z!G8jY^1BMO- zjW0ZRG_tBq`3Mv~Cw%#_>rRHjg5S|h5+6v1m}!kU8ee zl#^01tuqHH!pY=2a}=~N2YEoJT_V5g>JcC3$RBEP5A})UY%9d}-e9McD02G%<#k*0 z7UdEvIFMj>m=!2@;Tv;OwJDKh(HVM9EZ@w2V(erF*j*)1_H#=iOG&5}V%MW3pjJW6 zR}?1M6Lc7VTVDDDq`qwjqvu;KRRN^IQ09MWu}NqIvp*kX8*0mKlB^)$;MrI zaeCeWs_Ssp%yo0LJCWb zr;%+qm8H=dBt=w<$zmmH4qR8s01|tgQ502f%!N@g5!MJm)aw$}ab!7QcuftXPL69{ zI2lk&u2&1r*}d+XoEn_4hjcCv5y}R*?pBm1>O}cm_&U-_N=!MpO`R@qNlx{;DfS?X zp@<}B(blB)cbuC}a;`SQJgSnI-Gy>Uq)M!v4If`SVAY#`RRtj@@du-X<>R@0y`0LRj6yz%nRv;3sR z9IQG)tia}fyMgvGt%isKH> zv##NIbDZ^#E~J|v02iWWdV599CIm#p}>Qt)&$^krOri80Ig?ejt9Bs znx>)w?Hw8Pj!YpSg*`f3X+f0NS!sBgbanFGRkh(>6^fko8GrfheS7Lkq8klWY2Aj! z6e}p%>eytHIpw(d62&KSlQ1|kmweuM=DWMv&`!D*lbl9o1()k`erR1ku zLw-eywGd+m&_bAm-bXv5fJJV>fl<_ddA+2fV~V>6at>uVCr_Nvbbz*hU8p&|s-Pyg zi6Cch1eJh7H9@UQ+z_`KSG1wN%t5A2sl-UKNuFC@vyMd3_~LhW?}3j%v4^e~s?DnmFy8#`{{5ID zpR^!~ut)H0Q2D{Wj4aiy7I}W|9=u#}Cxig}Pc$|UB1;9m69zwBAG0TO1;A9#~LZ@pURfw0EjS(m& zsKG@}oOi&_;L+1hx0(_?alR!Ot8(6f_W^iKnw?3wYc-lYB?{2}S6toeGy5J%LCT%?fVUX&Glx|e));i5hX zBNoKMoZtmoVv~Ydiy2+tw~Z{tz*aeKZ%Qpmj6>5ARWIC`pWr~pRu|yrJtwNPVnO7u zU1Frae~C(?^@Wi3$}&xNuC`L!M<{MnXcdai1Nv{VY|3aq4(! zC2lJWLO)2X8RXMm%|XfKlE4LWMplttHifo~tLHF!J|$7%h+YfUrHNXk@C8y}4C)p#RoO;gRgon^avi`}k z0yvbk$MlSUSUiMt8e*JNnpGJ^GpO?k(X9H64bxo`EFFY~s8uyNxu6O>x@gDcH(X$k zK&u{9lAG11>Qq=ttxz^T_QTT;VS8XCJvN3=scX4WzH00fc@@$EFoi9PL>U%iI<4bXY&&271cK*O0Bb9h68fdzhjDv26VKN3PKl?=Oj# z+OMPpRp9C!8VSccm*>2a`ng4s^K;dimOUIlSA{%m=zkH9uWAPfX^4Vo`toF2z;8e} zHsIIMsjI3H)<#e=;jX_^c*dy>46+&&{YK&FP*csilE&k2?J_q1cvl-ERlse?4BR+w zI30Ag*3g4l_UircA0QPH!vDI*-C~a)%6h3oF{e?i$2B7y8=PESlvN}MlV74NV6vA| zE04E_Jx&U5Fi?>16@4tNFceqsgGg%$O3RkrX^^Ndim$F=$E)0l4-HNmfj5u0xl13yO9XMsb)|~$ z1vLs8Q%n)V>RqTl&Wl6nw;+wK;0_cHrqlEfx!m?BB;gA|A;7LBk>>(=RZWJ->XyVZ zX-g6PbhLG4iG+=YKjxsDS<&JmIZ@!d5-1^kO7!P>IF;j$^n8Z(t)X0Y5alkkFD-gT zDI~>)_?;-HyN8FbT~vt^jXiQPM4LLZ?KK!s5lUQe0Y$QMJL#a#rNi2qVNQHQ1B_Bu zP^%WV|G0y@eyMY$lg&|{A#u+MhjSgg<jrpJ4(;i^`fG>pHCx@|_thpPX(7=gunA6Z9I$TcSQj_?~mpo{X za8puU6R_SnJM74E8w4%Y5_}u_K1yKR_mR_n_H8Jq(Bnjz797;HvKucgfrd3Az3iZh zvP>5eN{y~T9=JQ-Ix*lxP(T6i?UU(=y6jWU+iry{fD^L(Qj(nR(G^poxS%$E=dHur zdw~L#%v6xe=}a0`3q1YnZ}}Z*SrSFD#J&6gy*mcfA}AdYRpF;eAK*bS7Fx@u18<+Q z4vW0ec-wa8kjMtz1vCla{dz^wccX%W@>R0N@L&IK8`lKz?&$rXgrcrb zPOU9qEniSznC{}x0w)k946_r@B{A%Aqr;2?qGaz`jE=goN?44ic>hwC6_sZ#0%2^uTP?}f=v@cG=F+#Q;Dwbv}n$F`lp9B z&yl$;%Uara{inNiek$WhnItj>RUfmj@8j9JtY0@vUR=!xXa%w*67i$G-z3z*O0iAJfb3q!>9uE*YsqAgMut;OH^ z*w0oz_Oq_b^XB{mU3ZyV0+vN{3im2IgyUI@3% zWs6CA#r#4Mf0#CZGG-Y&Lf;alVzp-LypV2t!1nMX_o*dVbR=f6nLFQax!WxHESqn$ z+fLh3dGncvEj!K6`7FcckH2HtYJU4M%U$N&4=sCku5En8cBb>4&e?B1>9@Eo>d^hB z?zc!&|Bhvc`8yMqy(`H{%YiNC|43Pmn1{Oo_`@OV7PI(3*KOwSU9h~*{Q5JN%{OD~ zCw|j%z`W(6<$nDpTs>oZm}pX@CNiaLB2@*c?^lHK9I+ysJa4|CLrk^@tebBs#tSL) zjY|Ok;128Uo62~L{*>*E-Te7OmP6*7mn_?P{49;1O{S`eO150fmWo)Tz&>KXG5DS4 zV=l{%YrvZa9#9@49=OLA&znXYf39VQvHVdVts%@#7 zs#I->c=1%t7Egd5Rr8A<>)O@d$fSz4S|-&zRY}>B*((1|K@kF#nhLsu=WS?f;x`Sp z)ulEwzSnxUd2+Ay#6h*P!{T$*avrQcZFAUd4%->?7x#AUTxnjiv~Dv;cUk{z!5W<*XfmW3zqk7Gu>9}{UrTO=A$39>|FWqvgL!8yEgm;;`D{0S$Np8f7hw9 z`q9O?j_#g2W$V5wKJ#U0Y47#Fd{79ks^_lIbp?Jp-fzpPO3%@@CJ*=xR5 zv20p#{)XkqQS+T#*RAHiK55xvKJf|5PggEKVd=T`%b&LF+`94ye`2|~eam>MmO5_> z7V}LK!|uLf(w0ok$CvUoTZc>*mTm5;P;BuU1eLAY%B5_vc5daLzG?aLN%I31>)w@r z_xF~^H?Q>k(h|DO{PTaY{GItym$lpc$uBKiRxbUkx+l7#cVAb&*z&Rs>m-W$}ZI2PfXso_2T)}feSVK803KYRhPBf z+L_+Dby6SEC-rfu+b*6bz7DOcclyfTyx02h4)cru$+F3O+Y5pGVaVENG4J?b*H-hg-@47rMXlS~ z%w*U_zPaWK>1Q*x)gShpQ`pt zF3fgs2c0HRfsL;Eb?hpbFFw|_(|kG#A=B@AZc{tVfBM_5J=@gYp3V?Wo8P?My3^b; zWj(#gcJzS+|HaX@**rR9J!t*{ z)R@Pk@b%>x>#gR0j9E{bnJMcDb7i}AhZ%@iHyy90YNu@znn%AhF{w{`^=SB#KCTJB z?!M&HV|GTyF#`Nbk9F-fKO40k=U*t@ge3fc>45q2?Kt3nO { const previewState = getState().preview; const enabledValue = previewState.enabled[ type ]; - // Note: Only reference previews and default previews can be disabled at this point. // If there is no UI the enabledValue is always true. const isEnabled = typeof enabledValue === 'undefined' ? true : enabledValue; diff --git a/src/changeListeners/settings.js b/src/changeListeners/settings.js index 6a4d37f01..627a5c0bc 100644 --- a/src/changeListeners/settings.js +++ b/src/changeListeners/settings.js @@ -13,12 +13,19 @@ export default function settings( boundActions, render ) { // Nothing to do on initialization return; } + if ( + settingsObj && + Object.keys( oldState.settings.previewTypesEnabled ).length !== Object.keys( newState.settings.previewTypesEnabled ).length + ) { + // the number of settings changed so force it to be repainted. + settingsObj.refresh( newState.settings.previewTypesEnabled ); + } // Update global modal visibility if ( oldState.settings.shouldShow === false && newState.settings.shouldShow ) { // Lazily instantiate the settings UI if ( !settingsObj ) { - settingsObj = render( boundActions ); + settingsObj = render( boundActions, newState.settings.previewTypesEnabled ); settingsObj.appendTo( document.body ); } diff --git a/src/changeListeners/syncUserSettings.js b/src/changeListeners/syncUserSettings.js index aa2806a95..2323092f9 100644 --- a/src/changeListeners/syncUserSettings.js +++ b/src/changeListeners/syncUserSettings.js @@ -2,8 +2,6 @@ * @module changeListeners/syncUserSettings */ -import { previewTypes } from '../preview/model'; - /** * Creates an instance of the user settings sync change listener. * @@ -22,14 +20,14 @@ import { previewTypes } from '../preview/model'; */ export default function syncUserSettings( userSettings ) { return ( oldState, newState ) => { - syncIfChanged( - oldState, newState, 'preview.enabled.' + previewTypes.TYPE_PAGE, - userSettings.storePagePreviewsEnabled - ); - syncIfChanged( - oldState, newState, 'preview.enabled.' + previewTypes.TYPE_REFERENCE, - userSettings.storeReferencePreviewsEnabled - ); + Object.keys( newState.preview.enabled ).forEach( ( key ) => { + syncIfChanged( + oldState, newState, `preview.enabled.${key}`, + ( value ) => { + userSettings.storePreviewTypeEnabled( key, value ); + } + ); + } ); }; } diff --git a/src/index.js b/src/index.js index e2ea80444..4a6cf527d 100644 --- a/src/index.js +++ b/src/index.js @@ -191,15 +191,11 @@ function handleDOMEventIfEligible( handler ) { referenceGateway = createReferenceGateway(), userSettings = createUserSettings( mw.storage ), referencePreviewsState = isReferencePreviewsEnabled( mw.user, userSettings, mw.config ), - settingsDialog = createSettingsDialogRenderer( referencePreviewsState !== null ), + settingsDialog = createSettingsDialogRenderer(), experiments = createExperiments( mw.experiments ), statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ), pageviewTracker = getPageviewTracker( mw.config ), - initiallyEnabled = { - [ previewTypes.TYPE_PAGE ]: - createIsPagePreviewsEnabled( mw.user, userSettings, mw.config ), - [ previewTypes.TYPE_REFERENCE ]: referencePreviewsState - }; + pagePreviewState = createIsPagePreviewsEnabled( mw.user, userSettings, mw.config ); // If debug mode is enabled, then enable Redux DevTools. if ( mw.config.get( 'debug' ) || @@ -224,7 +220,7 @@ function handleDOMEventIfEligible( handler ) { ); boundActions.boot( - initiallyEnabled, + {}, mw.user, userSettings, mw.config, @@ -236,10 +232,15 @@ function handleDOMEventIfEligible( handler ) { * extensions can query it (T171287) */ mw.popups = createMediaWikiPopupsObject( - store, registerModel, registerPreviewUI, registerGatewayForPreviewType + store, registerModel, registerPreviewUI, registerGatewayForPreviewType, + boundActions.registerSetting, userSettings ); - if ( initiallyEnabled[ previewTypes.TYPE_PAGE ] !== null ) { + // Migrate any old preferences to new system. + // FIXME: This can be removed in 4 weeks time. + userSettings.migrateOldPreferences(); + + if ( pagePreviewState !== null ) { const excludedLinksSelector = EXCLUDED_LINK_SELECTORS.join( ', ' ); // Register default preview type mw.popups.register( { @@ -257,7 +258,7 @@ function handleDOMEventIfEligible( handler ) { ] } ); } - if ( initiallyEnabled[ previewTypes.TYPE_REFERENCE ] !== null ) { + if ( referencePreviewsState !== null ) { // Register the reference preview type mw.popups.register( { type: previewTypes.TYPE_REFERENCE, diff --git a/src/integrations/mwpopups.js b/src/integrations/mwpopups.js index 1c4f5ac48..034b14fa3 100644 --- a/src/integrations/mwpopups.js +++ b/src/integrations/mwpopups.js @@ -4,6 +4,14 @@ import { previewTypes } from '../preview/model'; +/** + * @param {string} type + * @return {boolean} whether the preview type supports being disabled/enabled. + */ +function canShowSettingForPreviewType( type ) { + return mw.message( `popups-settings-option-${type}` ).exists(); +} + /** * This function provides a mw.popups object which can be used by 3rd party * to interact with Popups. @@ -12,9 +20,13 @@ import { previewTypes } from '../preview/model'; * @param {Function} registerModel allows extensions to register custom preview handlers. * @param {Function} registerPreviewUI allows extensions to register custom preview renderers. * @param {Function} registerGatewayForPreviewType allows extensions to register gateways for preview types. + * @param {Function} registerSetting + * @param {UserSettings} userSettings * @return {Object} external Popups interface */ -export default function createMwPopups( store, registerModel, registerPreviewUI, registerGatewayForPreviewType ) { +export default function createMwPopups( store, registerModel, registerPreviewUI, + registerGatewayForPreviewType, registerSetting, userSettings +) { return { /** * @return {boolean} If Page Previews are currently active @@ -69,6 +81,15 @@ export default function createMwPopups( store, registerModel, registerPreviewUI, registerModel( type, selector, delay ); registerGatewayForPreviewType( type, gateway ); registerPreviewUI( type, renderFn, doNotRequireSummary ); + // Only show if doesn't exist. + if ( canShowSettingForPreviewType( type ) ) { + registerSetting( type, userSettings.isPreviewTypeEnabled( type ) ); + } else { + mw.log.warn( + `[Popups] No setting for ${type} registered. +Please create message with key "popups-settings-option-${type}" if this is a mistake.` + ); + } if ( subTypes ) { subTypes.forEach( function ( subTypePreview ) { registerPreviewUI( subTypePreview.type, subTypePreview.renderFn, subTypePreview.doNotRequireSummary ); diff --git a/src/isPagePreviewsEnabled.js b/src/isPagePreviewsEnabled.js index 7e2cb3936..f76b11f53 100644 --- a/src/isPagePreviewsEnabled.js +++ b/src/isPagePreviewsEnabled.js @@ -1,6 +1,7 @@ /** * @module isPagePreviewsEnabled */ +import { previewTypes } from './preview/model'; const canSaveToUserPreferences = require( './canSaveToUserPreferences.js' ); /** @@ -28,7 +29,7 @@ export default function isPagePreviewsEnabled( user, userSettings, config ) { // For anonymous users, and for IP masked usersm the code loads always, // but the feature can be toggled at run-time via local storage. if ( !canSaveToUserPreferences( user ) ) { - return userSettings.isPagePreviewsEnabled(); + return userSettings.isPreviewTypeEnabled( previewTypes.TYPE_PAGE ); } // Registered users never can enable popup types at run-time. diff --git a/src/isReferencePreviewsEnabled.js b/src/isReferencePreviewsEnabled.js index ef5021c7b..d47728ad1 100644 --- a/src/isReferencePreviewsEnabled.js +++ b/src/isReferencePreviewsEnabled.js @@ -1,3 +1,5 @@ +import { previewTypes } from './preview/model'; + /** * @module isReferencePreviewsEnabled */ @@ -32,7 +34,7 @@ export default function isReferencePreviewsEnabled( user, userSettings, config ) // For anonymous users, the code loads always, but the feature can be toggled at run-time via // local storage. if ( !canSaveToUserPreferences( user ) ) { - return userSettings.isReferencePreviewsEnabled(); + return userSettings.isPreviewTypeEnabled( previewTypes.TYPE_REFERENCE ); } // Registered users never can enable popup types at run-time. diff --git a/src/reducers/preview.js b/src/reducers/preview.js index 78fc63985..321c51337 100644 --- a/src/reducers/preview.js +++ b/src/reducers/preview.js @@ -28,7 +28,12 @@ export default function preview( state, action ) { return nextState( state, { enabled: action.initiallyEnabled } ); - + case actionTypes.REGISTER_SETTING: + return nextState( state, { + enabled: Object.assign( {}, state.enabled, { + [ action.name ]: action.enabled + } ) + } ); case actionTypes.SETTINGS_CHANGE: { return nextState( state, { enabled: action.newValue diff --git a/src/reducers/settings.js b/src/reducers/settings.js index a921201c1..0f3a50e04 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -12,6 +12,7 @@ export default function settings( state, action ) { if ( state === undefined ) { state = { shouldShow: false, + previewTypesEnabled: {}, showHelp: false, shouldShowFooterLink: false }; @@ -57,12 +58,23 @@ export default function settings( state, action ) { shouldShowFooterLink: anyDisabled } ); } + case actionTypes.REGISTER_SETTING: { + const previewTypesEnabled = Object.assign( {}, state.previewTypesEnabled, { + [ action.name ]: action.enabled + } ); + return nextState( state, { + previewTypesEnabled, + shouldShowFooterLink: state.shouldShowFooterLink || !action.enabled + } ); + } case actionTypes.BOOT: { // Warning, when the state is null the user can't re-enable this popup type! const anyDisabled = Object.keys( action.initiallyEnabled ) .some( ( type ) => action.initiallyEnabled[ type ] === false ); + const previewTypesEnabled = Object.assign( {}, action.initiallyEnabled ); return nextState( state, { + previewTypesEnabled, shouldShowFooterLink: action.user.isAnon && anyDisabled } ); } diff --git a/src/ui/settingsDialog.js b/src/ui/settingsDialog.js index 3c9212713..61b15f6e8 100644 --- a/src/ui/settingsDialog.js +++ b/src/ui/settingsDialog.js @@ -3,33 +3,28 @@ */ import { renderSettingsDialog } from './templates/settingsDialog/settingsDialog'; -import { previewTypes } from '../preview/model'; /** * Create the settings dialog shown to anonymous users. * - * @param {boolean} referencePreviewsAvaliable + * @param {Object} previewTypesEnabled * @return {HTMLElement} settings dialog */ -export function createSettingsDialog( referencePreviewsAvaliable ) { - const choices = [ +export function createSettingsDialog( previewTypesEnabled ) { + const choices = Object.keys( previewTypesEnabled ).map( ( id ) => ( { - id: previewTypes.TYPE_PAGE, - name: mw.msg( 'popups-settings-option-page' ), - description: mw.msg( 'popups-settings-option-page-description' ) - }, - { - id: previewTypes.TYPE_REFERENCE, - name: mw.msg( 'popups-settings-option-reference' ), - description: mw.msg( 'popups-settings-option-reference-description' ) + id, + // This can produce: + // * popups-settings-option-preview + // * popups-settings-option-reference + name: mw.msg( `popups-settings-option-${id}` ), + // This can produce: + // * popups-settings-option-preview-description + // * popups-settings-option-reference-description + description: mw.msg( `popups-settings-option-${id}-description` ), + isChecked: previewTypesEnabled[ id ] } - ]; - - if ( !referencePreviewsAvaliable ) { - // Anonymous users can't access reference previews when they're disabled - // TODO: Remove when the wgPopupsReferencePreviews feature flag is not needed any more - choices.splice( 1, 1 ); - } + ) ); return renderSettingsDialog( { heading: mw.msg( 'popups-settings-title' ), diff --git a/src/ui/settingsDialogRenderer.js b/src/ui/settingsDialogRenderer.js index 4876d0be3..197baaff2 100644 --- a/src/ui/settingsDialogRenderer.js +++ b/src/ui/settingsDialogRenderer.js @@ -4,14 +4,34 @@ import { createSettingsDialog } from './settingsDialog'; +const initDialog = ( boundActions, previewTypesEnabled ) => { + const dialog = createSettingsDialog( previewTypesEnabled ); + + // Setup event bindings + dialog.querySelector( '.save' ).addEventListener( 'click', () => { + boundActions.saveSettings( + Array.from( dialog.querySelectorAll( 'input' ) ).reduce( + ( enabled, el ) => { + enabled[ el.value ] = el.matches( ':checked' ); + return enabled; + }, + {} + ) + ); + } ); + + dialog.querySelector( '.okay' ).addEventListener( 'click', boundActions.hideSettings ); + dialog.querySelector( '.close' ).addEventListener( 'click', boundActions.hideSettings ); + return dialog; +}; + /** * Creates a render function that will create the settings dialog and return * a set of methods to operate on it * - * @param {boolean} referencePreviewsAvaliable * @return {Function} render function */ -export default function createSettingsDialogRenderer( referencePreviewsAvaliable ) { +export default function createSettingsDialogRenderer() { /** * Cached settings dialog * @@ -29,28 +49,30 @@ export default function createSettingsDialogRenderer( referencePreviewsAvaliable * Renders the relevant form and labels in the settings dialog * * @param {Object} boundActions + * @param {Object} previewTypesEnabled * @return {Object} object with methods to affect the rendered UI */ - return ( boundActions ) => { + return ( boundActions, previewTypesEnabled ) => { if ( !dialog ) { - dialog = createSettingsDialog( referencePreviewsAvaliable ); overlay = document.createElement( 'div' ); overlay.classList.add( 'mwe-popups-overlay' ); - - // Setup event bindings - - dialog.querySelector( '.save' ).addEventListener( 'click', () => { - const enabled = {}; - Array.prototype.forEach.call( dialog.querySelectorAll( 'input' ), ( el ) => { - enabled[ el.value ] = el.matches( ':checked' ); - } ); - boundActions.saveSettings( enabled ); - } ); - dialog.querySelector( '.close' ).addEventListener( 'click', boundActions.hideSettings ); - dialog.querySelector( '.okay' ).addEventListener( 'click', boundActions.hideSettings ); + dialog = initDialog( boundActions, previewTypesEnabled ); } return { + /** + * Re-initialize the dialog when the available settings have changed. + * + * @param {Object} previewTypesEnabledNew updated key value pairs + */ + refresh( previewTypesEnabledNew ) { + const parent = dialog.parentNode; + dialog.remove(); + dialog = initDialog( boundActions, previewTypesEnabledNew ); + if ( parent ) { + dialog.appendTo( parent ); + } + }, /** * Append the dialog and overlay to a DOM element * diff --git a/src/userSettings.js b/src/userSettings.js index 62cef916d..232689395 100644 --- a/src/userSettings.js +++ b/src/userSettings.js @@ -1,3 +1,5 @@ +import { previewTypes } from './preview/model'; + /** * @module userSettings */ @@ -22,61 +24,51 @@ const PAGE_PREVIEWS_ENABLED_KEY = 'mwe-popups-enabled', */ export default function createUserSettings( storage ) { return { - /** - * Gets whether the user has previously enabled Page Previews. - * - * N.B. that if the user hasn't previously enabled or disabled Page - * Previews, i.e. userSettings.storePagePreviewsEnabled(true), then they are treated as - * if they have enabled them. - * - * @method - * @name UserSettings#isPagePreviewsEnabled - * @return {boolean} - */ - isPagePreviewsEnabled() { - return storage.get( PAGE_PREVIEWS_ENABLED_KEY ) !== '0'; - }, - - /** - * Permanently persists (typically in localStorage) whether the user has enabled Page - * Previews. - * - * @method - * @name UserSettings#storePagePreviewsEnabled - * @param {boolean} enabled - */ - storePagePreviewsEnabled( enabled ) { - if ( enabled ) { + migrateOldPreferences() { + const isDisabled = !!storage.get( PAGE_PREVIEWS_ENABLED_KEY ); + if ( isDisabled ) { storage.remove( PAGE_PREVIEWS_ENABLED_KEY ); - } else { - storage.set( PAGE_PREVIEWS_ENABLED_KEY, '0' ); + this.storePreviewTypeEnabled( previewTypes.TYPE_PAGE, false ); + } + const isRefsDisabled = !!storage.get( REFERENCE_PREVIEWS_ENABLED_KEY ); + if ( isRefsDisabled ) { + storage.remove( REFERENCE_PREVIEWS_ENABLED_KEY ); + this.storePreviewTypeEnabled( previewTypes.TYPE_REFERENCE, false ); } }, - /** + * Check whether the preview type is enabled. + * * @method - * @name UserSettings#isReferencePreviewsEnabled - * @return {boolean} + * @param {string} previewType */ - isReferencePreviewsEnabled() { - return storage.get( REFERENCE_PREVIEWS_ENABLED_KEY ) !== '0'; + isPreviewTypeEnabled( previewType ) { + const storageKey = `mwe-popups-${previewType}-enabled`; + const value = storage.get( storageKey ); + return value === null; }, /** + * Permanently persists (typically in localStorage) whether the user has enabled + * the preview type. + * * @method - * @name UserSettings#storeReferencePreviewsEnabled + * @name UserSettings#storePreviewTypeEnabled + * @param {string} previewType * @param {boolean} enabled */ - storeReferencePreviewsEnabled( enabled ) { - if ( enabled ) { - storage.remove( REFERENCE_PREVIEWS_ENABLED_KEY ); - } else { - storage.set( REFERENCE_PREVIEWS_ENABLED_KEY, '0' ); + storePreviewTypeEnabled( previewType, enabled ) { + if ( previewType === previewTypes.TYPE_REFERENCE ) { + mw.track( REFERENCE_PREVIEWS_LOGGING_SCHEMA, { + action: enabled ? 'anonymousEnabled' : 'anonymousDisabled' + } ); + } + const storageKey = `mwe-popups-${previewType}-enabled`; + if ( enabled ) { + storage.remove( storageKey ); + } else { + storage.set( storageKey, '0' ); } - - mw.track( REFERENCE_PREVIEWS_LOGGING_SCHEMA, { - action: enabled ? 'anonymousEnabled' : 'anonymousDisabled' - } ); } }; } diff --git a/tests/node-qunit/actions.test.js b/tests/node-qunit/actions.test.js index 08f90a628..cd3c9c7f4 100644 --- a/tests/node-qunit/actions.test.js +++ b/tests/node-qunit/actions.test.js @@ -60,6 +60,22 @@ QUnit.test( '#boot', ( assert ) => { ); } ); +QUnit.test( '#registerSetting', ( assert ) => { + const action = actions.registerSetting( + 'foo', + false + ); + assert.deepEqual( + action, + { + type: actionTypes.REGISTER_SETTING, + name: 'foo', + enabled: false + }, + 'Setting action' + ); +} ); + /** * Stubs `wait.js` and adds the deferred and its promise as properties * of the module. diff --git a/tests/node-qunit/changeListeners/settings.test.js b/tests/node-qunit/changeListeners/settings.test.js index 5df2c4e81..d19c453cb 100644 --- a/tests/node-qunit/changeListeners/settings.test.js +++ b/tests/node-qunit/changeListeners/settings.test.js @@ -10,21 +10,24 @@ QUnit.module( 'ext.popups/changeListeners/settings', { toggleHelp: this.sandbox.spy(), setEnabled: this.sandbox.spy() }; + const previewTypesEnabled = {}; this.render.withArgs( 'actions' ).returns( this.rendered ); - this.defaultState = { settings: { shouldShow: false } }; + this.defaultState = { settings: { shouldShow: false, previewTypesEnabled } }; this.showState = { - settings: { shouldShow: true }, + settings: { shouldShow: true, previewTypesEnabled }, preview: { enabled: { page: true, reference: true } } }; this.showHelpState = { settings: { + previewTypesEnabled, shouldShow: true, showHelp: true } }; this.hideHelpState = { settings: { + previewTypesEnabled, shouldShow: true, showHelp: false } diff --git a/tests/node-qunit/changeListeners/syncUserSettings.test.js b/tests/node-qunit/changeListeners/syncUserSettings.test.js index 19469be11..c2ea88968 100644 --- a/tests/node-qunit/changeListeners/syncUserSettings.test.js +++ b/tests/node-qunit/changeListeners/syncUserSettings.test.js @@ -3,8 +3,7 @@ import syncUserSettings from '../../../src/changeListeners/syncUserSettings'; QUnit.module( 'ext.popups/changeListeners/syncUserSettings', { beforeEach() { this.userSettings = { - storePagePreviewsEnabled: this.sandbox.spy(), - storeReferencePreviewsEnabled: this.sandbox.spy() + storePreviewTypeEnabled: this.sandbox.spy() }; this.changeListener = syncUserSettings( this.userSettings ); @@ -21,7 +20,7 @@ QUnit.test( this.changeListener( oldState, newState ); assert.false( - this.userSettings.storePagePreviewsEnabled.called, + this.userSettings.storePreviewTypeEnabled.called, 'The user setting is unchanged.' ); } @@ -34,7 +33,7 @@ QUnit.test( 'it should update the storage if the enabled flag has changed', func this.changeListener( oldState, newState ); assert.true( - this.userSettings.storePagePreviewsEnabled.calledWith( false ), + this.userSettings.storePreviewTypeEnabled.calledWith( 'page', false ), 'The user setting is disabled.' ); } ); @@ -49,7 +48,7 @@ QUnit.test( this.changeListener( oldState, newState ); assert.false( - this.userSettings.storeReferencePreviewsEnabled.called, + this.userSettings.storePreviewTypeEnabled.called, 'Reference previews are unchanged.' ); } @@ -61,12 +60,8 @@ QUnit.test( 'it should update the storage if the reference preview state has cha this.changeListener( oldState, newState ); - assert.false( - this.userSettings.storePagePreviewsEnabled.called, - 'Page previews are unchanged.' - ); assert.true( - this.userSettings.storeReferencePreviewsEnabled.calledWith( false ), + this.userSettings.storePreviewTypeEnabled.calledWith( 'reference', false ), 'Reference previews opt-out is stored.' ); } ); diff --git a/tests/node-qunit/isPagePreviewsEnabled.test.js b/tests/node-qunit/isPagePreviewsEnabled.test.js index e3817423d..8a571d66a 100644 --- a/tests/node-qunit/isPagePreviewsEnabled.test.js +++ b/tests/node-qunit/isPagePreviewsEnabled.test.js @@ -3,7 +3,7 @@ import isPagePreviewsEnabled from '../../src/isPagePreviewsEnabled'; function createStubUserSettings( expectEnabled ) { return { - isPagePreviewsEnabled() { + isPreviewTypeEnabled() { return expectEnabled !== false; } }; diff --git a/tests/node-qunit/isReferencePreviewsEnabled.test.js b/tests/node-qunit/isReferencePreviewsEnabled.test.js index 31ad3a63e..524e70d81 100644 --- a/tests/node-qunit/isReferencePreviewsEnabled.test.js +++ b/tests/node-qunit/isReferencePreviewsEnabled.test.js @@ -3,7 +3,7 @@ import isReferencePreviewsEnabled from '../../src/isReferencePreviewsEnabled'; function createStubUserSettings( expectEnabled ) { return { - isReferencePreviewsEnabled() { + isPreviewTypeEnabled() { return expectEnabled !== false; } }; @@ -128,7 +128,7 @@ QUnit.test( 'all relevant combinations of flags', ( assert ) => { isAnon: () => data.isAnon }, userSettings = { - isReferencePreviewsEnabled: () => data.isAnon ? + isPreviewTypeEnabled: () => data.isAnon ? data.enabledByAnon : assert.true( false, 'not expected to be called' ) }, diff --git a/tests/node-qunit/reducers/settings.test.js b/tests/node-qunit/reducers/settings.test.js index 4e3165dd9..62178b173 100644 --- a/tests/node-qunit/reducers/settings.test.js +++ b/tests/node-qunit/reducers/settings.test.js @@ -10,6 +10,7 @@ QUnit.test( '@@INIT', ( assert ) => { state, { shouldShow: false, + previewTypesEnabled: {}, showHelp: false, shouldShowFooterLink: false }, @@ -24,15 +25,15 @@ QUnit.test( 'BOOT with a single disabled popup type', ( assert ) => { user: { isAnon: true } }; assert.deepEqual( - settings( {}, action ), - { shouldShowFooterLink: true }, + settings( {}, action ).shouldShowFooterLink, + true, 'The boot state shows a footer link.' ); action.user.isAnon = false; assert.deepEqual( - settings( {}, action ), - { shouldShowFooterLink: false }, + settings( {}, action ).shouldShowFooterLink, + false, 'If the user is logged in, then it doesn\'t signal that the footer link should be shown.' ); } ); @@ -44,26 +45,62 @@ QUnit.test( 'BOOT with multiple popup types', ( assert ) => { user: { isAnon: true } }; assert.deepEqual( - settings( {}, action ), - { shouldShowFooterLink: false }, + settings( {}, action ).shouldShowFooterLink, + false, 'Footer link ignores unavailable popup types.' ); action.initiallyEnabled.reference = true; assert.deepEqual( - settings( {}, action ), - { shouldShowFooterLink: false }, + settings( {}, action ).shouldShowFooterLink, + false, 'Footer link is pointless when there is nothing to enable.' ); action.initiallyEnabled.reference = false; assert.deepEqual( - settings( {}, action ), - { shouldShowFooterLink: true }, + settings( {}, action ).shouldShowFooterLink, + true, 'Footer link appears when at least one popup type is disabled.' ); } ); +QUnit.test( 'REGISTER_SETTING that is disabled by default reveals footer link', ( assert ) => { + const REGISTER_SETTING_FOO_DISABLED = { + type: actionTypes.REGISTER_SETTING, + name: 'foo', + enabled: false + }; + const newState = settings( { + previewTypesEnabled: {}, + shouldShowFooterLink: false + }, REGISTER_SETTING_FOO_DISABLED ); + + assert.deepEqual( + newState.shouldShowFooterLink, + true, + 'if one setting is registered as disabled, then the footer link is revealed.' + ); +} ); + +QUnit.test( 'REGISTER_SETTING that is enabled by default should not show footer link', ( assert ) => { + const REGISTER_SETTING_FOO_ENABLED = { + type: actionTypes.REGISTER_SETTING, + name: 'foo', + enabled: true + }; + const newState = settings( { + previewTypesEnabled: {}, + shouldShowFooterLink: false + }, REGISTER_SETTING_FOO_ENABLED ); + + assert.deepEqual( + newState.previewTypesEnabled.foo, + true, + 'previewTypesEnabled is updated' + ); +} ); + QUnit.test( 'SETTINGS_SHOW', ( assert ) => { assert.deepEqual( settings( {}, { type: actionTypes.SETTINGS_SHOW } ), diff --git a/tests/node-qunit/ui/settingsDialogRenderer.test.js b/tests/node-qunit/ui/settingsDialogRenderer.test.js index 1cc4b39ff..399942ebd 100644 --- a/tests/node-qunit/ui/settingsDialogRenderer.test.js +++ b/tests/node-qunit/ui/settingsDialogRenderer.test.js @@ -43,13 +43,14 @@ QUnit.test( '#render', ( assert ) => { hideSettings() {} }, expected = { + refresh() {}, appendTo() {}, show() {}, hide() {}, toggleHelp() {}, setEnabled() {} }, - result = createSettingsDialogRenderer( mw.config )( boundActions ); + result = createSettingsDialogRenderer( mw.config )( boundActions, {} ); // Specifically NOT a deep equal. Only structure. assert.propEqual( diff --git a/tests/node-qunit/userSettings.test.js b/tests/node-qunit/userSettings.test.js index e80c22a90..6cd609b53 100644 --- a/tests/node-qunit/userSettings.test.js +++ b/tests/node-qunit/userSettings.test.js @@ -9,43 +9,19 @@ QUnit.module( 'ext.popups/userSettings', { } ); QUnit.test( '#isPagePreviewsEnabled should return false if Page Previews have been disabled', function ( assert ) { - this.userSettings.storePagePreviewsEnabled( false ); + this.userSettings.storePreviewTypeEnabled( 'page', false ); assert.false( - this.userSettings.isPagePreviewsEnabled(), + this.userSettings.isPreviewTypeEnabled( 'page' ), 'The user has disabled Page Previews.' ); // --- - this.userSettings.storePagePreviewsEnabled( true ); + this.userSettings.storePreviewTypeEnabled( 'page', true ); assert.true( - this.userSettings.isPagePreviewsEnabled(), + this.userSettings.isPreviewTypeEnabled( 'page' ), '#isPagePreviewsEnabled should return true if Page Previews have been enabled' ); } ); - -QUnit.test( '#isReferencePreviewsEnabled', function ( assert ) { - assert.strictEqual( - this.storage.get( 'mwe-popups-referencePreviews-enabled' ), - null, - 'Precondition: storage is empty.' - ); - assert.true( - this.userSettings.isReferencePreviewsEnabled(), - '#isReferencePreviewsEnabled should default to true.' - ); - - this.userSettings.storeReferencePreviewsEnabled( false ); - - assert.strictEqual( - this.storage.get( 'mwe-popups-referencePreviews-enabled' ), - '0', - '#storeReferencePreviewsEnabled changes the storage.' - ); - assert.false( - this.userSettings.isReferencePreviewsEnabled(), - '#isReferencePreviewsEnabled is now false.' - ); -} ); diff --git a/webpack.config.js b/webpack.config.js index 305f1f04b..553117b49 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -119,8 +119,8 @@ module.exports = ( env, argv ) => ( { // Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers // up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are // well understood. Related to bundlesize minified, gzipped compressed file size tests. - maxAssetSize: 46.8 * 1024, - maxEntrypointSize: 46.8 * 1024, + maxAssetSize: 47.8 * 1024, + maxEntrypointSize: 47.8 * 1024, // The default filter excludes map files but we rename ours. assetFilter: ( filename ) => !filename.endsWith( srcMapExt )