From f40cdabfdf0250f0004d39ec5e854b09bfb4f61d Mon Sep 17 00:00:00 2001 From: Frank Noirot Date: Thu, 14 Mar 2024 15:56:45 -0400 Subject: [PATCH] File based settings (#1679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename GlobalStateContext to SettingsAuthContext * Naive initial impl of settings persistence to file system * Update app identifier in tauri config * Add "show in folder" tauri command * Load from and save to file system in Tauri app * Add documents drive to tauri permission scope * Add recursive prop to default dir selection dialog * Add success toast to web restore defaults action * Add a way to validate read-in settings * Update imports to use separate settings lib file * Validate localStorage-loaded settings, combine error message * Add a e2e test for validation * Clean up state state bugs * Reverse validation looping so new users don't error * update settingsMachine typegen to remove conflicts * Fmt * Fix TS errors * Fix import paths, etc post-merge * Make default length units `mm` and 'metric' * Rename to SettingsAuth* * cargo fmt * Revert Tauri config identifier change * Update clientSideInfra's baseUnits from settings * Break apart CommandBar and CommandBarProvider * Bugfix: don't validate defaultValue when it's not configured * Allow some TauriFS functions to no-op from browser * Sidestep circular deps by loading context and kclManager only from React-land * Update broken import paths * Separate loaders from Router, load settings on every route * Break apart settings types, utils, and constants * Fix Jest tests by decoupling reliance on useLoaderData from SettingsAuthProvider * Fix up Router loader data with "layout routes" https://reactrouter.com/en/main/route/route#layout-routes * Move settings validation and toast to custom hook so the toast renders * fmt * Use forks for Vitest https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker * $APPCONFIG !== $APPDATA only on Linux + change the identifier back since it really doesn't seem to affect app signing * Debugging on Linux * Better directory validation, fix reset settings button * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * defaultDirectory can be empty in browser * fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * re-trigger CI --------- Co-authored-by: github-actions[bot] --- e2e/playwright/flow-tests.spec.ts | 51 +++- ...should-be-stable-1-Google-Chrome-linux.png | Bin 55495 -> 55402 bytes src-tauri/src/main.rs | 23 +- src-tauri/tauri.conf.json | 7 +- src/App.tsx | 2 + src/Router.tsx | 283 +++++------------- src/clientSideScene/sceneInfra.ts | 3 +- src/components/CommandBar/CommandBar.tsx | 61 +--- .../CommandBar/CommandBarProvider.tsx | 43 +++ .../NetworkHealthIndicator.test.tsx | 6 +- src/components/ProjectSidebarMenu.test.tsx | 31 +- src/components/SettingsAuthProvider.tsx | 73 +++-- src/components/UserSidebarMenu.test.tsx | 6 +- src/hooks/useCommandsContext.ts | 2 +- src/hooks/useValidateSettings.ts | 33 ++ .../settingsCommandConfig.ts | 11 +- src/lib/constants.ts | 3 + src/lib/routeLoaders.ts | 138 +++++++++ src/lib/settings/initialSettings.ts | 15 + .../settingsTypes.ts} | 21 +- src/lib/settings/settingsUtils.ts | 88 ++++++ src/lib/tauriFS.ts | 162 ++++++++-- src/lib/types.ts | 1 - src/machines/commandBarMachine.ts | 1 + src/machines/settingsMachine.ts | 64 ++-- src/routes/Home.tsx | 18 +- src/routes/Onboarding/Units.tsx | 6 +- src/routes/Settings.tsx | 70 ++++- src/routes/SignIn.tsx | 2 + vite.config.ts | 7 + 30 files changed, 842 insertions(+), 389 deletions(-) create mode 100644 src/components/CommandBar/CommandBarProvider.tsx create mode 100644 src/hooks/useValidateSettings.ts create mode 100644 src/lib/routeLoaders.ts create mode 100644 src/lib/settings/initialSettings.ts rename src/lib/{settings.ts => settings/settingsTypes.ts} (52%) create mode 100644 src/lib/settings/settingsUtils.ts diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 459daa59c..668a1a83c 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -3,8 +3,8 @@ import { secrets } from './secrets' import { getUtils } from './test-utils' import waitOn from 'wait-on' import { Themes } from '../../src/lib/theme' +import { initialSettings } from '../../src/lib/settings/initialSettings' import { roundOff } from 'lib/utils' -import { platform } from 'node:os' /* debug helper: unfortunately we do rely on exact coord mouse clicks in a few places @@ -516,6 +516,55 @@ test('Auto complete works', async ({ page }) => { |> xLine(5, %) // lin`) }) +// Stored settings validation test +test('Stored settings are validated and fall back to defaults', async ({ + page, + context, +}) => { + // Override beforeEach test setup + // with corrupted settings + await context.addInitScript(async () => { + const storedSettings = JSON.parse( + localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' + ) + + // Corrupt the settings + storedSettings.baseUnit = 'invalid' + storedSettings.cameraControls = `() => alert('hack the planet')` + storedSettings.defaultDirectory = 123 + storedSettings.defaultProjectName = false + + localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) + }) + + await page.setViewportSize({ width: 1200, height: 500 }) + await page.goto('/', { waitUntil: 'domcontentloaded' }) + + // Check the toast appeared + await expect( + page.getByText(`Error validating persisted settings:`, { + exact: false, + }) + ).toBeVisible() + + // Check the settings were reset + const storedSettings = JSON.parse( + await page.evaluate( + () => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' + ) + ) + await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit) + await expect(storedSettings.cameraControls).toBe( + initialSettings.cameraControls + ) + await expect(storedSettings.defaultDirectory).toBe( + initialSettings.defaultDirectory + ) + await expect(storedSettings.defaultProjectName).toBe( + initialSettings.defaultProjectName + ) +}) + // Onboarding tests test('Onboarding redirects and code updating', async ({ page, context }) => { const u = getUtils(page) diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-each-default-plane-should-be-stable-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-each-default-plane-should-be-stable-1-Google-Chrome-linux.png index 5bd90e3d9fd3582282d23401aa972604c0249dde..83c7f0d1acc06ef232935a8cc0fb016b6d51ceea 100644 GIT binary patch literal 55402 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 z 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 literal 55495 zcmdqJbyOT(vp$MTumHiGpuycCBtU{E1a~JmgF7L(yL%uZ3GVJNID@;pyE8Dq=6%n1 z?>TGTv+n)-?zI}3p6TAZx~g{7Q%_CUC#4TqFGyd&!NFn4ew0#ygF`BZgM+_BLjhi4 z8=b@he!)Abe0T>}Izqk&2S*JjD+g>(zy7xq=A{R4 z|JT)gF|?s3HhQ zEG~XUz1rMTi8Xken-kX8_w{>Mr{yS9_OaN*_%9zK$4Z{QVcFh2he>VL;07miPb*tq zT--SP;<0uw%Rb`HhGyV~ah|3n9XY|GpJ$9R9Vj#FnU`Q{kxL2TSy{xHpWQ2i6RK)Z zqqU(XfSjJ1NV~!KFNueyhYy(F^qE|(-`T9hNA?@j(lLcD9_pT-N1VYC8c@9mFVTbs zpkc)r-kNEQ`>oz-mLQuJ{G5~%eHLRMGri;JOJPsB17S?uKNe3c=*6!=@gLGG5c%Cm zqs0Ugsva*Jv1O&Cls*$u8ITHCj!Y|(%~tqeaCH!xKS6mc_ef@s_=m(>>vh>PU5Oql zFPPX^=$V*e$GJstadGMR73ml`Vn)l+?CS6MM|RyA{xsFCIn2heuoHlYfH&X@^`xOQ z_KjwIns^JvYs>w-!iXN>IPLQMEW*z&FsMsqom!u4-Z*U!6&Sg{Y??0nK`oGxxv0yT zc7}ubqkL4(S0CQUmjN%oD-`{lo|`WA@KC|kZY4aXC#AC!fuZf1JIC*w5`@lnc6A|9 zI;)B(|Ev_72Av9YM!|WB=`$OL4*0k_;Dn6$Pcuegp=gGe!yT{ci2}~LOBqh<>pNacTa&`I~BbPEAV%7N(<2wV(Au90$&5veCNuHo?swr83o=o_xM<{!q%_OsDr@p4iDU}8*}NEv(~Eg|o_elK0Ib92|DkEPd6 zDp>zWJt;Yv*@IqhTd%Ek(3Uzhk{4gm@@b% z0qFVtkuf#4_E zmbz~=Ar_ls%7uxRRt2KTDvpGNa<=AWzv0lpNgSCu_6vCji}G8^q@0~AL)3dHI;T$g zoUI|JVS7}Ue(=Ikw`G+Pum9L;#|FrVlNhaB6AkmUXV0O^;>M2LEU2DbeKA@ElU+o@ z=Z3^zk|uRH*{a8LQ68;O@gZ78XUaNSSM>5-XJ_X}WyorskP}>Jbfj@#>;?9M1P4bB zG6v!K%!*%?1%#5DrwR=VD^FIJJS;2>lT`4VdWp8W$Z?G|%zSHq%4RXOd%5MIT)Tvh zt{b0dCbhP(#t68~>BU9Sbv>}I8k`x=PiW1ZN-clAFW9Ko32eL0~E;CiO z5F_G$byaQ8%=+f9BdoQ>3-NTdD<(cW4*qS%bO{T6xhN%$#M31Yl@FAe{^zo_Kvktv z-@u5`@O)d^(}wcpu?K(MqBnjNX%FQE)Y8(j`TE{;y7;ka++?Bl#Bkx7f>pCVm80!G z{h*YrKlTFBWvVjv=^qbRgG_S&DfSd4IO}8;ME38JQ9EgWqzQO2baZqOE1KisMRHnrhp^HM8QNsq>Kp(a`H131!p zUV2|8XTq*mn|T2dIY~t?Zks8wIIJ&xL*I?f*j66vap7li+OWgqPfzRkVxiTEu}B>z zdsAu=mzrC$DZSY?_zqJ%7^S;fO9>37bfJh24V|pW&HZ$Jv=r5Q)NX?iQI?kaoAa9P zQQB7t+v84MyT&H0s5g5%!cKxn8y2NUf^dmEsjEA6;BrW`T>kIZxV-@VTmt*=4#@6xW{MB=b*vUma8(gV)@p>}qIiJv`% z&X#A)@fQqXvbZG3HddF5S!Z)>$s4%X3i3+&^E!pJfpBJ&c@A}MO@y%;o`1c?#|atgG>KtFerA; zKw_Hgn~qX_Rvva=S2{U*Ks!*ITZwT0D2qR>%Y((PnJlX9A+a3+4IYzvsfy;o2Hs=n zLMwF>g*;~aA&2uV8M(QJ_t(cg6DHZzti07fAlCNHlA2$M1)wW^_;`rz=SeJgrxOtO zli_rFw{aeBVu>~6WLBN%nr^wOZmHu2r&mOoZnvIEE(8`nJoX(3@d7D~Hj8!wh!0mH z$6OYW3`nxA4ogj))mXd}D)#Sl!H4ndvzz;dMsS9`wi?!hSvccHlDg0x1+=< zd#K)sgXLOkCu&j!;`6PHNk6Tp6OxnOOMPqPcqz;did3VDOcpVf@<|os+Nw(P zZ=JQWx~ozRW&=bse;&t*{>S_@f-kPA|D9{=VmVt%ryid1@-l&kiEy6EGiV42k5&4ge3kx=R^7aSzoFPvR(MD(y8HD04_B(p~&gs#C zMqmExS2Qgxts$6C-VEqyzILq#9kRBu@x@Xhf9#{CCNXwMKnSHrV)3C3+#)4T+ems3 zo4}Ih#?0C$pAm5&hC=}2e6c3CvU3nkM+fWe22N?ywRR*pfaPu7$)c>?@|oV(Db@U0 z6C#KNWkfQ2p5;4)J(9g*y}D>63SreXCR|+i+txABhjn=c-#dC^;~$pe^<4ICPo#Cr zVx(1bx3xp-+j12(q^2wCjonR@hj~1?yk~%>rHrgM2oq{qo4^&1s2(Oi98Y9fDOO+?X5{^ zrD9$sq_nels@C;QnlI976PM32eK(`(Nle&@_=52G7G{Bu1eR1Q61V)S%+HpT zB|h8KjF)NHmu=Ft8~DM7cMSZxjwi*CZc!cjM=?mmtIsPl-lx1Q)vO6;rt%r$OEDOM z`DRGl=~E7;^KW%DeZVIs{x-39WygA zSpBlVz6ILrAp*_Or>H*E5XF6g>>bTttctlg?L6X~9&gTsMuyV0%Hsc0$nHMMKk1s8 z34!-{m>y`rOXoFOh^81as4q7j;t_G%9vaDFxwp}5iH|6&50~d?O^ut;#roFceVTv! zK;^eNXy3ZFV_!JN#GkCepu9cuaBQ&$4amu7OXu5K*qiY9I;1JHk--~gGJm<}d@^6N&SNrJQb$>9FKGlnv*bxux8nRo z`uxRea+AyiFPA_4#rCRWqxIJu1&0_!ao6%}RvIueN+O+5FsWHt ziwAhR;cJg(JHVT!!FtX#MtMf81~q?G!oswwEbvK52l|Ri!5g6GKyI7uHj5?zbp|Z| znSSpfynlf8r$!rC|43QX%R${V<2r6g&jmSPBpqxaft_bLcupVKDvr966E^&wiM9G- zc30Z@n6xW@V-R05ZVbMLvTEtrcwgtB0T#w%Z7Y)*XDFJyQ8ozRhczY!=7bW%dfi7F zIIr7Ra*VCfS*C62JRaD&D;+s?hPq`wj8!Pg3)k2xPSk@JeXlrwc|X3M!6JJnHM?f- ztQOR<3!j3@xq2;Fk*tZ1j+JjV$RHRl^5v^bLaR_t`&&OI=AL7Rm#>IB&!hSS@3p^< za|Q>0?je3eIKx5j*m5*-nq^|HGCC(lLq`ubW{Ha;T|IMe{~|8gx8=O<*mP4^`FFcM zqMiyc8tsC_=ZA5jgNW#*jWg$7RC!A_TOItFo;#ZF)&*UTi@G5 zlrlS;(?52V`d8}}VS>JAS~Jv}q2&S_PQ>kK8?e8IM=;r)GIE=g}rPW^PU zIBt7jKd#^Lb#xp|`Apy4djXiV`(DkARrCg`5xIXtTX@ z0(~EEA8#NmOjTrndi$JRTwSGyd5;AVl{;Lsrt{f`=j2dzaWsaCF$2*sq3R{DgEP3C z=*#H35D*!-({PP(@rH@G?+hDP81klZl135i{62Q9>Mv^^DqU*Y^LAA(B^HXmk${Fm zp-{O;T_vKrWxjOErAEh@Bh&*%yZOo>iJ4-f!`C?Yc;xTg-34P7{Uq9kv*djxC6m^n zmS7m5F6Nh6`2hZ&t2Rj3d^IoCn6o8jQlK-#{=C2Y`};@M@5`NIyqHZ%RxjDnAv?T2 z|9MFnLl)C}H09b{d$QQy^rg_#=O;yFhEvFdG9xEv?3DF%`D%>B*Rke?ndl6L1WQ=$ zlnfA9nOFT$`2+6r^5O9xt_@HER_wwvc<|Ya44HLJY`@jNtsivSc5Zmt4AEIg}JkG-`XTJ zbU8x}h}eqGJhGHfaX)iCl;ICV6nlwM^4rQXaMGo_>NVoyQz$uXBp)tjNow1PH%zO< zyB-;Bv^vWvGT68hb>MQW;Fd5qiU%kg1tzY;t8v zOVbyJ*-XKr&(XVqOWDw^6xi8WVU9-)G78e^c?2M_@Zw1ciMCu79UH#nUwpRn0K|Hu z3e8MXYrQNhZ}$=b@3XpDPZw(?J*aCaM$`jS1T12>B}N?lJHr+bY$XkISbO{1bTCzB zSgpr_`gOstbLbakTw>z4)DByxYod41i`gUBIdF>2B+z%;WoSGYR%@VL#7gAioR|(^y1=TqdJ*wwyMs1r=Ormou}Ns zEj)08ZN>n?=Cx zW%LXtJ01~ezyXLejouApGs1p6@Y;5v+ARO4_qrHwT2FMZs zbn!Kfc(wpz>%h{DM4#YA{^hQ#x4dPf%KwO`FdYUrnTP8k*p&vq&0^p89>3E& zft6EmA~W=KDuq{KvkG*`1F5uoL=pF}51%<}ay>!GeP1_F)2j02OH24XRsXe~h~@O8 zSHmR95er9!)%2yIm?*0THhhk%fC=hiLt+fN!V#jom^8!k9}?b$~$_J`FH8nW1+po zN_SIP8FFrMH?qvF`v3}DwtFio;s~3v`rM(CxTl2}j@VR*`axV8T8G)t?{~^TN=YK4 ztJ^Bf!r1nW!|yI?p^Q?=y{}{G4gm^9v!6+iw$whEuqYPNMg(>Rq%?n+B*avJrKT8utv$kxxqt?`UY@{{4qCvovW ze!I6yaRgKnRJzgJi{FNgP!r-VGe!B2=c9Uhb4nI!*Bk{M1T%pe769@p>eO`X6>lI- z|LF1Kyc@1-o(CMU2dLAjyO*j}`)MsaTBoE3*bPjXTRHOBuO`b!wyT4PHM^gzaL>#5n6Q4cJ8I1C10x2uWOn^YyU6iu2vZ`;O zpN3)n!I-ObtOMbSgoNbzc?P8z98WO{zu8YcySu%0Cn(|5VHG^w8q@*2+}GJAjUE#q zi@|&8aNCp%WT|%;M4oIqdn<(kZNnJ{N$ux1H#eTyxNEqZ+vJ&SGn~KtXX>B-+niQx z;l%wT(&ClO2q&}TMfIF)t{E9sf1QpLU}){syOC&5h!JyLlx6)k8vBV|6z2VW0J&IK z*3A+`uWm{GbI$J9Z1;Q1Y1`TM%QA7sQV@AYot@cgEXJEF9?~c&Y08J2vooueQi`L6 znvG<#vYpPgl(VM~RA-$}^LmFcAoIr`-gH~pTJt(7`FeA6(fgIRlE=~{N`Rn3lU1L` zJ-XXe)FUGq`bEZ^p^Za3w9W6W^ikD&i&r>JyR_SEn(d8PWVZb{3CAx_n#ny9aI z@#W{0H2UkQx?#`(0lHQlu4?S4;FM%$G_4!1VYg19SAY3d}f$Fx{ z`X=*-BX5ZGBve%cUPCs8(=mei3L)+1*6TM6Fh$cyi4=V`eGGP9=<3zZje#9K?Lf{G zpF$!<8amF1Hsu#7sc4OFIaD*4Vs*40NgA5no64>5h>K23YJte8Af>WWUOQr+ZZ(Ce z&)O;q6WfJx%}*zfYKtA;a8oAwp>s~e-cKGji`8l1vy!il^Lqs+b8??$aU~^v>A*7h zWCQ^%^^wp@m2mm+(1@sZV=m(=5m1vDw1y2ire8r4L?cs${^c7^>o!nnJM zt5K&JcdW^7*KWC2&h~mjE;}Gi80+KN$XTzRi1tS1=B_+1E(Vi)w8NQ!n5)D_H%mb>Dx;Rl@V)*v#gxHtgRCNWZa{+_R(IKs~B?K0YrCvSzt6R>!;$C&0EqFe*mSk2v)KgY@rL)P>{Q~-KT3QSYT;fH z5oyqIL|-55K-#7q@5+t3`PdEc+zo}iT__+5QO_~XdWQY^-ouFoT@Da%Sgv3lM71nu$7aR0V95O&)~dRz4?tbWN30PcT0KFyW#H$_3A2xW95W9v4_vxx>4 z!`D_i6;=cbZik!cEM>pXE{d?|>FJ>rHq2fsDtk*+j3qj?#qO*kjHq2tep|WiZ`oT{ z5%}$w1~}TDN&{>B#@1FLJ}EH)wIy|b#b^df&oRJ#?cny9|fnr}BF zQkA>!8?gsUT~Cp-G&N#H8A^}8WaPhOG;J}^s$ag;Y&GSlZdY)K4MM3{E_<()mY}@6 z)SkLy?KYzVCm@|F$|#P`PD?wrak3--EYW!)#t|c?V>3#All{o-*?~GiO2#T$Ajkc|n{W;rqe_jlT}_&$#yJ4y#d^-_q8Z* zVr1k{cn(ihC6iERasA5nh?(I^YxCUzOY_#auXGNZ|so6nk>PyTZNlE=`>>5oe3^}Xg` zdgkk5=qXxu_9+67`3M~wTU5cODOlOvKbcK?l=o~^rT%x{$7CQw(e@N({_{sa#{d*X zRsq->T&?ZONDR*t>RUVgt@|$@@Ck4`f(z(sPi7@OWl*f2?u!O`85kH&ZyK+jA#58U zC8)0E{RO+kui6>eYIZ_V+h6(f9@VaCPv+QT*GTS$W62ti=mV_P$Njuvr@wEgpuRqp zF~=kMz^-NZU^c(U1Hf9C*xAj`7fwEDs`gz|h`8^Dap~u%xfs1y!OQW!qnF7~i_J4I74K1J*&~_K5ho-imGkrzwyOgW z>|tQRWxW{;4CPKU?A+Wu0B6N)Esen2)+@izda`;@PR^yZ|^cRMJ19l5Aj@&m<$c9rZ~!v zT#3wT&p80Hkz(3#(6IiMmhy^kZCG#=!me2Ec9T3sScY+AX+o6^u$fqu`Wz76KHTHI zT7~SDXsfP;?pF{Jjh2HD2qSL7YZ0q{9+~kcvt~o$k=t-HV&B>%NYO&s0+H`=NlCsh ztV9lP((;SPZ!40$B*crH(*8cN)oE43ec1R}*AMm{lh_rWP1uDx?~$atMl7J7(Dv*2 zA+NDXhG7jMB~9ai6`OyJ&jU84k2mJE->cO|>q&~sBd^<~t*1XaGgZyLBg4a2e;+`C z0P+%6ru=tuIp6D!q8fmnz_&3AUo9<*&CGRcPKi&hrio41APVwpjj- z>@0Dx>c!la`z_7Me8XAz?xL*i<9u`4v0AYZ11(HxC&vq0L*JFzZ5j5qMDM#{1w|q%s<&qJ}BD=(lGeBlj~G}I3I_Ef^t5Gdg8&BMk==NT5Gc)bL0C+ z!VOgcY{;p{1VF&4$>QU8C-WIiz!F^;$`aBLN-^MrxQD;NnCmR;6iQtvSb`S2(w9PR zoRAi%TvAL_Wa#FBviPsu@^6Es6p}IUh-sJCB9T-g$r0?on=^4&TbqtMA&{6edCMijs%LxEgU{^uJ~OcI}x$wn%n@eZQ}tTppHeinJfV6 zXYBmm6A=~}+P(o=bM%6o@=;c1p5=&0G%m60vNHlG{gBSCj}M3y$V|2;%33e)@3)&y z;6KVLnq5q@VIFFGN4TVJH{muLeu$G({rpFyG(?=O}C{w8~qYnY;m zjH|5=XCKlT7>@l6QXmcXbIr(wHFm=sJQinD)%F0MK}$zBX&tihjNAzd#+J3gqLo{r zc-FFevP=G_-+&lxr^2G~V#y+--so0g8){F4ze3e+d4#>Z$W4s7c%w6NVyMyt> zezVp~m~y8|Oq58PSP$#(C}5fUDpFt3lvkWEHjW#^Mq~F=P=AL*7gmlbAN#PK3wMXteHLo6bFFTBLmd1{7Qk~l0p;z@hX2BE2@Jo8i$i|sSD3M3U36%wD5M}0R$_|nF6 zN4wUu;5RWjaRO4o$uazJPgZr0|@!2Pj7+ar!eVrxt;HcgrC8KqeAdx(aHVP z|KW|V?^6&_8tma696PBeeEy9AbbD(H-#1lD&1rjNV=Ao7i%qO5b=do9uUDEkA>p~T1akm^`tC|0j$H}>2X*a=48X9a zOLclDl_luSPW0csf44hRLfc7QuK`h&%&xz z8;QP=Q@ZPBD4PTIf?kL@35Ix4wFsHLa9eUEBTGooY`9kz&IX}m36p17mnudi~V?FV1h9wX}{uK9Jh(s6b7nBrv%6hA1yh!`=%+ZQE3KF)I8E7U z27Xdj<8nU&0xauRpQH>-VMWW?yQ^JWQ%Ny?ao5E(j z3VhrLH_=9)$)!utCe#@5^Oq~7 zvDtdT(DKM`;0GNuqLSck09-zHObJZIYZmDiY)^ck7FLyb?JO zZM-5?Fm!)!?kyB_ME2A8hP-16 z9|0m93b`tCMP{I$y!L_WwabY(-139r{^#C`)L~JPC+4zEg15T5)f z?X^XuccrBM9#k5otU3tR`&_m-?=6NU4`)mW*5VGTmm0oZX?3B8C`9ag=JKeDBKWOSO%M3-jW|WyWh|4 z{1xw`q5lgYW+mD+-CK@a*ZqY!B4qmx-w_Z!j?)S)$9~)yQQ0pwdRwo%986z8F*u%_ zZgyVpd${6I`W?o-xh~BHvJeTHjcJSLPy;9b02$TkjDdBL+p1BeEMT`!J`a`FJM8BeG5SeQ029dRM9gg>x1%EB zGq}5ZH2;GDA)CJAq%Zu_ldjj5nl7lvtha`u!JQ{fVbSsN>iMYQ){iM&+65)jQ`aqj zx+1lqJzpUNFeG{+{XXD8dr0vMQ~dxq%JSnCtvZImb!jDDRIOPtul~i{VH+u_?u;%Y zM%xo5yOtdgLmOvu<@k8`oQz!z8E0&IWkzZ+Ar$(^#8R)3(3!{cD+=lkeFD4P*4tGG0nEyfJ+?ndiDujlKm z*W$}S2|H>y|H1Nc zsRb*e2Gv}$rjBgjh}&@c68IBakW90mXWBhzCUsa(J< z;o)+{A23fD_U|eBz<+v0RN7|e{i;p5|LqQ>4~b@%lw}8&}le+?U$U zd+!gFEWW5F_Lfrg*PLf2wgV35OE&PR@9ypbgMt8xw&ROF2&8ufHcR9plaK4L;hK|J zzKIqo12DB+D8S5vvXpn(d-b(q??0=X>V>?1o?0b8_*MN)AY+B7ouBSBxK~U`W}b4$ z9K%-mM5@epC$79r z7dW_AoV0^v2n3GiY66@10BhPQE+BYxY~wM{zE0%ha;B7}rB+rCeId4lgv5OIGzt|} zPX4u1gDybdS#@L%P)h+S99YJrB8+5r++9_s}?E^o>!G7uojsXUalw(nCKu|EKz z1_fW8<%bVi|9 zUW9AsvrJkK{L_o4|8$`gs6P{6fZ-VK6tDcpf{h=d6saW;+fadHS65Wpg$Jyg@87?cQpWu6te^kAO8;L^2yqJLmYTf$_j*8mg!cWrci#(31OD$< z@Yw#_{S^Nd)$`l`%sgx4za!4`|1T$obr%qo9#{1c0uX@4oZjAE)T%kvg~DfXz_;%? z8`UlJ4-a=~+id;Hu>Yrd1W+ZU-&aWLpFUAXZIDh~mp= zrnPOJ#qd1VOO_$+=?@PGzkJV`l?toW!Bq=KkvXRbvKjn5?xSxz-*ve=4R{qVl;^2~ z|7qcR7TmmAnYp2seDNy+f{Aj;fbiNW>!m$yL;ewxq_;`22#FP#Df9oge^GhXJ6Gl=f{ml-ARwK+jW%5AAy#EL?({XeTGn#r zDxI-pXYm*u1;m;p-@+1BR{Jfo3#+TE!8>y)$=GinYLDR1u+Z0Phzx-EQyi{Ycn^!8 zf!+zW+eGYpCJp7?(C~$tp}-jcoiPCN>a-MU4Zuo+$BBvIAd8{Q9-yGQ6`x0RK8YVw zX?pjf^>#ahw3qo`anjOm?MovJnpbhTLY)p}W!I9)N{hZ4v~i(lEXe>xY`FTPUm3Gm z@tvt>5aX&jE9cT5KaAlJ5E0&psejV!d#-d%*O0Y%Uap3(bpd@gMgUF;2W{~c`y3aC zN#Q7-g4Gl_8XFsKvcskcG+!~LV7A0wAI*;yS}1QnwdTrj{YQQJB(G@w`FuMgo+pt8 zG6C2YTG9{cHP)7|$mH{9gW;;bE=Qi3^p<-;D6_rxl1Q^y1O?H)85h<37(Q}Ej7>?w zd8@hG(XIN`ffG3h8SY=!wf32&yQXH(jkI*nt5>fa=W1Rz01y~Jh06hoBml~0^{Vhc zCX!f0gq#j=ZR*POFNlEp^ts)oJGS?id?!A|k8p+nYJ0gmO^`7RE^&+62Es(1^Zg$#c*?=z z_h@H|>71_6Dw~B?#Os;A^C`*Tv@Buu%LM>w-pKZe2C~#__uD+utb@zzqfmezkXE&G zI`Tlp2?z>cTZ7CCl>lM4(V%ZP5Og5qDbj!_~9rc^-EhJV8N0Vrm(P%kCR4 zYP(y*0EMN=0_REhg2;Ntb~O|JQR}vs%4RmT`?rq$pN|MFxBF56I^yBFNDT0HtjJAv zqA5NuHN^nrhz$opV$f6)=HUv?w`hyZj_k=-PXbO1UMkm)9UcKBzWvg~Q8GY>%}BEC zf>yZ$R}`5*rz!Vm6_tTenw^Pe%P;NiLvv24fW7~C{kp-fyGGxo5^y}5-S?+m$_K(o z&+`?6$3#a*=hKOBL-K=im4SP;OXyz_5RH{fB4+#gBFN;&Rm6y^oVf#~2B1WTgn~4! zQVnSTx&3(2Ogj+$J$o~yZ=Mx_r{CJ!;Yj(vep|BJIk`I6Tdc^R*#N58Z~k$t&jxc& z^`#6?6BVdbJyV(jP%euOK0SZ@c@#uXMd+cy`fa#*pz9I)K*Q>PjX$XSTbTcZ4bLyc zA^yMZg+eruYK@EHvqe^q(m=;ygwj1@L5G7?G~0hn)@7sM+l54}9(7xU-s#K*5ieG9H%U;Wkg1Lgwz zk&ylG&MZWtYbRirk|Co=c4RM=>n#XH6TKsnkMb>?r$tDTqLTwDhXD-7-hRYS8atYQ;^*W zvj*z8W{*xKCvlV}C(!oCNj!*HQ9tMvC=4BdY^$#W?XR0spAr7)g54LZTZmeK1Il#_ zY#S|93lddt231kM?V!2PV<*bHP~;zX`y);H(+8p};=^+`Md_Pl=%E0Rq(jpH#Gd<~ zX*&M+ceXIda*LQi{Fa+BEIWbvU^->56yehsK9v%3%BXNzD-&=<^l89fNoV)wHNA(Q zAqX{jvp&`7NL|xZhWruM5@iH>#jcT*flNVm;H+b z(mq`u99A|=ASS+C+6?X@*P0QVN`R@ZMap4~7N9E(80PzT%xMfehBdZJJnv^|v8 zCH}1Dp9>0=bFe`i!g3GmByjD*7ii^VAG8lfn*TidG~d=%PKlX3GiX+a0$! z&YznOR7JP#WVgERNHLRxsX~jELjOcOxj(HjEuE!+p2|hMw}%Z3=h6~F@deNCgm9OvfmX>={ELs#K_rlFCv9y0?DZ>&;fB{w`9NvzVLDUDz`JLIF}+i;Esz@H>CtPdoy zKyhV&c}OAF{Osg-9Ou{|nq!1}Ip2Ox#z4>c#n$1OkICFtO5ub=qdySZ-x3_os^XYnS=Ee6FtZ>z!QQpfKz z2UqQmxw1t)5FV>1(E9`FaxzyxPm*6!=XtP=wR~XQ;}snHnYb?!8|c?>(6nB$#_lCGpL{*DKKL>{&*ux7-eX6-`X21XEFZE(&lu}2aYxSr)~Sw!TY3k zQs7G$;zO}mpPeM>+}=bX7ihkHqc_Ii;!XM8`lWs#*QO2glr54`J7%%b*ODa)c(46^ z>-sDH?~P3q*V7E0=_=Xaq{PY{4-_#A3+$QSvcDy(>J~R%du~s|>Y6Y-wiRHcinO#p zkW3K&iR6MSIS3*r@MAx{eCcldHPngZ_Xo)7oh=4OOFA-ba<>b<7L7d8(byktktS>u z3Cj1s8aoqoTTv21^~ZM#>O1chM;2M?EmjDo;whM*uc?Y5B16Bvx*6>A!P>OdvJ^6lh9I#wRoAbh1Bda{7 zD}e6(%>IC-DYmPqK*8I;1>6h&ORHUaA?}uP64n>xT};j~SR?Y1#O_%RzbkwueCAff zJGe%tm2y)Oz2^^Fvxq^mzk6hr+Fb@VAW_P|rkF8^h=Ij*Tj1 z3GKZdY-aQfU>e|hJ6E2o?gC)b9 zC0u+7#$~wq;K*sEV1m_nf&rV zJhn#{5pbz;5zM}V$^ z9$EaZNYDGeJ5-b?1q%=NGik-YPx<|4nH}zKu_Kt(Tf`gg7w-MvXh!}IPgemIRo8Y0 zP!v?UQ;?QWx{-L122l}^?nZKGL}@{gl+Fd} zBxE%F2@L4eijJSwbmA^!35(2Z7}HGmrSlGivmIo0wmed>4^#EL(2+gC?|M`um+}9;rj_8e;B`o7upx@oE5&5 zO5gJuZ$in{JTM`Px%lr2UkoL)Zkap8uxZPJZBo)K$iMh~fH2$c@ci9RYjm-by1h62SbAmwt19U+2p;tc15p!#NH%6=;&Cbyo+C>=}JbF{n_v z<2Q0>xp@*nT8@gyNUp$t%NUZR+h!kS>yEki7XLw8nh3v1W@n*zSnspJulKCl8XL8$ zzY&PaG@a%m^){#Wwb!*yzR$EJOfP8s>Hh`@ocy3$mgOD6r-{TNr-vb5TvO#2wxE%VO|ylf zjUqs!T!Y_1L^{vZnAp1-L z#8X7H{Y*Q)&{TQr1(!icbvYQ8C(lJn@<|SK^xmMZfMSBooI4izLqTy+{` zi~_@3UGUx+lqXM$_y4lgt~f731x}`_18uHr+p~ zWL_Y4Bcd?B6u_@al^!X?Ol*`Z&L&+l$hD6M%<6Cfo{TJNA}lh}&gkE2ywI}6N3==C z;Hw_pf3j%AOADSA@oN+ZA<#M8;(Q<%kxM6NxU}6Iv7DobM~eXWpPyv$x1Cua`z_q* z{wok|Z%@}>W^kPCVd%b}pM4D?9pF=OWjuEoh_>vM|F)EZy%vsVwz!!eJpbeNT=-KY zCd=S)mU7iA2Kox4usHSeyV!VkAbc3yWavxSaDU z1So1KLaIL&hG!+wWto3#@l5oP=5!mABfl#imD~vnebnQT9r1$jQhF9+0o!OCe)o3u6y`InI*R zh8F|a7~XFzlzICP%0yFTy%EY#I}lN_8k|vQXG_`&LP^V}k23sO@On{`rf;sv+nPIf zE5hC)Y*2{=)uWLS~6XJNn4nJ2T4Xhorp?JsVPPZtwupj6%Z zSE|SbxQ2xh(xtCax;~RVcUW2dO#ZYdS0e=fYqGXbbETU>aatC%B=>IL4kdlx zNp@g8$uXYGHeP`@7ltxgoT+tNmEY63Cr$9>pt`UhVJHy_YLf2eolOPPAO+s8n8=i@j$@^lTD|*nO0Betqsl8%^KVa^Ia% z0ZQfq$^C)DiUn-<|Cd^vu|MgB3Q!l+J}vicv>B}l9pCSwY7HN{2Y`DO{&%me5~{2i zrq@9V*T0dbi)*#3dNDEhH028G*@`|1?)WB=o!F_XFrE4n${F)XXjS$OtPj7xolq(z z^wrDu(DW-mer+Ll8S;dfW57B$|F0!*aP-(`-h4>+dLcnsfSY0Sz6_G7fQ53*hmc;J z*}KJDm1S$=>+4FUH+M85Km;&SU$BAQV9m!#LX&A;XY^^kuz9yP5EPv)u~Jxh_B52e za#1wZY7rIs(*A3!%xftrarfj{fn`t< z7G~+VRvPE{BWwE=f;l-LnvM`X{^GhSZ?H@U-ESjeCTWHfpXKb=;KqCZ-MESxgm!^U zfxE|XYtLjtRoQ-zqda`-+_m1A&_B^KOF~C*?`T=LvdMEHrsQWT*#W=eMzgc7x^%Nx z5S}{jw|iK0^Z%ExDNxx_>8QBY7U#WDVKpZI+k&ws$}2vLpa20Qif>ywyUWTdNpLg@ zRxUD<)oVw+F)?LFDTV^#EN0wJ{^)!HO0J_N_(@8rZ{`6VM-*iM5oJwrp1&+!mhV%9 z1~0~`R9=%8!&@p3CkPjfFMNSCKhCioykB#LNtRHyn?$RZfF|Ab6ab#Nqr}T=;HOJ_ z{n3dNv-6!ZFRa8L=!zC}Aam3qchrBRP5_cW+xA}~LI`zl&R=MqcvJ4}m@Ledl_y3f z(BARnNTz7!hPl~T`ex`WctY<8n?3l0>MwoGR{6>W?W}xDj<_%QrlR`dK~dfzE!!QT zgIzYjhz0)bt^U!d@+0`ww+Y|qtW(v~%Kjoa8Lu$oD2 zYngj6?DvAKFN=8P*eH7Icbsrc_Fx!E2+}QGI?LDSXCW%PFPNc>GW6Snb(d+i`Z`s( zi9b{7c>@LRcMpc(K$+ruEVrK=1%|~B8L~urFhtEyYun@1(;p|@(fot}y^Cw$Xz>1J zm<;{;#r8tQBQ0F>N*H^Ua@xBUx$ay>!U)Zm|1}dWBRUBEwGNqQng49(^@tp$VDmOk zZs0O+b3gvp+Dd5NIz8=;8IH0GW7yrIH^?$3JWZwKITf7ODE&Cooffeotvnk@xIw(l zrl>*FEw6VQ%%s?H|FyIsZhvY8maBzxPLg(XcT0Q8$GD_ol*i>!cTQ8TFa?qKEi}P{ zF=hmSVCE;j{9Bx(3-e}$6WUihlFkO$*&%#aYp5V>F$5hlpX;JTwC>!%f#c!Jv>bY1 zo?BdHX%hPEm#81U(HuO93PtSh1v3TBzeV(oG%tjvchU$*g{HiZPkfn@bpwzv(BuFY z1Nrzq-n~$GWT-95BFRd6JmjqANv~pf zmkY=LQ0`W4#+Q+i?Aq=H#Bt`DaPd-}p)(P<+aa6t1k%Lwyd@ z3e+N0_xr7;%986j6X=8#*D@@rWBDyh9GSiAg|G(aVoscxC*>IR{V9sU1pd&6ReBN2 zA$>Ida>#el8uxC|M9OY#N2pre5@L2-+-YG!T7>a=#WeCeF^w-@yMt2r*qFtGY4Zg; zT(hJtjBVS|g;(MwL}(>+0%_z2auH{r=GS5PHzN^RVF&CE&*U-N_{LCu&C-i0Lo!_& z?;SPy>jmh|11?S8JoeC(tcq!_HSVby6%Shnu$eHLf1#%1xuf!SZZaYKCsRj5Xci_{ zsLIdrt(N6+oZAo8()k%UvWoZaPRr{fau<*n_Fc4C%}R+vbqeRUb%cMC4Ep;*asr0eH*v&yV7F%tHM{ zFT>51Hg5ZDDUF-MD*fbA^XiV{=)GD$Hn#sZk5%|KJOU==N~+yg{z?>Ss1XH%BRa{8 zN{xAZEmyVgF2C#HhdAFgdI`Djc{V`1bxXZ zn%4*+9D-S9IndCnTy0gb`(c55DZ1bjI&Ls@=EA}sao-J=bevU6!R)#rEn+h|>E8GI zi3j1m!6vAr>i^n^b6#%Dg@LY`=kKQkd7H+E!l6Sr@Lh?_x#Nw!M2h&duQzb>(+%Wf zd2J`!w6G#JP5e(G67~6&uBW#k1ui)`c~)nl zX35WV!`Hv9uMT{3M=wkLH{=qAKNvEK{@m|t?{{_xKF`P$nc9a{rNa4 zZr)ppGzSebam=s2XiJ&Xgw%qH2OIA7d3GuBJhaIyOfrSOKYjV@9Rh)`vC+8YryvK!C>SE8jKW-2XX0MRDC#xAj%2zZu)l++vGS%h*c>FD^J zY;YfhwSx-IG1lu-vY%J8#f(`#bs_q;;O<7IrzO;(H3$iAdr;GtbqbfN@2Zww3HsBiosV&^{~i_|NAqEM!%STYt} z7Vh2?|`OVwbm7VtP!N^y6spA+uRN<;J)*e(pW751GhYB$%RS!1_ocp$Ep78`#E`05MHe@EX$@VOww`K^Oau~)FhvB zq8fA9pG45IWeiw&cnFOrJ*=KmDj8EK7#3d=l8AagFNgf{#AoHqS33Fhs3eOob1H{n z>CwZ2Cp?VP>lLXBJ$=+BIN=O$nLY%knF40=SnB(a2^zu-j}?2n*pY8LgFWsEkEg|E zzE2V8C|vnaS(tHqU8XLhpHS06;;=4an%K^wSUU%DnvGm$YrzMe$F?FUDtFr+Ik<2v z$(7ny@3ySG_ZnasvgjMj0Qw|@S>i27WUYPTAhNQiCbYJ;HhVp;+V?7?y!pUzqcw?& z@TxFf>|`oe+J~sQxjB8uHkN@H$w=^2A16_NyB;a8KR+fl@5;I2@pAQ2jNkV7jLvu} z(%Hm|v3xjHdC#s$;o&5I*dr;5gE=uP+lMSyPz$Zh$4$zW4GmF7{1FWe4T@46KudJH z#N@g?!kS=Jc?ATRXCz+Jk-d5IM&Hyl?(*)oPu9zO(iQMtuC3WSxw`fk;z547H&~wR z>b#TqRAHmCS3sm)GA8+>;f1rJr78YXRN)&*O(u3pshf@V>;!gG8D_7to;WDtDac16 zFjCBiqA%Lh^$f2c=1W)C)KIwi@o2c>eX4u6B0Vqhfox8A95f5|#x?#cqLq@i@r@oK z6fP+x$cCVco0EryL!FCzlpAbmKf!ZV4XDP)$Ia3?G)5T$NI-OlnqxT*S+70KG5y2i z0H%_Xl6v3W-<9Lbn5&Z9eFwjP|E_3hVRF2s`(24G=fj5&Lr$WWWh|<6vEA>4dHjCs z#=if)BNkFf60e9#LORJ*ma>0PqctLSU;b`IcS`D_oEuN{)BhB7T(@e85tRsLieyR9pASvN&?sD&KC&a8WOlVGBi%t-8c^{h6M|>c zhPyCfK^;yoXtGUdtg-tKPY4g0qNvV-iQOj1%(@6*yGk}PGC3AOQT%p?d{iwrCtTxn_e z(K4k^ly*ONQ#qwI?W(q-3Sohe&RvDYmrz^LXKk6hn9@>t#rfCSbzmY^XMl87xFtod z9);-gJxhegK}7s;?-qnS7CdnqbD(T6B7*7K|H7&{T_IQ$t0p}vT-n%2=Qyh3c#E4| z4W_%HDLe_kU}p(rhk2%=7!Ofzf1}mH3er8L9jCKAJ#$uPjp;38oo;bbC}41WM~Ex!HPs9~g4 zKpaF-f6X|Ff|-{WC8)V&dUsI?X6ZJp>CeDI8m=ofae!Ydf=S;#C%Xs${Vz?(c1r`b zJY4Wld^9_zHoL=5GYG?pAp)Nb)skBke!ju-NA2T$XX0GuFz~@Z;n0RW3bO7uMQ%Ua zuS$~+n#on{1`lU2k$pPc`~>yTWnrt))8RScD}y2xFsKg*FTq8U`BNV4Prd%lKjzQ| z@=_mp7LGGdp>Z%j*&H3(vtJ#=A+8wXUmk?JrspTYA;H^&nEw_gavnEhOe?a=6WI#Y zK~ZjxsVgG@K}t%lVCLdN1R=Zc`pJmj($W&I)^JnjOTC;OU$PoI>8;YSu`#8yk@cH* zbr@bK`6Qt<}0;OgHpX>VhL-(^7x3RTqmvce=ar8{Lh zTy0gQN*gE0{-4oO`&ayr&8nwj%BVO4M^gxqrZRCw?#xnBaPfWK`8vbr&!4xk@cIXC zg{5xqP@6FQX;Jv*Qq+}g4D-j&JVYeZSx?BIwO(lnsb8PkjGG!@N4^h?qG!<{yiSI04@L2b^%!snO&#KS_`g``US zTM+%cVTi7&E)oTt160t+sD_7)!+w^KnW)|ZQS>}TD*6KdN+Bx8SH!LY-tl1T^P3A= zJ##^Yw`Uv+RlKF8rMXXd+?cQp;c~#9B!1(@jY?u?Xi}dwwy8TlQp?lff@D9@oCQfw zp7`V2ihGp4ZkjMCd8o{)*IR=ze_Vg>5sMMi`w&6eUA_@=-!6PvV{3C&#so-LpCx#U6JlSJ!?|ODF|<~Y~v5>Ao51b6d@btnS-en?wi#I()3m76hEn` zs&eOoKLa7a$Z=ssagQYXe_jC4zLX4%jjxhbjpo>=zt76j>>QDwTq`jwqk_`Dv^_S{ z1szLq&I?^QIWwaunjy30);XmrU3rbiD_xsVeu{@Pg$k>$g zkR6aP0rSa^rhILb_L*lIk`B1+m!m7-BZ@tSqSjqX;goP@bHAjM3QLnR7HgctMhCL6eF5+$LR9VS7ci4ee)wjEq0U0no)!A zP!rbzRmV9v!;q?;1e|6%iuZxM+AT%92X$~HO%)E79Vn5jJKPi@@AI8qTYNcVrq*$< zU5to?E~J-NuCJ=%%a?GAN}t7YMzFyhmm|L}{+z)AP}qEZX~%KA=Oz37Qc_aD|CbUe zaqokDyYN6~VL4$KE?ulL>qY6}R7iYdlWE<6&2C%Yb?q$4v0Y!+M(%ohEw;U7_ip*er5jM1c!<5D6isw^FRb^3tS`8!V(>Krv)#iSAseT1AYIvEWSFo=3}G4%}5 z#ChNT0WF;Z4GC*w8Y~$lS|#1f8!V~*kSGq#R+ahkv8}8gG%f$2AN@zVe4Z<5_v}oS z7wr;O0VwW=R5@SD#>V4|#!>!j; zCB(->qXE9sE9vsZUS8^wC5e8p&)4TYIA0ISy!Fn#A@jZQHS>wY!nq5uqwWn1E(WuAjCpy-~9(-QBQ z#P)<2C{{3NqSK9Rf#qFT`ePOX1KE=FWO{ajH{^&R()Dr|RH2xSx}X~ZG8qh~+vaq! z_`i|JN;o{@BRd>O7H4Ky8OGvgf7e_$qXZ`K&R$ zoG-3)Mr4!g`-17Awklpg*etEB*&h)of|*j#}8>)#K;vhT7!Vi%TPV_)-)q2ozjxtRqe)B1j~ zOP1Xuez?vsn$i7dAWoG9;-B`h&R)cU>@HWBbzEUj^XVaei;2vWq6U0Ooy%OiDU&>I z-+eK$2p9~e#8-<=Iz@NPF_2}oC}5JUc9QCYDXFQ;{Jl@PC~}k3p6q_N?`JFa1-~N` z(O^0t3RR6ys(Jdptc(w8uyJoq1SzWqDH_bifkeLS@ydasT>|CdyS}!j_T0Ax)3ipr zpakhGSGAWuL48hM6qn|jqhCN-_t=^XE8-@A3Y*PFl`Gl%Ih)q-dkde>H~7w`%c{2H zaSpmL;U(Qq=f?9{!4dHtu|q)i>ReA)-BOpnBX{); z4cn#AP2^B+89+1g!|U)KfQT{D1PL-T1M1#8pEA9^tZmVLRC>p3kKB9d8eahJ45NKDcuuYCtSq_UH1$5wvxAYz({q=cg!4t1lrjDHX9a$y_(_Apg@oa~c zU<4e`oTRU63n7KB@B0iI+1neJ(kAcDvGixt*0+FbD8ZsL4UAMj73*;24%@NukmAE} z(mK-4%e_Lq<}qUX|Og+s=}64o;i!8B)uvGJCNsiO zEsaU=2|(vPZi^3AGbZPGP2jahuE5);0MnIkFk*820_4|WajcA)Z&_Ji57eTBkNA=H z>ECRsD+jfA?k20%d#&ruTJOC`5!RL5+}vbI?PHudKI>b3MWz~llj(%1=E)Fga{mrz zQ9c=vqNoC5OXm&GZZ%#sm|Udiw{8p|&mC&%0DpeFnF-Q7;L_su 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 JnWW*f{|o+(H1+@h diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b80b0038e..d685fcb92 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,6 +7,7 @@ use std::io::Read; use anyhow::Result; use oauth2::TokenResponse; +use std::process::Command; use tauri::{InvokeError, Manager}; const DEFAULT_HOST: &str = "https://api.kittycad.io"; @@ -142,6 +143,25 @@ async fn get_user( Ok(user_info) } +/// Open the selected path in the system file manager. +/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169 +/// But with the Linux support removed since we don't need it for now. +#[tauri::command] +fn show_in_folder(path: String) { + #[cfg(target_os = "windows")] + { + Command::new("explorer") + .args(["/select,", &path]) // The comma after select is not a typo + .spawn() + .unwrap(); + } + + #[cfg(target_os = "macos")] + { + Command::new("open").args(["-R", &path]).spawn().unwrap(); + } +} + fn main() { tauri::Builder::default() .setup(|_app| { @@ -158,7 +178,8 @@ fn main() { get_user, login, read_toml, - read_txt_file + read_txt_file, + show_in_folder, ]) .plugin(tauri_plugin_fs_extra::init()) .run(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 256cc0255..d46ff92e9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,7 +23,10 @@ "fs": { "scope": [ "$HOME/**/*", - "$APPDATA/**/*" + "$APPCONFIG", + "$APPCONFIG/**/*", + "$DOCUMENT", + "$DOCUMENT/**/*" ], "all": true }, @@ -60,7 +63,7 @@ "icons/icon.icns", "icons/icon.ico" ], - "identifier": "io.kittycad.modeling-app", + "identifier": "dev.zoo.modeling-app", "longDescription": "", "macOS": { "entitlements": null, diff --git a/src/App.tsx b/src/App.tsx index 4ab1df50f..3f092babf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,8 +33,10 @@ import { useModelingContext } from 'hooks/useModelingContext' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { isTauri } from 'lib/isTauri' import { useLspContext } from 'components/LspProvider' +import { useValidateSettings } from 'hooks/useValidateSettings' export function App() { + useValidateSettings() const { project, file } = useLoaderData() as IndexLoaderData const navigate = useNavigate() const filePath = useAbsoluteFilePath() diff --git a/src/Router.tsx b/src/Router.tsx index ae04d4867..faf638d9e 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -12,68 +12,54 @@ import SignIn from './routes/SignIn' import { Auth } from './Auth' import { isTauri } from './lib/isTauri' import Home from './routes/Home' -import { FileEntry, readDir, readTextFile } from '@tauri-apps/api/fs' import makeUrlPathRelative from './lib/makeUrlPathRelative' -import { - initializeProjectDirectory, - isProjectDirectory, - PROJECT_ENTRYPOINT, -} from './lib/tauriFS' -import { metadata } from 'tauri-plugin-fs-extra-api' -import DownloadAppBanner from './components/DownloadAppBanner' -import { WasmErrBanner } from './components/WasmErrBanner' -import { SettingsAuthProvider } from './components/SettingsAuthProvider' -import { settingsMachine } from './machines/settingsMachine' -import { SETTINGS_PERSIST_KEY } from './lib/settings' -import { ContextFrom } from 'xstate' -import CommandBarProvider, { - CommandBar, -} from 'components/CommandBar/CommandBar' +import DownloadAppBanner from 'components/DownloadAppBanner' +import { WasmErrBanner } from 'components/WasmErrBanner' +import { CommandBar } from 'components/CommandBar/CommandBar' import ModelingMachineProvider from 'components/ModelingMachineProvider' -import { KclContextProvider, kclManager } from 'lang/KclSingleton' import FileMachineProvider from 'components/FileMachineProvider' -import { sep } from '@tauri-apps/api/path' import { paths } from 'lib/paths' -import { IndexLoaderData, HomeLoaderData } from 'lib/types' -import { fileSystemManager } from 'lang/std/fileSystemManager' +import { + fileLoader, + homeLoader, + indexLoader, + onboardingRedirectLoader, +} from 'lib/routeLoaders' +import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider' +import SettingsAuthProvider from 'components/SettingsAuthProvider' import LspProvider from 'components/LspProvider' +import { KclContextProvider } from 'lang/KclSingleton' export const BROWSER_FILE_NAME = 'new' -type CreateBrowserRouterArg = Parameters[0] - -const addGlobalContextToElements = ( - routes: CreateBrowserRouterArg -): CreateBrowserRouterArg => - routes.map((route) => - 'element' in route - ? { - ...route, - element: ( - - - {route.element} - - - ), - } - : route - ) - -const router = createBrowserRouter( - addGlobalContextToElements([ - { - path: paths.INDEX, - loader: () => - isTauri() - ? redirect(paths.HOME) - : redirect(paths.FILE + '/' + BROWSER_FILE_NAME), - errorElement: , - }, - { - path: paths.FILE + '/:id', - element: ( +const router = createBrowserRouter([ + { + loader: indexLoader, + id: paths.INDEX, + element: ( + + + + + + + + + ), + children: [ + { + path: paths.INDEX, + loader: () => + isTauri() + ? redirect(paths.HOME) + : redirect(paths.FILE + '/' + BROWSER_FILE_NAME), + errorElement: , + }, + { + loader: fileLoader, + id: paths.FILE, + element: ( @@ -85,155 +71,52 @@ const router = createBrowserRouter( {!isTauri() && import.meta.env.PROD && } - - ), - id: paths.FILE, - loader: async ({ - request, - params, - }): Promise => { - const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY) - const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial< - ContextFrom - > - - const status = persistedSettings.onboardingStatus || '' - const notEnRouteToOnboarding = !request.url.includes( - paths.ONBOARDING.INDEX - ) - // '' is the initial state, 'done' and 'dismissed' are the final states - const hasValidOnboardingStatus = - status.length === 0 || !(status === 'done' || status === 'dismissed') - const shouldRedirectToOnboarding = - notEnRouteToOnboarding && hasValidOnboardingStatus - - if (shouldRedirectToOnboarding) { - return redirect( - makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1) - ) - } - - const defaultDir = persistedSettings.defaultDirectory || '' - - if (params.id && params.id !== BROWSER_FILE_NAME) { - const decodedId = decodeURIComponent(params.id) - const projectAndFile = decodedId.replace(defaultDir + sep, '') - const firstSlashIndex = projectAndFile.indexOf(sep) - const projectName = projectAndFile.slice(0, firstSlashIndex) - const projectPath = defaultDir + sep + projectName - const currentFileName = projectAndFile.slice(firstSlashIndex + 1) - - if (firstSlashIndex === -1 || !currentFileName) - return redirect( - `${paths.FILE}/${encodeURIComponent( - `${params.id}${sep}${PROJECT_ENTRYPOINT}` - )}` - ) - - // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files - const code = await readTextFile(decodedId) - const entrypointMetadata = await metadata( - projectPath + sep + PROJECT_ENTRYPOINT - ) - const children = await readDir(projectPath, { recursive: true }) - kclManager.setCodeAndExecute(code, false) - - // Set the file system manager to the project path - // So that WASM gets an updated path for operations - fileSystemManager.dir = projectPath - - return { - code, - project: { - name: projectName, - path: projectPath, - children, - entrypointMetadata, - }, - file: { - name: currentFileName, - path: params.id, - }, - } - } - - return { - code: '', - } + ), + children: [ + { + path: paths.FILE + '/:id', + loader: onboardingRedirectLoader, + children: [ + { + path: makeUrlPathRelative(paths.SETTINGS), + loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser + element: , + }, + { + path: makeUrlPathRelative(paths.ONBOARDING.INDEX), + element: , + loader: indexLoader, // very rare someone will load into settings first, but it's possible in the browser + children: onboardingRoutes, + }, + ], + }, + ], }, - children: [ - { - path: makeUrlPathRelative(paths.SETTINGS), - element: , - }, - { - path: makeUrlPathRelative(paths.ONBOARDING.INDEX), - element: , - children: onboardingRoutes, - }, - ], - }, - { - path: paths.HOME, - element: ( - - - - - - ), - loader: async (): Promise => { - if (!isTauri()) { - return redirect(paths.FILE + '/' + BROWSER_FILE_NAME) - } - const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY) - const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial< - ContextFrom - > - const projectDir = await initializeProjectDirectory( - persistedSettings.defaultDirectory || '' - ) - let newDefaultDirectory: string | undefined = undefined - if (projectDir !== persistedSettings.defaultDirectory) { - localStorage.setItem( - SETTINGS_PERSIST_KEY, - JSON.stringify({ - ...persistedSettings, - defaultDirectory: projectDir, - }) - ) - newDefaultDirectory = projectDir - } - const projectsNoMeta = (await readDir(projectDir)).filter( - isProjectDirectory - ) - const projects = await Promise.all( - projectsNoMeta.map(async (p: FileEntry) => ({ - entrypointMetadata: await metadata( - p.path + sep + PROJECT_ENTRYPOINT - ), - ...p, - })) - ) - - return { - projects, - newDefaultDirectory, - } + { + path: paths.HOME, + element: ( + + + + + + ), + id: paths.HOME, + loader: homeLoader, + children: [ + { + path: makeUrlPathRelative(paths.SETTINGS), + element: , + }, + ], }, - children: [ - { - path: makeUrlPathRelative(paths.SETTINGS), - element: , - }, - ], - }, - { - path: paths.SIGN_IN, - element: , - }, - ]) -) + { + path: paths.SIGN_IN, + element: , + }, + ], + }, +]) /** * All routes in the app, used in src/index.tsx diff --git a/src/clientSideScene/sceneInfra.ts b/src/clientSideScene/sceneInfra.ts index b91f972b8..4a65623fc 100644 --- a/src/clientSideScene/sceneInfra.ts +++ b/src/clientSideScene/sceneInfra.ts @@ -24,7 +24,8 @@ import { useModelingContext } from 'hooks/useModelingContext' import * as TWEEN from '@tweenjs/tween.js' import { SourceRange } from 'lang/wasm' import { Axis } from 'lib/selections' -import { BaseUnit, SETTINGS_PERSIST_KEY } from 'lib/settings' +import { type BaseUnit } from 'lib/settings/settingsTypes' +import { SETTINGS_PERSIST_KEY } from 'lib/constants' import { CameraControls } from './CameraControls' type SendType = ReturnType['send'] diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx index 17fdb7008..bec598108 100644 --- a/src/components/CommandBar/CommandBar.tsx +++ b/src/components/CommandBar/CommandBar.tsx @@ -1,61 +1,14 @@ import { Dialog, Popover, Transition } from '@headlessui/react' -import { Fragment, createContext, useEffect } from 'react' +import { Fragment, useEffect } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { useCommandsContext } from 'hooks/useCommandsContext' -import { useMachine } from '@xstate/react' -import { commandBarMachine } from 'machines/commandBarMachine' -import { EventFrom, StateFrom } from 'xstate' import CommandBarArgument from './CommandBarArgument' import CommandComboBox from '../CommandComboBox' -import { useLocation } from 'react-router-dom' import CommandBarReview from './CommandBarReview' - -type CommandsContextType = { - commandBarState: StateFrom - commandBarSend: (event: EventFrom) => void -} - -export const CommandsContext = createContext({ - commandBarState: commandBarMachine.initialState, - commandBarSend: () => {}, -}) - -export const CommandBarProvider = ({ - children, -}: { - children: React.ReactNode -}) => { - const { pathname } = useLocation() - const [commandBarState, commandBarSend] = useMachine(commandBarMachine, { - devTools: true, - guards: { - 'Command has no arguments': (context, _event) => { - return ( - !context.selectedCommand?.args || - Object.keys(context.selectedCommand?.args).length === 0 - ) - }, - }, - }) - - // Close the command bar when navigating - useEffect(() => { - commandBarSend({ type: 'Close' }) - }, [pathname]) - - return ( - - {children} - - ) -} +import { useLocation } from 'react-router-dom' export const CommandBar = () => { + const { pathname } = useLocation() const { commandBarState, commandBarSend } = useCommandsContext() const { context: { selectedCommand, currentArgument, commands }, @@ -63,6 +16,12 @@ export const CommandBar = () => { const isSelectionArgument = currentArgument?.inputType === 'selection' const WrapperComponent = isSelectionArgument ? Popover : Dialog + // Close the command bar when navigating + useEffect(() => { + commandBarSend({ type: 'Close' }) + }, [pathname]) + + // Hook up keyboard shortcuts useHotkeys(['mod+k', 'mod+/'], () => { if (commandBarState.context.commands.length === 0) return if (commandBarState.matches('Closed')) { @@ -164,4 +123,4 @@ export const CommandBar = () => { ) } -export default CommandBarProvider +export default CommandBar diff --git a/src/components/CommandBar/CommandBarProvider.tsx b/src/components/CommandBar/CommandBarProvider.tsx new file mode 100644 index 000000000..3baf439fd --- /dev/null +++ b/src/components/CommandBar/CommandBarProvider.tsx @@ -0,0 +1,43 @@ +import { useMachine } from '@xstate/react' +import { commandBarMachine } from 'machines/commandBarMachine' +import { createContext } from 'react' +import { EventFrom, StateFrom } from 'xstate' + +type CommandsContextType = { + commandBarState: StateFrom + commandBarSend: (event: EventFrom) => void +} + +export const CommandsContext = createContext({ + commandBarState: commandBarMachine.initialState, + commandBarSend: () => {}, +}) + +export const CommandBarProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const [commandBarState, commandBarSend] = useMachine(commandBarMachine, { + devTools: true, + guards: { + 'Command has no arguments': (context, _event) => { + return ( + !context.selectedCommand?.args || + Object.keys(context.selectedCommand?.args).length === 0 + ) + }, + }, + }) + + return ( + + {children} + + ) +} diff --git a/src/components/NetworkHealthIndicator.test.tsx b/src/components/NetworkHealthIndicator.test.tsx index 9761d102a..069bb2501 100644 --- a/src/components/NetworkHealthIndicator.test.tsx +++ b/src/components/NetworkHealthIndicator.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' -import { SettingsAuthProvider } from './SettingsAuthProvider' -import CommandBarProvider from './CommandBar/CommandBar' +import { SettingsAuthProviderJest } from './SettingsAuthProvider' +import { CommandBarProvider } from './CommandBar/CommandBarProvider' import { NETWORK_HEALTH_TEXT, NetworkHealthIndicator, @@ -13,7 +13,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { return ( - {children} + {children} ) diff --git a/src/components/ProjectSidebarMenu.test.tsx b/src/components/ProjectSidebarMenu.test.tsx index da3670e3f..c1fc2d8b1 100644 --- a/src/components/ProjectSidebarMenu.test.tsx +++ b/src/components/ProjectSidebarMenu.test.tsx @@ -2,9 +2,9 @@ import { fireEvent, render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import ProjectSidebarMenu from './ProjectSidebarMenu' import { type ProjectWithEntryPointMetadata } from 'lib/types' -import { SettingsAuthProvider } from './SettingsAuthProvider' +import { SettingsAuthProviderJest } from './SettingsAuthProvider' import { APP_NAME } from 'lib/constants' -import { vi } from 'vitest' +import { CommandBarProvider } from './CommandBar/CommandBarProvider' const now = new Date() const projectWellFormed = { @@ -41,9 +41,11 @@ describe('ProjectSidebarMenu tests', () => { test('Renders the project name', () => { render( - - - + + + + + ) @@ -60,9 +62,11 @@ describe('ProjectSidebarMenu tests', () => { test('Renders app name if given no project', () => { render( - - - + + + + + ) @@ -74,9 +78,14 @@ describe('ProjectSidebarMenu tests', () => { test('Renders as a link if set to do so', () => { render( - - - + + + + + ) diff --git a/src/components/SettingsAuthProvider.tsx b/src/components/SettingsAuthProvider.tsx index b1bfba366..4e84ac730 100644 --- a/src/components/SettingsAuthProvider.tsx +++ b/src/components/SettingsAuthProvider.tsx @@ -1,12 +1,15 @@ import { useMachine } from '@xstate/react' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { paths } from 'lib/paths' import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import withBaseUrl from '../lib/withBaseURL' -import React, { createContext, useEffect, useRef } from 'react' +import React, { createContext, useEffect } from 'react' import useStateMachineCommands from '../hooks/useStateMachineCommands' import { settingsMachine } from 'machines/settingsMachine' -import { SETTINGS_PERSIST_KEY } from 'lib/settings' +import { + fallbackLoadedSettings, + validateSettings, +} from 'lib/settings/settingsUtils' import { toast } from 'react-hot-toast' import { setThemeClass, Themes } from 'lib/theme' import { @@ -20,6 +23,7 @@ import { isTauri } from 'lib/isTauri' import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { sceneInfra } from 'clientSideScene/sceneInfra' +import { kclManager } from 'lang/KclSingleton' type MachineContext = { state: StateFrom @@ -27,7 +31,7 @@ type MachineContext = { send: Prop, 'send'> } -type GlobalContext = { +type SettingsAuthContextType = { auth: MachineContext settings: MachineContext } @@ -38,37 +42,66 @@ type GlobalContext = { let settingsStateRef: (typeof settingsMachine)['context'] | undefined export const getSettingsState = () => settingsStateRef -export const SettingsAuthContext = createContext({} as GlobalContext) +export const SettingsAuthContext = createContext({} as SettingsAuthContextType) export const SettingsAuthProvider = ({ children, }: { children: React.ReactNode }) => { - const navigate = useNavigate() + const loadedSettings = useRouteLoaderData(paths.INDEX) as Awaited< + ReturnType + > + return ( + + {children} + + ) +} - // Settings machine setup - const retrievedSettings = useRef( - localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}' - ) - const persistedSettings = Object.assign( - settingsMachine.initialState.context, - JSON.parse(retrievedSettings.current) as Partial< - (typeof settingsMachine)['context'] - > +// For use in jest tests we don't want to use the loader data +// and mock the whole Router +export const SettingsAuthProviderJest = ({ + children, +}: { + children: React.ReactNode +}) => { + const loadedSettings = fallbackLoadedSettings + return ( + + {children} + ) +} + +export const SettingsAuthProviderBase = ({ + children, + loadedSettings, +}: { + children: React.ReactNode + loadedSettings: Awaited> +}) => { + const { settings: initialLoadedContext } = loadedSettings + const navigate = useNavigate() const [settingsState, settingsSend, settingsActor] = useMachine( settingsMachine, { - context: persistedSettings, + context: initialLoadedContext, actions: { + setClientSideSceneUnits: (context, event) => { + const newBaseUnit = + event.type === 'Set Base Unit' + ? event.data.baseUnit + : context.baseUnit + sceneInfra.baseUnit = newBaseUnit + }, toastSuccess: (context, event) => { const truncatedNewValue = 'data' in event && event.data instanceof Object - ? (String( - context[Object.keys(event.data)[0] as keyof typeof context] - ).substring(0, 28) as any) + ? (context[Object.keys(event.data)[0] as keyof typeof context] + .toString() + .substring(0, 28) as any) : undefined toast.success( event.type + @@ -79,6 +112,7 @@ export const SettingsAuthProvider = ({ : '') ) }, + 'Execute AST': () => kclManager.executeAst(), }, } ) @@ -103,7 +137,6 @@ export const SettingsAuthProvider = ({ if (settingsState.context.theme !== 'system') return setThemeClass(e.matches ? Themes.Dark : Themes.Light) } - sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm' matcher.addEventListener('change', listener) return () => matcher.removeEventListener('change', listener) diff --git a/src/components/UserSidebarMenu.test.tsx b/src/components/UserSidebarMenu.test.tsx index 66f11ccdd..15b9acaa1 100644 --- a/src/components/UserSidebarMenu.test.tsx +++ b/src/components/UserSidebarMenu.test.tsx @@ -7,8 +7,8 @@ import { createRoutesFromElements, } from 'react-router-dom' import { Models } from '@kittycad/lib' -import { SettingsAuthProvider } from './SettingsAuthProvider' -import CommandBarProvider from './CommandBar/CommandBar' +import { SettingsAuthProviderJest } from './SettingsAuthProvider' +import { CommandBarProvider } from './CommandBar/CommandBarProvider' type User = Models['User_type'] @@ -113,7 +113,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { path="/file/:id" element={ - {children} + {children} } /> diff --git a/src/hooks/useCommandsContext.ts b/src/hooks/useCommandsContext.ts index 393397450..536d00085 100644 --- a/src/hooks/useCommandsContext.ts +++ b/src/hooks/useCommandsContext.ts @@ -1,4 +1,4 @@ -import { CommandsContext } from 'components/CommandBar/CommandBar' +import { CommandsContext } from 'components/CommandBar/CommandBarProvider' import { useContext } from 'react' export const useCommandsContext = () => { diff --git a/src/hooks/useValidateSettings.ts b/src/hooks/useValidateSettings.ts new file mode 100644 index 000000000..98aae9dfc --- /dev/null +++ b/src/hooks/useValidateSettings.ts @@ -0,0 +1,33 @@ +import { validateSettings } from 'lib/settings/settingsUtils' +import { useEffect } from 'react' +import toast from 'react-hot-toast' +import { useRouteLoaderData } from 'react-router-dom' +import { useSettingsAuthContext } from './useSettingsAuthContext' +import { paths } from 'lib/paths' + +// This hook must only be used within a descendant of the SettingsAuthProvider component +// (and, by extension, the Router component). +// Specifically it relies on the Router's indexLoader data and the settingsMachine send function. +// for the settings and validation errors to be available. +export function useValidateSettings() { + const { + settings: { send }, + } = useSettingsAuthContext() + const { settings, errors } = useRouteLoaderData(paths.INDEX) as Awaited< + ReturnType + > + + // If there were validation errors either from local storage or from the file, + // log them to the console and show a toast message to the user. + useEffect(() => { + if (errors.length > 0) { + send('Set All Settings', settings) + const errorMessage = + 'Error validating persisted settings: ' + + errors.join(', ') + + '. Using defaults.' + console.error(errorMessage) + toast.error(errorMessage) + } + }, [errors]) +} diff --git a/src/lib/commandBarConfigs/settingsCommandConfig.ts b/src/lib/commandBarConfigs/settingsCommandConfig.ts index 7061877d1..bdadadf26 100644 --- a/src/lib/commandBarConfigs/settingsCommandConfig.ts +++ b/src/lib/commandBarConfigs/settingsCommandConfig.ts @@ -1,7 +1,12 @@ -import { CommandSetConfig } from '../commandTypes' -import { BaseUnit, Toggle, UnitSystem, baseUnitsUnion } from 'lib/settings' +import { type CommandSetConfig } from '../commandTypes' +import { + type BaseUnit, + type Toggle, + UnitSystem, + baseUnitsUnion, +} from 'lib/settings/settingsTypes' import { settingsMachine } from 'machines/settingsMachine' -import { CameraSystem, cameraSystems } from '../cameraControls' +import { type CameraSystem, cameraSystems } from '../cameraControls' import { Themes } from '../theme' // SETTINGS MACHINE diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 8468266c3..fbed86041 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1 +1,4 @@ export const APP_NAME = 'Modeling App' +export const DEFAULT_PROJECT_NAME = 'project-$nnn' +export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' +export const SETTINGS_FILE_NAME = 'settings.json' diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts new file mode 100644 index 000000000..e122b0f7a --- /dev/null +++ b/src/lib/routeLoaders.ts @@ -0,0 +1,138 @@ +import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom' +import { HomeLoaderData, IndexLoaderData } from './types' +import { isTauri } from './isTauri' +import { paths } from './paths' +import { BROWSER_FILE_NAME } from 'Router' +import { SETTINGS_PERSIST_KEY } from 'lib/constants' +import { loadAndValidateSettings } from './settings/settingsUtils' +import { + getInitialDefaultDir, + getProjectsInDir, + initializeProjectDirectory, + PROJECT_ENTRYPOINT, +} from './tauriFS' +import makeUrlPathRelative from './makeUrlPathRelative' +import { sep } from '@tauri-apps/api/path' +import { readDir, readTextFile } from '@tauri-apps/api/fs' +import { metadata } from 'tauri-plugin-fs-extra-api' +import { kclManager } from 'lang/KclSingleton' +import { fileSystemManager } from 'lang/std/fileSystemManager' + +// The root loader simply resolves the settings and any errors that +// occurred during the settings load +export const indexLoader: LoaderFunction = async (): ReturnType< + typeof loadAndValidateSettings +> => { + return await loadAndValidateSettings() +} + +// Redirect users to the appropriate onboarding page if they haven't completed it +export const onboardingRedirectLoader: ActionFunction = async ({ request }) => { + const { settings } = await loadAndValidateSettings() + const onboardingStatus = settings.onboardingStatus || '' + const notEnRouteToOnboarding = !request.url.includes(paths.ONBOARDING.INDEX) + // '' is the initial state, 'done' and 'dismissed' are the final states + const hasValidOnboardingStatus = + onboardingStatus.length === 0 || + !(onboardingStatus === 'done' || onboardingStatus === 'dismissed') + const shouldRedirectToOnboarding = + notEnRouteToOnboarding && hasValidOnboardingStatus + + if (shouldRedirectToOnboarding) { + return redirect( + makeUrlPathRelative(paths.ONBOARDING.INDEX) + onboardingStatus.slice(1) + ) + } + + return null +} + +export const fileLoader: LoaderFunction = async ({ + params, +}): Promise => { + const { settings } = await loadAndValidateSettings() + + const defaultDir = settings.defaultDirectory || '' + + if (params.id && params.id !== BROWSER_FILE_NAME) { + const decodedId = decodeURIComponent(params.id) + const projectAndFile = decodedId.replace(defaultDir + sep, '') + const firstSlashIndex = projectAndFile.indexOf(sep) + const projectName = projectAndFile.slice(0, firstSlashIndex) + const projectPath = defaultDir + sep + projectName + const currentFileName = projectAndFile.slice(firstSlashIndex + 1) + + if (firstSlashIndex === -1 || !currentFileName) + return redirect( + `${paths.FILE}/${encodeURIComponent( + `${params.id}${sep}${PROJECT_ENTRYPOINT}` + )}` + ) + + // TODO: PROJECT_ENTRYPOINT is hardcoded + // until we support setting a project's entrypoint file + const code = await readTextFile(decodedId) + const entrypointMetadata = await metadata( + projectPath + sep + PROJECT_ENTRYPOINT + ) + const children = await readDir(projectPath, { recursive: true }) + kclManager.setCodeAndExecute(code, false) + + // Set the file system manager to the project path + // So that WASM gets an updated path for operations + fileSystemManager.dir = projectPath + + return { + code, + project: { + name: projectName, + path: projectPath, + children, + entrypointMetadata, + }, + file: { + name: currentFileName, + path: params.id, + }, + } + } + + return { + code: '', + } +} + +// Loads the settings and by extension the projects in the default directory +// and returns them to the Home route, along with any errors that occurred +export const homeLoader: LoaderFunction = async (): Promise< + HomeLoaderData | Response +> => { + if (!isTauri()) { + return redirect(paths.FILE + '/' + BROWSER_FILE_NAME) + } + const { settings } = await loadAndValidateSettings() + const projectDir = await initializeProjectDirectory( + settings.defaultDirectory || (await getInitialDefaultDir()) + ) + + if (projectDir.path) { + if (projectDir.path !== settings.defaultDirectory) { + localStorage.setItem( + SETTINGS_PERSIST_KEY, + JSON.stringify({ + ...settings, + defaultDirectory: projectDir, + }) + ) + } + const projects = await getProjectsInDir(projectDir.path) + + return { + projects, + } + } else { + return { + projects: [], + } + } +} diff --git a/src/lib/settings/initialSettings.ts b/src/lib/settings/initialSettings.ts new file mode 100644 index 000000000..e9c7a27e5 --- /dev/null +++ b/src/lib/settings/initialSettings.ts @@ -0,0 +1,15 @@ +import { DEFAULT_PROJECT_NAME } from 'lib/constants' +import { SettingsMachineContext, UnitSystem } from 'lib/settings/settingsTypes' +import { Themes } from 'lib/theme' + +export const initialSettings: SettingsMachineContext = { + baseUnit: 'mm', + cameraControls: 'KittyCAD', + defaultDirectory: '', + defaultProjectName: DEFAULT_PROJECT_NAME, + onboardingStatus: '', + showDebugPanel: false, + textWrapping: 'On', + theme: Themes.System, + unitSystem: UnitSystem.Metric, +} diff --git a/src/lib/settings.ts b/src/lib/settings/settingsTypes.ts similarity index 52% rename from src/lib/settings.ts rename to src/lib/settings/settingsTypes.ts index 6d18e0e03..54384e877 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings/settingsTypes.ts @@ -1,10 +1,6 @@ import { type Models } from '@kittycad/lib' -import { CameraSystem } from './cameraControls' -import { Themes } from './theme' - -export const DEFAULT_PROJECT_NAME = 'project-$nnn' -export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' -export const SETTINGS_FILE_NAME = 'settings.json' +import { type CameraSystem } from '../cameraControls' +import { Themes } from 'lib/theme' export enum UnitSystem { Imperial = 'imperial', @@ -21,6 +17,7 @@ export type BaseUnit = Models['UnitLength_type'] export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v) export type Toggle = 'On' | 'Off' +export const toggleAsArray = ['On', 'Off'] as const export type SettingsMachineContext = { baseUnit: BaseUnit @@ -33,15 +30,3 @@ export type SettingsMachineContext = { theme: Themes unitSystem: UnitSystem } - -export const initialSettings: SettingsMachineContext = { - baseUnit: 'mm' as BaseUnit, - cameraControls: 'KittyCAD' as CameraSystem, - defaultDirectory: '', - defaultProjectName: DEFAULT_PROJECT_NAME, - onboardingStatus: '', - showDebugPanel: false, - textWrapping: 'On' as Toggle, - theme: Themes.System, - unitSystem: UnitSystem.Metric, -} diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts new file mode 100644 index 000000000..dafaef761 --- /dev/null +++ b/src/lib/settings/settingsUtils.ts @@ -0,0 +1,88 @@ +import { type CameraSystem, cameraSystems } from '../cameraControls' +import { Themes } from '../theme' +import { isTauri } from '../isTauri' +import { getInitialDefaultDir, readSettingsFile } from '../tauriFS' +import { initialSettings } from 'lib/settings/initialSettings' +import { + type BaseUnit, + baseUnitsUnion, + type Toggle, + type SettingsMachineContext, + toggleAsArray, + UnitSystem, +} from './settingsTypes' +import { SETTINGS_PERSIST_KEY } from '../constants' + +export const fallbackLoadedSettings = { + settings: initialSettings, + errors: [] as (keyof SettingsMachineContext)[], +} + +function isEnumMember>(v: unknown, e: T) { + return Object.values(e).includes(v) +} + +export async function loadAndValidateSettings(): Promise< + ReturnType +> { + const fsSettings = isTauri() ? await readSettingsFile() : {} + const localStorageSettings = JSON.parse( + localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}' + ) + const mergedSettings = Object.assign({}, localStorageSettings, fsSettings) + + return await validateSettings(mergedSettings) +} + +const settingsValidators: Record< + keyof SettingsMachineContext, + (v: unknown) => boolean +> = { + baseUnit: (v) => baseUnitsUnion.includes(v as BaseUnit), + cameraControls: (v) => cameraSystems.includes(v as CameraSystem), + defaultDirectory: (v) => + typeof v === 'string' && (v.length > 0 || !isTauri()), + defaultProjectName: (v) => typeof v === 'string' && v.length > 0, + onboardingStatus: (v) => typeof v === 'string', + showDebugPanel: (v) => typeof v === 'boolean', + textWrapping: (v) => toggleAsArray.includes(v as Toggle), + theme: (v) => isEnumMember(v, Themes), + unitSystem: (v) => isEnumMember(v, UnitSystem), +} + +function removeInvalidSettingsKeys(s: Record) { + const validKeys = Object.keys(initialSettings) + for (const key in s) { + if (!validKeys.includes(key)) { + console.warn(`Invalid key found in settings: ${key}`) + delete s[key] + } + } + return s +} + +export async function validateSettings(s: Record) { + let settingsNoInvalidKeys = removeInvalidSettingsKeys({ ...s }) + let errors: (keyof SettingsMachineContext)[] = [] + for (const key in settingsNoInvalidKeys) { + const k = key as keyof SettingsMachineContext + if (!settingsValidators[k](settingsNoInvalidKeys[k])) { + delete settingsNoInvalidKeys[k] + errors.push(k) + } + } + + // Here's our chance to insert the fallback defaultDir + const defaultDirectory = isTauri() ? await getInitialDefaultDir() : '' + + const settings = Object.assign( + initialSettings, + { defaultDirectory }, + settingsNoInvalidKeys + ) as SettingsMachineContext + + return { + settings, + errors, + } +} diff --git a/src/lib/tauriFS.ts b/src/lib/tauriFS.ts index efaf8d43c..895685ea3 100644 --- a/src/lib/tauriFS.ts +++ b/src/lib/tauriFS.ts @@ -3,12 +3,16 @@ import { createDir, exists, readDir, + readTextFile, writeTextFile, } from '@tauri-apps/api/fs' -import { documentDir, homeDir, sep } from '@tauri-apps/api/path' +import { appConfigDir, documentDir, homeDir, sep } from '@tauri-apps/api/path' import { isTauri } from './isTauri' import { type ProjectWithEntryPointMetadata } from 'lib/types' import { metadata } from 'tauri-plugin-fs-extra-api' +import { settingsMachine } from 'machines/settingsMachine' +import { ContextFrom } from 'xstate' +import { SETTINGS_FILE_NAME } from 'lib/constants' const PROJECT_FOLDER = 'zoo-modeling-app-projects' export const FILE_EXT = '.kcl' @@ -26,39 +30,100 @@ const RELEVANT_FILE_TYPES = [ 'stl', ] -// Initializes the project directory and returns the path -export async function initializeProjectDirectory(directory: string) { - if (!isTauri()) { - throw new Error( - 'initializeProjectDirectory() can only be called from a Tauri app' - ) +type PathWithPossibleError = { + path: string | null + error: Error | null +} + +export async function getInitialDefaultDir() { + if (!isTauri()) return '' + let dir + try { + dir = await documentDir() + } catch (e) { + dir = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions) } + return dir + PROJECT_FOLDER +} + +// Initializes the project directory and returns the path +// with any Errors that occurred +export async function initializeProjectDirectory( + directory: string +): Promise { + let returnValue: PathWithPossibleError = { + path: null, + error: null, + } + + if (!isTauri()) return returnValue + if (directory) { - const dirExists = await exists(directory) - if (!dirExists) { - await createDir(directory, { recursive: true }) + returnValue = await testAndCreateDir(directory, returnValue) + } + + // If the directory from settings does not exist or could not be created, + // use the default directory + if (returnValue.path === null) { + const INITIAL_DEFAULT_DIR = await getInitialDefaultDir() + const defaultReturnValue = await testAndCreateDir( + INITIAL_DEFAULT_DIR, + returnValue, + { + exists: 'Error checking default directory.', + create: 'Error creating default directory.', + } + ) + returnValue.path = defaultReturnValue.path + returnValue.error = + returnValue.error === null ? defaultReturnValue.error : returnValue.error + } + + return returnValue +} + +async function testAndCreateDir( + directory: string, + returnValue = { + path: null, + error: null, + } as PathWithPossibleError, + errorMessages = { + exists: + 'Error checking directory at path from saved settings. Using default.', + create: + 'Error creating directory at path from saved settings. Using default.', + } +): Promise { + const dirExists = await exists(directory).catch((e) => { + console.error(`Error checking directory ${directory}. Original error:`, e) + return new Error(errorMessages.exists) + }) + + if (dirExists instanceof Error) { + returnValue.error = dirExists + } else if (dirExists === false) { + const newDirCreated = await createDir(directory, { recursive: true }).catch( + (e) => { + console.error( + `Error creating directory ${directory}. Original error:`, + e + ) + return new Error(errorMessages.create) + } + ) + + if (newDirCreated instanceof Error) { + returnValue.error = newDirCreated + } else { + returnValue.path = directory } - return directory + } else if (dirExists === true) { + returnValue.path = directory } - let docDirectory: string - try { - docDirectory = await documentDir() - } catch (e) { - console.log('error', e) - docDirectory = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions) - } - - const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER - - const defaultDirExists = await exists(INITIAL_DEFAULT_DIR) - - if (!defaultDirExists) { - await createDir(INITIAL_DEFAULT_DIR, { recursive: true }) - } - - return INITIAL_DEFAULT_DIR + return returnValue } export function isProjectDirectory(fileOrDir: Partial) { @@ -309,3 +374,44 @@ function getPaddedIdentifierRegExp() { const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER) return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`) } + +export async function getSettingsFilePath() { + const dir = await appConfigDir() + return dir + SETTINGS_FILE_NAME +} + +export async function writeToSettingsFile( + settings: ContextFrom +) { + return writeTextFile( + await getSettingsFilePath(), + JSON.stringify(settings, null, 2) + ) +} + +export async function readSettingsFile(): Promise | null> { + const dir = await appConfigDir() + const path = dir + SETTINGS_FILE_NAME + const dirExists = await exists(dir) + if (!dirExists) { + await createDir(dir, { recursive: true }) + } + + const settingsExist = dirExists ? await exists(path) : false + + if (!settingsExist) { + console.log(`Settings file does not exist at ${path}`) + await writeToSettingsFile(settingsMachine.initialState.context) + return null + } + + try { + const settings = await readTextFile(path) + return JSON.parse(settings) + } catch (e) { + console.error('Error reading settings file:', e) + return null + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index ce323c06c..de655fc6d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,5 +12,4 @@ export type ProjectWithEntryPointMetadata = FileEntry & { } export type HomeLoaderData = { projects: ProjectWithEntryPointMetadata[] - newDefaultDirectory?: string } diff --git a/src/machines/commandBarMachine.ts b/src/machines/commandBarMachine.ts index c61f9b030..5e690dd30 100644 --- a/src/machines/commandBarMachine.ts +++ b/src/machines/commandBarMachine.ts @@ -450,6 +450,7 @@ export const commandBarMachine = createMachine( const hasMismatchedDefaultValueType = isRequired && + resolvedDefaultValue !== undefined && typeof argValue !== typeof resolvedDefaultValue && !(argConfig.inputType === 'kcl' || argConfig.skip) const hasInvalidKclValue = diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index e3497ea5f..332fadf10 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -1,40 +1,41 @@ import { assign, createMachine } from 'xstate' -import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' +import { Themes, getSystemTheme, setThemeClass } from 'lib/theme' import { CameraSystem } from 'lib/cameraControls' +import { isTauri } from 'lib/isTauri' +import { writeToSettingsFile } from 'lib/tauriFS' +import { DEFAULT_PROJECT_NAME, SETTINGS_PERSIST_KEY } from 'lib/constants' import { - BaseUnit, - DEFAULT_PROJECT_NAME, - SETTINGS_PERSIST_KEY, - SettingsMachineContext, - Toggle, UnitSystem, -} from 'lib/settings' - -const kclManagerPromise = import('lang/KclSingleton').then( - (module) => module.kclManager -) + type BaseUnit, + type SettingsMachineContext, + type Toggle, +} from 'lib/settings/settingsTypes' export const settingsMachine = createMachine( { - /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIBBY4qyrXWAbQAYBdRUABwHtYmLP2w8QAD0TsANCACe0gL5K5THHkIlylKgCEAhrDBUAqtmEduSEAKEixNqQgCM7AJz52AJgAsAVg8AZgAOEIA2fxd3XzlFBCCXf3xfdlS0kN9vGIiVNXRmTSJSCnQqAGEDAFswACcDCtE0Wv5iNi5xO2FMUXFnWQVlVRB1Fi0S3QARMAAzAwBXYmpJzFqwAGM0flr5K07Bbt6nRH9w-HcXcPcI8PYAdgu0oLiTu+98EPdQ0-8g8N8gu53HkRgUNARijoytM5otqAAFFoAKw21AActUwHsbF0HH1EFkvOxiSTScSXLFBggrsk7r4AuEQuxAd4oiEQaMitpStQAPLYABG-AMtQgGkYaAMaHm7WsfAOeOOCEC+HCiTevlu5JcYReCBCLhSFzc3m8SWJrJcHLBY0hPKoABUwBJqAB1eq8XgabHy+w9Rygfp69jWjDg8ZQ6gOgAWYBqPtsCv9+P17Hw3juIV+Pn87kiGeeVINIXwuf8rPC4WiVZcQVDhQh3N05mEjHksDQcYTuOTSrp+Du5ZC3g8bizbkp8QCaaelwep3YTP8vnr4btDv4UCgpCo0wF8ygVHhBmwYGI3aTR0DiFupfY-giQSC3iflZfepHvnwQV8Lge93cX4qxCO4VGGbB+AgOBxE5eAcUvANJEQABaXwQj1ZCQLvUkmXpFwzStYZYIjfY-SvJDXBHLxa01Stc0yIE7j1NwKW-NUAl8a4-DuZkwKUIA */ id: 'Settings', predictableActionArguments: true, - context: { - baseUnit: 'mm', - cameraControls: 'KittyCAD', - defaultDirectory: '', - defaultProjectName: DEFAULT_PROJECT_NAME, - onboardingStatus: '', - showDebugPanel: false, - textWrapping: 'On', - theme: Themes.System, - unitSystem: UnitSystem.Metric, - } as SettingsMachineContext, + context: {} as SettingsMachineContext, initial: 'idle', states: { idle: { - entry: ['setThemeClass'], + entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'], on: { + 'Set All Settings': { + actions: [ + assign((context, event) => { + return { + ...context, + ...event.data, + } + }), + 'persistSettings', + 'setThemeClass', + ], + target: 'idle', + internal: true, + }, 'Set Base Unit': { actions: [ assign({ @@ -42,9 +43,8 @@ export const settingsMachine = createMachine( }), 'persistSettings', 'toastSuccess', - async () => { - ;(await kclManagerPromise).executeAst() - }, + 'setClientSideSceneUnits', + 'Execute AST', ], target: 'idle', internal: true, @@ -125,9 +125,7 @@ export const settingsMachine = createMachine( }), 'persistSettings', 'toastSuccess', - async () => { - ;(await kclManagerPromise).executeAst() - }, + 'Execute AST', ], target: 'idle', internal: true, @@ -151,6 +149,7 @@ export const settingsMachine = createMachine( tsTypes: {} as import('./settingsMachine.typegen').Typegen0, schema: { events: {} as + | { type: 'Set All Settings'; data: Partial } | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | { type: 'Set Camera Controls' @@ -174,6 +173,11 @@ export const settingsMachine = createMachine( { actions: { persistSettings: (context) => { + if (isTauri()) { + writeToSettingsFile(context).catch((err) => { + console.error('Error writing settings:', err) + }) + } try { localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context)) } catch (e) { diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 9f38c8123..94769b716 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -31,21 +31,22 @@ import { import useStateMachineCommands from '../hooks/useStateMachineCommands' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useCommandsContext } from 'hooks/useCommandsContext' -import { DEFAULT_PROJECT_NAME } from 'lib/settings' +import { DEFAULT_PROJECT_NAME } from 'lib/constants' import { sep } from '@tauri-apps/api/path' import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig' import { useHotkeys } from 'react-hotkeys-hook' import { isTauri } from 'lib/isTauri' import { kclManager } from 'lang/KclSingleton' import { useLspContext } from 'components/LspProvider' +import { useValidateSettings } from 'hooks/useValidateSettings' // This route only opens in the Tauri desktop context for now, // as defined in Router.tsx, so we can use the Tauri APIs and types. const Home = () => { + useValidateSettings() const { commandBarSend } = useCommandsContext() const navigate = useNavigate() - const { projects: loadedProjects, newDefaultDirectory } = - useLoaderData() as HomeLoaderData + const { projects: loadedProjects } = useLoaderData() as HomeLoaderData const { settings: { context: { defaultDirectory, defaultProjectName }, @@ -54,18 +55,11 @@ const Home = () => { } = useSettingsAuthContext() const { onProjectOpen } = useLspContext() - // Set the default directory if it's been updated - // during the loading of the home page. This is wrapped - // in a single-use effect to avoid a potential infinite loop. + // Cancel all KCL executions while on the home page useEffect(() => { kclManager.cancelAllExecutions() - if (newDefaultDirectory) { - sendToSettings({ - type: 'Set Default Directory', - data: { defaultDirectory: newDefaultDirectory }, - }) - } }, []) + useHotkeys( isTauri() ? 'mod+,' : 'shift+mod+,', () => navigate(paths.HOME + paths.SETTINGS), diff --git a/src/routes/Onboarding/Units.tsx b/src/routes/Onboarding/Units.tsx index d95d35bc5..f14d185ac 100644 --- a/src/routes/Onboarding/Units.tsx +++ b/src/routes/Onboarding/Units.tsx @@ -1,5 +1,9 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' -import { BaseUnit, baseUnits, UnitSystem } from 'lib/settings' +import { + type BaseUnit, + baseUnits, + UnitSystem, +} from 'lib/settings/settingsTypes' import { ActionButton } from 'components/ActionButton' import { SettingsSection } from '../Settings' import { Toggle } from 'components/Toggle/Toggle' diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index f4f04be57..b4c6ec219 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -2,7 +2,12 @@ import { faArrowRotateBack, faXmark } from '@fortawesome/free-solid-svg-icons' import { ActionButton } from '../components/ActionButton' import { AppHeader } from '../components/AppHeader' import { open } from '@tauri-apps/api/dialog' -import { BaseUnit, DEFAULT_PROJECT_NAME, baseUnits } from 'lib/settings' +import { DEFAULT_PROJECT_NAME, SETTINGS_PERSIST_KEY } from 'lib/constants' +import { + type BaseUnit, + UnitSystem, + baseUnits, +} from 'lib/settings/settingsTypes' import { Toggle } from 'components/Toggle/Toggle' import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom' import { useHotkeys } from 'react-hotkeys-hook' @@ -15,17 +20,22 @@ import { cameraSystems, cameraMouseDragGuards, } from 'lib/cameraControls' -import { UnitSystem } from 'lib/settings' import { useDotDotSlash } from 'hooks/useDotDotSlash' import { createNewProject, getNextProjectIndex, getProjectsInDir, + getSettingsFilePath, + initializeProjectDirectory, interpolateProjectNameWithIndex, } from 'lib/tauriFS' +import { initialSettings } from 'lib/settings/initialSettings' import { ONBOARDING_PROJECT_NAME } from './Onboarding' import { sep } from '@tauri-apps/api/path' import { bracket } from 'lib/exampleKcl' +import { isTauri } from 'lib/isTauri' +import { invoke } from '@tauri-apps/api' +import toast from 'react-hot-toast' export const Settings = () => { const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown' @@ -54,8 +64,11 @@ export const Settings = () => { } = useSettingsAuthContext() async function handleDirectorySelection() { + // the `recursive` property added following + // this advice for permissions: https://github.com/tauri-apps/tauri/issues/4851#issuecomment-1210711455 const newDirectory = await open({ directory: true, + recursive: true, defaultPath: defaultDirectory || paths.INDEX, title: 'Choose a new default directory', }) @@ -302,6 +315,59 @@ export const Settings = () => { Replay Onboarding +

+ Your settings are saved in{' '} + {isTauri() + ? 'a file in the app data folder for your OS.' + : "your browser's local storage."}{' '} + {isTauri() ? ( + + + + + ) : ( + + )} +

{/* This uses a Vite plugin, set in vite.config.ts to inject the version from package.json */} diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index ef6fb2eb5..97c3580c7 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -6,8 +6,10 @@ import { Themes, getSystemTheme } from '../lib/theme' import { paths } from 'lib/paths' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { APP_NAME } from 'lib/constants' +import { useValidateSettings } from 'hooks/useValidateSettings' const SignIn = () => { + useValidateSettings() const getLogoTheme = () => theme === Themes.Light || (theme === Themes.System && getSystemTheme() === Themes.Light) diff --git a/vite.config.ts b/vite.config.ts index 4b0f719f6..bb568f8bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,13 @@ const config = defineConfig({ }, test: { globals: true, + pool: 'forks', + poolOptions: { + forks: { + maxForks: 2, + minForks: 1, + } + }, setupFiles: 'src/setupTests.ts', environment: 'happy-dom', coverage: {