From 518ec4f605a7677cc7d3ad7a339875a37835c1e2 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Mon, 13 Nov 2023 11:55:28 +0000 Subject: [PATCH 001/119] feat: added /enterprise upgrade tip (#12207) --- apps/web/pages/enterprise/index.tsx | 74 ++++++++++++++++++ apps/web/pages/insights/index.tsx | 1 + apps/web/public/static/locales/en/common.json | 15 ++++ apps/web/public/tips/enterprise-dark.jpg | Bin 0 -> 210797 bytes apps/web/public/tips/enterprise.jpg | Bin 0 -> 123629 bytes .../pages/forms/[...appPages].tsx | 1 + .../ee/teams/components/TeamsListing.tsx | 1 + .../components/AvailabilitySliderTable.tsx | 1 + packages/features/tips/UpgradeTip.tsx | 8 +- packages/lib/hooks/useHasPaidPlan.ts | 7 ++ 10 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 apps/web/pages/enterprise/index.tsx create mode 100644 apps/web/public/tips/enterprise-dark.jpg create mode 100644 apps/web/public/tips/enterprise.jpg diff --git a/apps/web/pages/enterprise/index.tsx b/apps/web/pages/enterprise/index.tsx new file mode 100644 index 0000000000..9854d5c6bc --- /dev/null +++ b/apps/web/pages/enterprise/index.tsx @@ -0,0 +1,74 @@ +import { getLayout } from "@calcom/features/MainLayout"; +import { ShellMain } from "@calcom/features/shell/Shell"; +import { UpgradeTip } from "@calcom/features/tips"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, ButtonGroup } from "@calcom/ui"; +import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon"; + +import PageWrapper from "@components/PageWrapper"; + +export default function EnterprisePage() { + const { t } = useLocale(); + + const features = [ + { + icon: , + title: t("branded_subdomain"), + description: t("branded_subdomain_description"), + }, + { + icon: , + title: t("org_insights"), + description: t("org_insights_description"), + }, + { + icon: , + title: t("extensive_whitelabeling"), + description: t("extensive_whitelabeling_description"), + }, + { + icon: , + title: t("unlimited_teams"), + description: t("unlimited_teams_description"), + }, + { + icon: , + title: t("unified_billing"), + description: t("unified_billing_description"), + }, + { + icon: , + title: t("advanced_managed_events"), + description: t("advanced_managed_events_description"), + }, + ]; + return ( +
+ + + + + + +
+ }> + <>Create Org + + + + ); +} + +EnterprisePage.PageWrapper = PageWrapper; +EnterprisePage.getLayout = getLayout; diff --git a/apps/web/pages/insights/index.tsx b/apps/web/pages/insights/index.tsx index 8782acb0bb..1526bb6660 100644 --- a/apps/web/pages/insights/index.tsx +++ b/apps/web/pages/insights/index.tsx @@ -46,6 +46,7 @@ export default function InsightsPage() {
r8Fp| zfQY{ZysrDY?$7DZ%E_6a z{hc54|F;5u_W*9=qO0R}VW8aupx;KrxQ+I^4?qt9V4&R);18i=pkZQRV%1!OxsYm6q-_YidrRr#7r%=;hatwtUpyXmS2h48*FJLjba}a-l+4 zj0#$_DYfbf%WKoC4h*W2**;a~ABB5}7#>P|r#INIuLcv;^v0kzzh-!>Dk_73PyRk3jMg#ghX{|qo%nJ9FMLT6=u-VX(n8Fn4-H^gSPngL-m$kJMcKV0m zzQq&)>|`LggD9q@nP+YJgDrn+x>b2XMo1Oh*>u1JAt)e3D?_I@stMV4dpuxhDrG4`C!-g@E9A5ybu(2B$a!Hyki71oFGM&Sd*Jlcw^h%$tfjRF zU)2}CnoaW!UFr*NwITjitF=*T0&l(Vt0F{qBW?6y{d&qw2`zQ`VKKl#f}UjG+Rb_v2oQH> z1S;3JPzxB&D&MNJ)yu(BfhgDIKXbl!>ag7+F$rF_Dzse=qfW@o3Y~C@*sP98HG~#p zXQU{Y@-p~B__rN#XVC3?YPZAGqb;z7%XV~fKi0Vp2)V8IMAg69lK_kktJglAPF~(B zuc&8BOM^VkQm*mVb5eY|Fv_1iIJp4B8Xk_yj4MCNj|~xdP;OCK&cb@m#nFri;MLRY zhZ_rE>!;B;NV=?BY|cOpr!b$=+R)a!-CY)Uj-{M#9^158Ktso|hw z_r~1323cTOW`kbxtxDEmr$*s^z!2 z7(+GhCVd+T9H?lxNFn8X3ETt$)@X);mO@mTM}U5KsDE3OyzjcEpdKL~z3w!%0)ddb zySpcrrQ#@Tl)S%a)9rZ*@N*brcF&)gZ6+bdGFk#Vl zGc=$v8n6Yfx(v6JRkERhnAKFL6;Nx|I&ExgR%TbkOR%F|+1~I`-$-|Cg{dixMtXJS z1JA*M_z4|Swb?bE!i4+aT7eO7H^(IS>gikp5YrlD4t$)y$zd?7Gp>rj<0*%$E~>#s z!R0CGLiaXLnVFdr9@vb}?i7>LV#|B@cI+M|+bo)nE0=4j#)Ylh#=PCY#sc%CukzQ1 zL&_cI9ynNuT0-mPrusQwGjx_|^V5?$D-I8tGT0V!;g@!GX$U9}B`Z8tuD92qBfu=@ zXSp2?*5pfmy1@D~v!YE^cOa{{xFEASxe8JRh17CYRej!h8CMa#1>WB7umu2lR_9_z zTCyOPZE?|{a%>%FIwdvJKT6yM^KU$A108laM!ET*&eAfhu!~T8yN` z@D^l6tUXu|qDabbZIiK(&)mTT=$+i!qe6pviP`;L3pDj%GzbLEMX)Z=5{! zO<*u51+odx$F4%cfszmN(r?N~vk&kUBqHY+FjO0Iq)n}stF18qD&V%FvH8FB$xl6D1a zr^|ofjF|opF4~VHp(4I_kWgrhtj4Pfr?LCWbCZuyT*z6;cvi! zdCgECPR&{fAdqqTV&enl&!wb8e7XOl1we8K2a7@VINQ^nX`teVp>jWR%F)`14>FsO zkzu$MdmkD`x`>3C7T&MpCy%b|#gTaM#8q2y~STmQLl0Xos5jOn6N;$oU8d~ekhsSKo z1<8`I%YZ7O*A29wZ1HSNTT0ezher+>aYBAPc)bq>zn*;+zudpfy!8GJkh+X%n%Ow$ z$otHFY_|cxnFth@5~Kav+`(Hfjd9L+(%I7q{3W~*$S8)qD0%hoM*D@uqDeOpFB)Tw zT#)uQiObN0=Q=i|fnsCJcM(YIVJG_x!{QfC)pZPHQ$uT|RzMAoml4}la6VHGHZ|&G zSCc@~^fTJmvzafm5}=ta;d+ViI@kTPC2`TSKR_rQwmaKdh?7Jf1$CZop!)EiMBG6N zCRh06{S+HHI!MV6XKxm#XL!xg8apv;k!m`tE%G&~-kggeDwOJj4ePw{@F4Z7zz0xk z$?SPHkKQLHe0DYeR`SW!tNRa2p zrJ^9p5C2pE00_L;v{GBwC4l7=xs=G#+!MFQ9nzVdpnnv`T;AH6;o?jIUfm;i9fx5V zJ;_fU_3ZO`gr9%tGq3GUps%N&W8IpjsIy2QV2s7b8AdHP=*s~__+Pq@{Q>!(bF^c? zcfun~>?r{MLC&+Ip#JMe8v z?%VsKkKQwUWe%J9L{#+~@Nh$NXI+0b0d3Za3pUHpEG!(eaiw!mNKeezTpTYo!D1?# z)2L>sZ)5g3TWrV?UrB1OfnhNM_$Cmv&Qe?BNl!|z`)vWKOe-9hVxvIdK^3nFS%&K& zxakqL>Ze0lajb%RrVBz;-T24NQj}L=_r9NF4 z{s^MumWudUoqKN1xUg~BS+uu)zAyFqrxfMnL11Un?&&*}mc;N{XVH6zTaYOp7h6e~ zxxU#GJ-s+mDt)q?Xp9xa!67Qd5Sa_`&y@>7 zu_z73w?t~Ks8SP5C_OCVKHCDzViTCWdvvT>KkO>a>MhKxsXBYgsKPlU5T6=EgR{(M9gg9t+CL6c(Qx@& zvw1TIjmaQN@dZ?6>!q)%{4c^dmYlG*NEX{Tb%WLF7?usE|e)1@L;vq77(T-7Zrg)o_RLG z&X)h6{4XPO&ib*njq`9$z321!?G;0VWR{f7DVIze*1manVQ(6Z2LUCLNZI6Z`w3t; zjZ?9$ZLuzc9?3us>ww@+R6apr_W>i&W!Q?Vsy@@Hb?4P?oG5b@KX9FevU)@O+aFwR zl7Ch6I`HfE3Qw{%!r4}bL05M$hGwF`aFn2Yx5=Ao(iCX1d7j+5uAj&AY}9gmu^)_I zYxxaU@dY9PgNMK{k-n>j%4+sJT3TCtybHGXSGBEeLDnXA^2HtQV`36{j4=z}V3oet zFsP*xvGw2R#HETBL)Eh4qu^-u1QrjE3KdRGVP`l`^px<8qB7xrAT+v3o1~nh zu27BEb*6#9la$T`=>+$H^?^le*pwg48&k7cuy&VX@Bp03M-nu0j>fG!VrQ2d zQp6rXSXZbh_<8C7hCiQ_iA&i4LI8| z)Nnh)L@&aeKYMaKm9Hd)&&VTH6P{wBIemL{>an&bmr!6t!~L^3!eSP6f6im~j%QnT zXJg_gVm|Gi(A80!^`A?L->w=SWz;3=F>3jU`ZvsSt>8x?0DuReUS_=gMz8bsIdMA0 z+|(%h7(-pcto71DhY6y23TtaFe4eA@yTV3J$CT_<%?Q*6$Zd5*Nz#T5VnJQl-=wVy zngF@`mWMeY$lqc)^HL;PmZYYASw6B$rutAt&nUcs0=gp9gvV(NPyVoZ;+wEjU35{h z^*%5D++cA1((0fS(aw-y7qk8=xotz@)~|;nztCOJ=_Q}H9e#?W9P{bA%Fk%YJ!k?$ z)*A(K5#}q$loG{PihF`3`%64iZ-4j9Y_F<&P#S+QOcf*VhF%(F1Gbo;)lG%)L900r zPwxBqZrZT4#7|I}IH?8*xw~z&sHHjcp{2d%sv1s1HP;mYimuy;19^^GI!~)x2ETXu zub%{rAuk5U&w>km%tL++=5W$=6cOC1{%j8aNc=d9a;Jl>|2peAyy3B7+5@Wigrec2 z$w7JNNjF;$CC*kJ=&oRXOKq+Aak8!ud#oJfLxs6MO}5u$1?-uZ;l7s7I$F4|fmd=; zAl&F-2#_BZMgeU_OeOkik-e1#UM%GrPaZL0FM~o*C={;&{`L>&1IDH=oR+Uq|8^Kb zG(56%%;Fi14gRoBSB?y?z;2>w2#e1VlUV@w$EqqwfW6#`)FQu%(~oNGzn%8(npCBXWs49v}+ zC8XayFS0okWsNyS>6X-X&%c4y#Ub<^e#i_DGl8n~Uk&@E{~X7g$1lkIVdeV83<}SP32m%GVtFo z9(tJzeGa%XXnzQ2PU%3{TUe9p6-dH6(?iA0nTdlft~drE`hd|u&H94}H*0I0VQmBr zqW*-vypp@Lt~?_JgjFD>GcQV zx>c}u)urFBH*O#8%?^Tk|D0Z~5@76ng$2qj-bqgYPMgXInSyB4k9t|gTO%CtoF0Th z6Yje)-gXpYEohBrR*QyMR7;E~(j*b2zBsV9A{41=wU|s3BKoe>1IM^aAGCrQ@ge zcqosW+8;Y*+-zi?>Xg@zK7(o>Ej;MMBV8S)4EFAwb`mRp-Ik_Rb9*@C;=kWo-pU_6oBwQ%Tu&FQKHe@@@l`@-1!OBJ z`Z2nE6q6fhHyFdSJ4-91rkQj|R+Le!V>*B=nzt!hY@!qvpn?UNMG3g<4)pR(vW`v^ zfxsT_0ZVx&znE@d>;?o;HdprK^2OR+7SP(jW^GCU#FA{CQRgVxwbTE$v6EX!zPiAv*YO<<+oT1W})KzZ6MV%u1h%s{| zxHrF7s&UR6-#S6l&sf$+gI!t8<8y_5Qv(Bu1`|%~G!71}nrd0BuP28ZOJHZe0XFA~ zo55&jWBZ->YfCfVn#HdmG1i`-jAAJ2032-Yo44M+{6~#H9;533=JTku7&gU62)nc| ztjyYOTI03gV}<7kn9l@dqOP2Ez9xkS3{=0#)6SU^uEWcQU^O(XVe>I0X_eo@zbnc) zONn9Tk`WhQzIhflin>fQQfX20c#&q!Hrsy`6g(T$eJ-_Oo_Gm8=k93w_H`p{ zbC!@u8kobdrpWI$dxRa#7u8}rhv&IW*MMjTMX26{RrC;CBvZU*p1*y+;$mNLx}5xrw_?sl zCD${Em3oW11LRSve84lo;Q_x>wI*){9YuTbd47a1c9a#r-u?Fh)tt`W*@L(DhK|gF zs~2w2Faxfq9Ddu2vYlh?p(V|LCG*@1;cp@TQATogyomj9*y6Mdq@|MuDc@|2U(_bl z4_6&Dp~kkEMA|r;%9Yphh5>;ZhV+Cc)C$TndxCB{%2i)T-BK*sGh?gNYCxPCbg2jv z!(-<1+88C{+{232v#hu4?ce^upf=Rs$NvL)!%5CgRAfRKN*e`Rt#yS8f{c&aR%=D0 zXXalm=>yf|O-gM^y<=b#=@l&v@oUOO`%ra7_HI$td)B z1`28vtQK!OFcZw-_;_R?+AQffMqGLp_+w>_AyWV0^+mw-rsThsUdrIDFto<4ehR*Hl9*UKKWYN`%& zpPjp=ML4S0+LW(Tu^8lvMhru~1WdzF@TkiO zhVzjB(gWv{?1YMYOB*yDW!jw+5v=p9?zbJ|M2{px9oH0&ES*_=iCdXRxR}j&;vxzK zXTRP0!X{^_mlRMfnR%paZ)u|_XARF|dD3ui2Ae;^QLwbtrXYH5Vrv;?^Cj}AHbtAy znSBkJuTcNz7XRn5HG@a|z|SmB4qep%OuG;8ru$mtMKzvTwtWLg-wDA~N-bz?#dAj0 zA8wPQ!)lS4DSV`bdK7n(o#vid6~w11)$PByn7(d% z6z)~M|F6;C)WpuQlO^`urHSDb+(Xx2)a|w23$lf%(W04a($h_f`f|0UM??%3l7fekZ<4D4o!$wE3_n;>vKhW;f;aKcr9ElwEch~3oiKMe_V%i!B#(f} z^j;QUVY-5ms_X~0$;Ga3W$$^q9W(VvvNlrnSY@9G-$}NN)3Vw*@^Ei^;8dml>XYf6 z1&>*e7bhCR*ty%1G=)@|Z3RF}`z&#<5|+eJ&+tAtjRZTJe%4Wt@3uc9-$VCaqEAf6 z?N_toda8;B1aF$hUUW{a|N0Hsl#;wmzNj1eSxp_x`%Am)=r@4q7uVoXo8;S-=RX)J zj*mN@Z|NsZ-PN#L2})ADt}o7~RvR{_blwVAMqU0u)l*xiec{jb6^2NDlIcHImtnc!!?5}>D3Z!o@Iq8e~; z{WJtsF|KW&F05aSbSM_^3o~b(xfaGv|3Ep}HZx@*eA=7!II5~tPt}^Pb>GJK@aT@S zK^?k4#SfcexVwj@Jz@YSHx6f*RK3bAFRf-9Iq4%doHz>JBh@w}^U&K?UDOk46M0Vz zJlOrxE-GG~aWv+*73A3eormRO37qh9_w-kLP~OKs$GVr_gU4qtf;W%ItJyK~uk`st zI7_?duE~i-(X879tqn3qWJn{8+uL_jvLw|$5p#`cO_XpM8W}YyW-%e$=vgOCRBqWM znIx7b)c1jq)KfYok~^>xxyj+GVk?<(HbcU)I*6oli0hXh2qMe6j$j`*HA(eWzbeqQjUyj$=CLqZItLW7NbVH^d;aa zV_o%ln}zmz7M*gUFpVdBYjU0y^IUW#VqEs|G+}~tiWId|6z#2uO%qKA{=!UX9EBAl zw#*hRizf3zO5k6Lyqri7VJLa`iOb*Q@_E|`wMW1|u0?S+N#BdqcXA2RHN7H0E5VEjhI7;z;g5hh9RO{|{B?85- z>NQ8FlNB1*C&uX~=#y1J$K8grFPwUKzWXu?Y=q@wFJIO7x>Z81xPK7ZIS=0K*f>yr z(Yi}{9m^usNRisi^WINkGzO49sUhZqP5HYAJ9r&VocHrQwe1m{wE=zb*cYl3b6{tt zG_Y;G+5%~T4xBc3(swwfbPigX*<$y(qMAiid@SQ=@Q3&%@k9VKGFYGvb|#I2Mcu3s zJO2azn#DK{Q1!6(@YJ-OS>zumVo}up6u^z6JRDkCs}T>)?`3v3^Z6M&5>vYRYSECq zG6Ui+79sv(;aJhQYT!surB8v*cb79OQ`cQ^vUd`ELO06((wv`gQpl{2xx>WmGdqS#istp!`TV`^*_w4_7PWb%Rs5R+zi9 z4kbOjK(Ei*bTDoOakqXnrcNm_X{*ktXg1H*2Iw#|mNYzCK?lB7p(1bXWL+N#n_i#R znOAa_m^o)){O4WU|2P^2#XhNUNR5G5L2F9uN$qUDbYwl)OmlduO@OqG8?R7&GgJ?I z^4>~~EE#axC};CXCBD*`f*itF9^^q=Sz)RD;2d$$D1$6K2fmgE8HzFJJ&YK998rN%9VSM%#2$M*F)phFUJ0*6}_1vi{E?P;Kv?K z6M@8|Kg|U$etx+u_~N_Rsdn~5WB$VU_ISclv>*vGmg6cjLxgg#AC^r78^aMC`GdP~WtEnxl3a#7I_?&h*Lg zIu^N*r|H=bY(CEh_ArQmb(63v8pLJS{%|v?lC2?2MpWxf{_wZy;m4YLI9Z|dAI=iQ z|A1T@O@ZNGlv}~1=R`Lu{E0#t8j3@YM36bMDWH>3;IxR&{qPF4Jj}INODq^iRS97vuHsR}FkTFd}sg z;?ggPJA3}*oDR!^5V0E2`CnazRXn}K=)JgomgnX9w)k_y#q;dH)j=r)eDSHnJEq;w zw=_KeZ69?a_M`_N@-yo9Smziq4X^CUUICh9J)?VR$zzWAYHnLt`E6OHJ+<-CS}yf* zY$kg0G#9CljhuEe(vnbDNyEt0{F$aurxi5 zO58s*T1j%Y%P<@TURQh_k#WWz%$1^U7TPapa(zTSTMy0 zz1pgQBZoq#=mXBRbIT;dQ{C_qy^tgBUo;)r7F{g7)Pfwc$`5VhC2XLjhmEF!)okVh zC3bm>gUFF-jNY@ne+`uzb^hZLBiI^dm*a9cfUvkTeMrr-=QJM^iyGK-NR>_$Y5hbB zXR~ud-NzuL1w1WUVrnz(hjsPk(Dq+^IiT9byYSSt?R6t3f94vnvbS^$~_KojrI z@Qoac$>8dyA1&Z2SVZ_Y9+Q}L7mnEBTccue4@EPv8+USj|-Z*owe zJT!|3cvm5`4W=s(0C2i;9$*2kV6l0x0ssK*l|w;+2f%y^08ojXe8&iD8o!cZccB~r z1=RKeH;0UjaO)j@lAB?f3=MWaeV3%L&56oRS-IjWd^svFs#geWc8Qvm-Z0~kC& zlm2fRK<5D#E7MKpYfZ0M^zJ8AA=k3M%0mSV44wf1I3K64RH3WYbq^ z6ig`qOE&6R$W3pc3P3x{9jiwrB$P(=ufYu-nnmADR+JFcdpG<)MP%VAfLo|If~$BG zOZti3QrlhZeR5Eg<6j&iwEUF3fuRQ?Oy;lzm*k3-q z35ni#XfUl1GoHzC}@)+zp3nL5myC z8WaC|9$kh0%8M%ZPwX|u?9~{i!b&5j(BZsFhN3HIgo!i?Cj|9<`tAnkKl!nuqf;=! zS1JE46g6GsNdRaV=;+uuSlC#o7ssfHhK73mjdABLA0`RuJsC|VUViDPmTt_l7OpSJ z1hk$d7GRM-4yoye-`9TO9x7-BUio(L=OH+n6vk!${I6KzUCR!ZjiZdtncm=<@huY(;cAK%e`b z^l{PLW95lji8Sc3TIv*V0gQy$>*PGy66Lu)=K|q7F`??_;ec_wIYuybZ`Q<@0dKvO zl0j-gwyPSTy8|qeG3isCFQ+im@h#wCP(iUIe|puquZhVA?se0q_3`mtss!Ax=5z>g zi6n|;vy9bNw{RqkNQuUtRN!Jiuw>=qgv4`hW%79#8>r+TP?sQ`6~qHGD#Kp7+}_S* z|5c(~@ZHN`iqdzP1zUT<82kv~h+m#Y>)NeLBa!-25R-6(^=tWfo9=JGuFh`&-nK{C zW>9zhL)r;(L>U5pIsH_dsIfAR=mY0R#}Sh`J<9~(Rz(+^=lVv)m1=dxz4=a0(XA{JYd96O6N1w<$8i% z7W3BAm{t_%jUPv&Z^!m#+P*B-vDqt$B+$+@P%&Xj$o@E1zXpV|D+19eDf*S0qp6o_zjzl!2IE9>G!NlOq2PLqR7~guuyxh$ zRT_mtb2=V{UyP|V@-VzB+afP*pS1cBA7uXkAr9yv=x+N=thE}eUrPqbF2 zI>D(8Iad>_h6$hMrcKSeN?Je5R*7e9CJ=z;ivUyl9HD^ zcfnQ+j?Z?&tZf87XMdo?!6CjsAT(OKx*1*vDp6ga708^Wc6CEv820CmwWfQmCu|is zGdz=BMHxgk^(5(gc&*r>bbYrm2A{w6syG7l0ka+WB1gTL2O48U#qr$YlBPN8V?72&2B7u~83!lh6sN-%Yb^RdwzRjU3n{cVsY6fU6E=V**RpvM+!`JbI z4`xR7eeGrOopLb`VSD-nG`tKa9miM`0d*!s(W5A1y+u8GN)nlHU4(td=HV-RwR^K+ z9+eeY-}8Nqqph+mBC+%Q33L_f_m;osqLkgUZ*?WSQ`U?fReI0O@l}cZz(NU@=AM|3 zLyZj}|&sy8l6X{6anvY?g6X6QzQy=%j>$V|TMpH|Y<77t<-ROoF{HgHw zxYbJ$jYsvw;6-&Tcnj42Fv&UF1 zZO3xBEv=0vpFTxlLGYZ#4&Ak1x#1vsX@1nzepYfzvOf7CI&Vj03Y_#8<1t(fu#-x( z)2^GEK|ic&e)f57zlmFyJ5g}MgO#xGaqz8Q(L_0g7rz141n)}@vj-ejK}w@ySSDFD zI{v}3Wozyt35&vC4aRz;JXt1}f^lD2h@$FNz`OtM*%Aax3|;X%-tI{Q{NZdM5jBIl`YOwLO54O^vOaDoX+e$+s(q02{1@ihC%-KzI8(SsFv@hgE5VzkT8>GTJ!Q@2ZcA0acJBhxJq%nq^ z>EEB#89)9F7(lwk1bi{W8d!E+=94YB9nzZ+Tz~k~kD@tLnwJD?22FIiZ-AzkZmXfF zD|um6)b*JaaeDk%ugisK6?62S-_9l7dSgU#uyvZ?Z@`z(ar$mc8-uN{}cY|glvaWZY2 zg074^8s%0pq?V61cuBLYJR%OcRqZ2NW`)qy_HQ$zn;t(MD9JGOp5Dns2- z#b6ybN$wtvsoo0z{%{pTJrrt^&T5}}KfJGHMR zb+<^XTbb{`tTi{rv>lBVDyjAcBwueK>9rM0SAIM`d8|y~w5>@@NlD;avQ^~|ZSCHp zqRA){N%2xcy?G0$Zv3g(`D~{2?%uLy)x)A~oy-w}giH9u4`@q$$XqRv_-SGBonO@7 ze|@k&Y0#{2_yzWQ`eU)qHvtYp((HwyfgV`XzL?~ae^qNT$%)$lOXQDJ|AgTuWmEqN zV)ZVp&uINahCbn5cfxLCq1QUP>>R$dJ1f6B)C)lb#|L4}XO-|azA-Wyze^dTV)P+< zYT9={e*D#7fAW6PQxBX=o8ZNjS!0>k?)Ob!2-)rm*fWC3aREd`sr9rIFhnoR`R)qL z{&u(dOSEQFE>QaqMTcOIg~zPDbV@@y!aBZmN@vgMagp>mc2#UFbpd_s%u+Z(!P2GQ zgUZW<$>q{@{cZdUH^l0uHEJ)cSX?va^59an3w9DZeY}G4b9ElMOx=2dkDBO2?y65W z-nv6vE)gxIvuy!f@kUBicxufGGs1ZTKS~~$(2AHRxf?l16j1ci_~d@yeQCtxm~F6z zuTn&1wBBl?1>cI6ZY{E8c{CEGh)Yw3@Ko*em@<9TMW^h}9nbVx@kxCMV(c@6IiZ+@ zQ-3u>jID11QVhGHHAffG`UF>(@~!d9sCi?j{)Fq>DAesqLd z@9Ud<@e=ELYi-O!7&CR2bVR~nx=ft}5g6}Z8TZ*#xSa14{HXt>O`sXHn-yA$$p^>k zW%19DPfP#A(f4-Rt_{@v#N`8y7zrS;!5E5hKj z_X}z!`QwiQjy25Uq@47JtNXOQp-|yxwsK<$=iW;i38s0GCOWwx_o;D3VEvHWECWiZ ztjwV;6RPXsiuKZN;a35KKr}hSLiSVlmv7wko3pC;GdF~5E$=8Lxt-cQd_y$$f}{-)fuy4v7_SV{p2v zYCzGKl_hgpFTbtNr*~X6&*Z1dbJ9M9l_EpO$8jAN#GFtDR&K&V;60aHQ&v&-T^u0R z$3*Je3Byv7D`TzaJ_XyZo0j8VfpXchBezixRJ+!?B=hjvB%W4sPW(T9@=qqP4WNrK zGoRJEKF9Hj=*7533iG6i$0MbU?^!3jX~OPFMnhhA%b%2q^zlZu)a0?4E0BAX=k8Dq zB%{&e`-LX}C;8f$!i%03KcO|-dHLG?S)dSKrAYE7`t0KzHji5}vF@2PyTrQ{CC)!Y zS^16W*Wx!x&r50nuIb_kqxt8)JyuwbJYuK5m~&o}cK3)tl`Y+c(cC4pRRSCRPFZ4N zO(-MBQOmKg)*c7!U74v9q~$Ie8?gIDItNb_5SN{8L_LC!ztXMO{j&>N`%@vc#o zG!)*mOJtxN>+n0p4jg;iMdx=^HoHyjquJ$Gu9Ks?5~mX!X%BCwTiXbo@W8$$$kY20 z^};Dhw6r6{an>wD;Hz*wgXKn0?1mhn!ce@Y_hkL`uu$v&FvNw&%ai^FV0KGQNv%9eE&|;xNFZ`#De7D42B|rkbWQj_ zYIJEDX~{kxbma^;joFAK8^rmws7+U_Q#Ggmct6Of8Lh0+z3ncA2xNj!9r8VjxfnCl zH(=4rFG6{PB0_z02weu#wlQJp(_V&mp1;URuHt<;M|3{X!KqIi>sV|8Jaq8yPTUeD zLBlC3pH&6H%T$BA^Y;^zYAS`|_laJ&+4zhho%?qzV}E4c}qQ?ZU9K5E4*e;@tFVc&@|5cRfy# zYK2->$V7(9DjUncsx#VCR?Y(FZOkvfOO?ohB{S$&xhRP{5=Vz#vs_39wf7Uutwyx_ zoeID0RxIf{!I4lOE3oEQ_xtYBY1eOH)BA15-MQPa#f<}2KNkcXe6JDeerh_qV^hat zFtC*&E?c^+PV*jC{iQ*w*8FtEs-bS-H$curp3}lDWuGOdO>r|OzQ6fEeNWGI4_Et# zx|ba>xQ;M^4*3&FBw2`ByKf0FuU1PICg`3_j}*RTIM`YUxFlKjyBb{Moi7$&JbT0; z!nH`TV5x!S4zVL=V{wOU_1{sqG1+Q$lj(0EQ+kvaX;mo2VVd_L40(Qm5gBzLAxSSY zrd{`K0UPz(S7=+hnUz_TT2+DRdIS>2{W9U4SsuB=pX?F%GAw&F58RT{2rgtm3j+-i zubl8W0#m+6E@sQlh)mkQwmos=S)I%bEwuU(G0Z4@3Ra^u*RQNtH8Cl^9^k*C@hl(jKjq%7z06Y81Hs z@ggE+urcyN;p0HaN?jX|a&FqhaD`BXTbYl027ku{tWpJP0>lcLr}{-M8cV~PteUey zp$HFzBpgvpcg_yDo#`N;V1$kAcY`&eMw!$L#jo?f0nTML_gTZb7r0}<$o7MkcPPoBA?6}51h#**dLr?Xfrm``he*Az~8*u5KIcU5*S7j z9D+)*qjf7izFcbOA{qyQjgnER7+#Y48c30!C39h$lMX`<$*qPLv_EhciD3S?Ixxpr z3a!l&4=)Fm({SiUYS^`GOdRh7v{paFjyg5T%_B53Wk$`N$_%N?)Xnws2xm#7%D44P zT^3E2NbZamHFbPhEV#3a4CfxAb+25xJi)o2hZ`#9sM2{!SLdv8GqiWbnClB({^SaR zdRAmv58+~)s-<~lQfyfQ`^c7OfP%L_yL}~I*{r7+iJJ_|8vUm3{@7G^b@v$w$%=Kz z_si&fN0Z0quIumN@Sl2#wqs5^e$jsN4v(};W!(yOf-oNWi z^axj+0zPdLGZOxedB~Yy@l{Xr{uyhB%9kz;@l-_&`;2g}y>?bT2z-7-x^gL$*~oaxP8IwH@Mm-nA@MPmUivI&ySOnaCN0CqnPLaKa!q%}-UmwF zy;+J@a^-L1+y7czYfEyE8FH^h1+l@9VuL-yo=j<=5vneat$|sMA7dphAcG z7^BX#v7|+~*RmZmmm|yJ0)4mh-3e+clB$Kek9zZzS^m;k4ZAZ;80_zhs32(U9cJ`& zoz`aD+>ZBxWX`>B?VpzJ8=TmYN=WhKuZia54OJQa28;#e5sh`yO_!*=+$KWZS!@QY z{07`#0WoW5((U?ar&>J$fkp>7b_p-E*Rx^g;rqOIg(|U=Gu_!6UP(?^vo+*?Ah^#~ zQ+c7iaq!8MP9Kr)w<}gH^~z^VBl4*y>VUz!th2AUuED>mPzhlpeu4DbbaG2UDdn^V z!!g^~U-n_61sYhtkbwthzAxf8PI^d@*66lzV4fHC?c3cd{{$(lt0kVqQ%ov$?X#e# zZ$$&$!8FREJ@s{{->yG2izWgSmDnBBkG)tyY)l{RGra#gQg(7+lAwXA5aza55N(>n z`XxE{v1YYmq0FSCUS$CPr?Jd3a6?bnvGMEDCy{n{1CyV$RLp)AV=8|NeG>BB?k%;> zFGnVO0(#D)Lo}MQU(w#0wMkdn3eNJHfxAGR>9HsXJxG3K*_^J zZ2El2zN78Nb3vCep}N5S##NG{BS|xd!A0w5arbD5?`-Pjtc(b9h)1D0#gBnDJ{_E9 zja3ZxZj_GEtB>{alb$~?@w9%KAm@rK)OD7iRDLr^ZM)N4E~;I=B`Wm2CiW3p2wvEn z11R<(V3LT?O=cHfwmG>iF4EJm&R5SWY^SeUy1jd^kkRG4J+^L1fs|8sU3XWuyT$|P z_igUd@eT3YY#4a4niI#C+E0k;85LHN%I7&nio&*{olItjHACi_1uY}^)j4{8;6#Qn zxeclB5%}aCMp1$upV1jUVEHZZTDxpg;nd46E zmp`7Pk*m+;;?v4G++x&0fQlyE(3V;+Uj+ppE7t!wgh;vJO>931`%3m?i8Pg6+FYp; zbmvz06Jh=pAodt|SKET^JQ4FiSn!|mD~ zs~*W5FQc06wN|&SZxYvghQV(P%HgKT``kDfna-RIIV~E;Mz%4U32BYS_9G+utzu(I z&2zL}K6KszqS>N2J&`ZZz1ep}9O%Eyo?10Ca;sa_2c{~tE0Ml5a`x8sA#Jng+kq}V z@3Jain|iUoq!EVWFOr*wG0A|MY!SB1I-|Z0=z+H<2aS1!TgpTKKib|qs;TX37xq{{ zq$7fKr6U~@s=%Qow7`L=bdV0A34|i`Cm_8Dkrvva1%tHEgH)wOY6vA@=p7>h(wmoa z-uJuzec%1YxcBantg-jV+AGQ4YtB9AGv|EP1_sml@T?+}{I-qj{;+h>v~CpIqw;5-P*UG(R6#_$Bhj`wyM7ov&y9o>(xhg9gbBWbw57(Qsk*ARxg;-tN0`G zRzVmvZ~9rQl+g!ju{-@dA-yL-E;WIvQTc_}0>_neZDbm(EJ$tnZCyUSdl06)fwvnp zCA{Elv>SZE6ZjNpRpYTh$vDY+aHV!kEPX(?R*}+!APYQ{nd@%{c~yW<^K$T$^cPo$(x7eKUTLe zMeMQ_M|z6;r@Kg-Lchg&Wrk03(zrRtRf}xY)5fa5Cl*Fw+A1fV+-DHG78o94A)a2UiB`kpZqD z`^#Bed_T=L`cr8LAtuYH4C26&G3T_p$-Gf2dNiichzDG;xUp^j`-UdB=~?^l8?ii) zFdO$W&ojeLs!i-1K#lu8pg>g02Gi~rJf9!3x45`OaK}+xS-su~exOh`N+MyqKA}2z zVD*}NE1j``)#)z_G1RAfW>pu`Fbs4V&C^*}vGIM_t#Dne4&dT6gEP?MmCp zwlw6tELpP3(+~$Tcr9mvorq*)t6p_8ykI`rH+#W~w;j2oQ$5*}BfjKX?%!9yQdK&h zQ)x~x;*wrs*_}@uEd0>X_)!hIaB6iae-+sIF=D)tlYp3$@B=)GYS^50cd93l;z`iR2QFWX zc7?V>_KvMbow@i%+$QCu4>rCSmD7x8?ML3-jq%dveYZtXDl2%G7>mBGA*nN!)rcN< zz|l=fX_l&_+^v5U1HR2@>a1BRU&fo}q63d@e2o8?M{7)sWM?U*Vr1^7OIFf;jEu!H zPc?U|auuMx=|RV;v@%cW@_D+Wg0-0|r>>q7T5ZxGgR^eOd$U=*NfYyVFXrV{@(ID4 zgU`8B;m1q(1aBkw&q1W$+JkT3y;qp$J8w@%lR5CH)-rF5%|rHLENZH2{#{blnw(y?P=;?Ss2G%x!jd}C9c`J|>|KB2HbrNHMwqtZTBQK{-G z;r9*dlGMF`vrG2N(C1TXU7b_J)XEUvgXQMpUL};&ORR$h@7K<#u^%%fXr3`@gNvg_7hwve%D^yTBlv@K_MNwN)Wr7a6zb>S57pHY{|Im^63x9IBlFIdzxHCFzcS#v=@qjlJP;iy0wGauS*^sI4883IOnmc1Q|At^=En+#k@bZ zvS=sN0N2P#&|o#ramMYBn~sM|qZ67xM-v{=*IY1do+6hq3AK8F~1JM6G=#oi`(HH<|LZRQKSud~?Xr2DXk&6*JG$5)zE#G@q&_^1{w6Pn z?QVSi#?92o)(_c7g%Y{_+9HnyTVy{~C-BbG$e|29L%ACwS$iNa1`>UU&_u2q zK*CI18}<&@Fg(HPw@|ue=>E4UA~SYIF_Yn3C$sD=Z(G+i6EVI?Ug z&j(GtneR8Jj46e>7m|V^UN)DomJJ43*wcONE(4$r4&WZHKF@fsxX}x-y|U@@N(#T~ zN_V9($F6P6TqP&ur%jd&&B%?PCr|!=H$A{p#4EIjW-I;?wTW{)(Np>yEz+jX;@uKY}XRO*ci=PDU;{rkPh9^vt#hd?l!e z^n>3woQLXB8C_e$VNiV7=6iy}bZjwIcOfmfCZ=z?++G4Cik!dVGy$rL;L}AZ$u^>I zY->27-kjIUU)N-6cPC43aWt{K!r%M>6KmogaXg<@);js(l`wp~g@?z)MwWbP?^W}8 zR0Hrz;w?IepDQUdRFWs|36;NZtb`%c z{M^n9hBG9=6y?lmk1yj(%FxB-fXP%nxd07CCGp$gMu8mAGzl>efOjzXtfbo8P}GOu zqb*HQbr(gKEE=OdjK;k6Js&rEo-Q)S6I(~?jLhZ13Dqgq_&56392ys@Y4o)er{1lW za&9GbQlCKh6ATaF4bVCHkH2bb+bAOS&NcRwQIaZbEz`o|@xw>An|bSH)y=}!m5M85FnKAfs7K8d z`TJ>dEsn2mfGVT>V6Lz96Uxaj zB zTJ+;C+x7J4M$g&nXkZb4tAb(>x%U?_TsCCHtTviK_eg4G<1vGKg-E;Ur>G~_+C)`{Kh{0 z+>jw)z@1N3Q7fJVO*-RDG|iCFA`T8~8XaOM)?1W`E3iOoTI-$=#m6q1`cMlGZ-}HJ zwKv2)sUUF&)DIRzSP&09WGgP7({fm3?#<@so9HHNqtK)rbqis?ab+X5PcD|>?gbuzaUC8FL`Jtv# z#-hwE>B|WR*6hR8bXr8Z_i4KKAEJnMCsy6AL-`W%yB-$7p(Q&$&ENg>VQ3GNJ{j}% zkM?8mYv?@dfhv8v7X(G&mf&VEuB57)=qhhHxH~^jXGqtkz}guI)r53#P=_%Y-MN%wmpP{64xXYsXGin z6{zuFC5TqkHpSa&e_7mcduln4V+F~9mKwv*bdt3 zC@ES5g>vPLtwHs+Ltlpgt9w_6-=<0kA2y1GKeb(*&sJcYRGGH8&ijzX|Gq)roC;P| znSA02F;soeo^S-%UoJqSo+`5(MieDpD;)j7^J(Ujl5&8#&{L!@s9A+OR`Z;P^4a$< zn%C-MIhWCZZAUaXz&YoEce>+&w%DtuCUo%K6p;`@uqY36UeLO0I&M3clhf^*(#}00 zG;3^XeFEMXh!SaKLk2?7C{bJ3m z@XuUUiClInw8uBtyM$BGeory&yctWI)W7J=d;dSfLMf6*_ltMRta+acyh7w$P;+?m zY|iR&5H%Zyt=izq3PcBpU|k|vx|09aGk}wj8)!3Kwj2Hm3=l0seV&~F968R}iK;6Z zvq~PZ{Gq6-oxV=McKJXFp6xxE>#kzz3!eo?iG*nyyUj0elz0v@GU^~d-&CchpiRem z!!x1RajINQSwKEptoLq9@#c0D^Qv~PvK%INdbN;xC(w@)v=e9f!{}!{lVfjk0N)Gw z>ez+eO^xwUyhe$)3?-D0iE9)uZ0v8c=zC0`Ev$o=N!OF(hS(JNbeqW;_4e z7k=X{8(K6)fz5|4zOoDe>WaJV5YQe{d2(eCw0R-pW5A|1)>~LqPk{&rv(I>zmZlXB zi)ClN{7)obl*T!=uBm@xEkXwN_d?w~<{m8;1FJYKEyFRkko$y%o9D&)warG^TcP;X)^OTN;ed`GpzAvWoK2 z_cf!|R+!R=HbTct@li#tM-Cgbu}4y-UTUF?aC9ajjxj-=cd+y$YE540-r{a9x>=?C zL%K65PH@*u+@@b{$9E9R_?k4it=0EHHv|}_e^}NvZh_Jx5n5yF?W*(N>@&Nt!P1?Dr0l3A#QZ(A^92Y$G^kMjJ2`oAXUQ zYCxBsqr|S8%AL%UMEC)5Z+tMELe0*#ocU>Y*yWNS<-JtIZHd2BCM#JWA6AfEKu5c; zF@ULxFsPI+@|8tSP{L^)rPk+A~I=FTZdIjeTQKrFQV*`kdSZ4LTcCJNfzSL{)#jm8DKu zocS-0;PO2X=H?^f;@fNgIG=}d^-3f|M`aqev13D)sjzWXNv&uy3HX5yCHx6Y^ka8o!+v z^uzk?dYiaeO|M$24D~x#kQE3s6V`3$0Zlu=s?xd)Oo-P`fdI8qnK9tL+14ntja32N zAL655;u$|nT(yo~$!d66v?9=(=>hVQ;=`O|e~_T zk+!_7>K1_@e;pfr2Ry>JA5>1?oy%u&t*;7Haj1eP1#zW8WdE~A|NN$Q)joDmYRL>v zuoOvHPw`4l888T)V!=v;X1fT~N77}MOqn;v5wCQmzK7e(2&B}+eZo;bGED4tgd z)F}sdho!>vMRB{36-|wC`=0r%etzzltY}l_Cb#W&6T=VNA-O!<_xF|x>C^m{j&~C- zxBTV=Yl4kphb1k!2!M9r{sG)qckFPFoAQwOG(mj0hC)^gV6KxPTC?{eJW?ZF*RjrA zEK^?~9rwSxQ(sJ}ZIfaIerAN!-r6&WplAY~mM<=6Nx&pTyb9hQFJC_e$PMp&gG+md zTdP}8bPrUf=SD*LKGSWYi?Bo7DJi;gd);`}s@hQY=RVg$NDn-OKn%4FOx&tzw>o1V zkk;S%2I{a~AUUt6F#7c6_s!r3d#m3DFoVyGiY4B_VqZ5TY`-g&vj3uG9D|36@(1c& z-e>t@&~PN7Ml{u}`Kqn=^F}4Ru}u?cwvb(gGVAKz=-9VwyeRtkc5WbK_(AMIA{6s? zl`dmaJ^m}oT1>S57@J+)`}@YsMe0m~ePM#|gW{wNC60ovq3Kr3{%oLR5lqo~%I&-` z(hc1{%y7VLn!x97Y}Ff|PY=hPpLp5~INI^}F?W}s+{)PGB_u9n*N}b`We3GvNqJNB zz5c5GMG4fFVj6(h`tkhM>f`8rw5%((i;`MyFeA9 z@p({KUx#Rxu3-n{_gZn#EI+#awmT&8v!58Pa=h0(O?AxLbryWL`56bi&1nkY1YFFf z=F=Mp6K~QpFeg}B-!GlC{JTL+d|@*7KFhu*mAOgVlN2dobv| z6=Bhvc{ceRvDiHi-?JV+0DWR*j0*M`{3{ zpuBypaBoBLF!wzh>P^X~-~1N4_d~h2ld_6eC)%Zau7&$RZtM9qCFe0rZr-dmGKtw^ zz83t0C~bL}5j&iXG5PUGsRA z_8(I>@5DMHA7oagRY|cOa6Adp%Xv?RJ@N-|3e-?_Gh4=Ruga2LSKr9wMbq(m3K;I% zFuiN^p`o?{bajyDyl|2lPm#uR?nQ^P8Tw@LXArU5OSJkIUacy|0#(y60XeD zqC&v4Yder%cF(k5)cZv&7fvp9KLkeMQ`lJKRYjson*4RK39G+vfCYCAtW2eJyE#mR zhh83PWjWXDN^Wc}o|cne_V7VNa!NgQ9}Enuee65v4v#DE#?SO{5lUXTaX-z?-W_== zy0(7VpygbEKfAwkT*Zi8(zZSi3SsMGWBx0S)lU2kYAq%Q z?rFR?m4EW!P4N9pa)t(^E|$P|1D(5k*N^UA^?sV)U*=xVYiN>e9$Pod`VHhBO4h$i zw0!a|&ZwniGDV{6r<1nKGbTg0700c_9$j9YCcyof_&_?y^PV;!Ux>$D9bq;}st$RJ zgqsF)__VzE3985ZQgSH6nH*Bn1JqVF$F=T{{@irbIRU(z4PMv1PH&jkeLsc^is^4} z+}2LDe2^WVpI24XcS}>mKF-Ft{~L{${U&*nmRCR4xk`knjm6qaXnb7UP5NggiTCoN*7( zBQ*oZMrfy_VF4kE(yrGk0~w40V1rs^@1^O(hUp9fo>(m#ZpDd#3`MKtq`;||I?6@# z&)#2_L5|Dw)=(Dy&KP*$+qVH_MqpqTKxN@K~os572veFBi! z-O0Q9mIBzWtRQWF#;Slw^=~?C`mD&dIWh27nTtT&amwM%udq)Lsdhsr3?Ld=t|=g+ zec?~Q6i1ajs$ppk3o&AFTMHk3K6a$>H$BIY+}xZA4a^L}Jx4+Iz`v3`t2h@L>=>1? zG@81)+yYSM?Qd<_d+dMA`1=M0qfh&^5}%3ujH_4X-=yTl%JgrtGOS%%VW*;N8f^0F`?e_)=|= zc#O_gQQCh39HST21bObw9;_USvds5kaPvI{tG2Lkx9a!F!$W~a!(2cGaVvF?woFCL zQ0m>utZeZ{HVToj4^<9xsS(`?lB8DDT;559c~l6Lh@Hw^ysD7Zm}pof_0A}=oiHrp zgP(vlE=yo86{~YE)~Pu-v$!8k=0P9IsJDmZ5?YJTPp(9ez7`3&^Oeh3=fewvd=2qC zj;sXp*%)TSUQE{9f2`YX_18?Dr+k7`8Xz578Uc{}naYHCT%2elcW5gwg8fUd@sg@Q(B6CVg(4{ZQO5V60&!U46>LBy?|!^A zI#Tt5i=C0;z-&mFD|f``$Iz!UB{^PcD-PK>CC+f87eHkKm*cSPS-4HJD~N(bt3u>=sQvl?xrq_^eQA=R(pR-)us!iI*)XSyZ4dH% zuwZZ*X*Q8$hE0dds6Wk=N4nL-81gUTGqv-#Zf66eyp4i(o3KiwNBgRZdBrL6xG=q($Lkl2!gG3AW1>&|irh2hGPR~yqO=}<8s_fb_e+5L zF%ykrDK@HT!rfpno5`nnuWSn9cJeq}Zms6ifvdBGL=(jOLWI^4b_K&V%Of%&sF?SS za$>rk0}5razd&t)!;+11;*}f)NlF$STMbYrh(1ecCDg>wtz8AbVEa^v^yj>G@ z*4Mu`kOa~cekq7k^{x*2u459b{5TV0z9u5`%&|`hF!}ASI8&Qko)FkSZO>(E?afQDm%5t zVdIYc-@QtugmpyIM4wew`Ev_sS{BhNs3jpB);VWPK5{i;cK9L*>63rHN^WG5Gzloj2~We3zsnq zd$L$NxwzRkAwdQJY90GPzWCoaEO<@^|7*H_{cD8Qjc1oYA)M6pqby$t&xSP1ck4}j z$lb$=Q2Kr2^Bu`z0Jz<-(#`j3G(e8ep%Vi*71Z%>LS>f?=`vCR*6AbNV83tJ*UL#& z`s&%OWkNxxZO{1(9s=91L!^#t=F?EhR~7|mgwi{G1<*9lgJTDns@q3JP2Os52(O4= zB%NEn2)YRNz1J^YPEpVco$&Kov=2~5&vLG4x2odJrxJi#Qt_6Y>aM+8F4}PNE5o;^ zC2_q{E?wh>gIiQobfdhUh{rYi|EiS>p<@T~(-5``Y9E*4@U_43kXCaKBz|5%PhqACnW*Ic&YD{Mi!Ho!=`Qg3duv8eTkFoXtdr zW(w_to<}?wb3V}97Z>S;^Y&z`>L|Mu&7Y5Zu7pApJdF|zJ8rr}zseJ$`uzXh0Gy0n zHa*8HAT630Y)9;c%Povk_@tGe*+r8-H%;dNzV@$dnHHUt!|66VfscaNryHVhC$P<> zR{l#6NhHzrkGD=)n>~ISNvm?#Ze=sA?F~1-ZUVL~8Wfj65wC~Hlb>;~lKLjLB zp6Q&!sBY5)~rhi=qC|U=gu!S#kJX5Y7w>1<}0cdK<1WBe5ZNm>tCrRk~eg(;<|7pt6Sg7 zXpudWU-;4WoxJi?`mcRKuiDfeG8wUkDl*Jj$otujeKDK+$jAJgN<4MnW*nZf&MKNS z7dy5+_Q`w8_Y!FPsJ8a_N_#!FeqT-#Q=vugq_Cp(w1&a6MFa0#MxV~*F042Shy7W; zc=-o0pH|;w4A8q%=m2wp!aRKkRyRML)3eB{A@DHu&ed14e2W)MN8&qqNcCPNoM%#Y zeR0crer~oOYdWzU(3{WsZbU>5yzdE-fyc%14^~TAZHIboXUU1B7zew-&cqveY3L}s z5f95rjlL6*lM6eLo~XI0;7o}bTQ@{?>C!i9M7EOqgO{fn5_>Sc_fmyh8HU7GxG;?sHzT*UO; zxE^5Q=KK4Goz*%-%EBu=EX|Ei&xR7)u|e#h9UlB{i%1tT9I+*h6nw9Qwr{!oAe80b z0VL~lBJRbRmtV-Uca!IE{+lk~fO0souu++{t8_W1xDZaZ%AL;vJX%K5&6<{Irk`9d zfCP3;Jzn`|5N{lI6MLdcALS+}+-AT<+Z#L^AT10QNk&_OUtZ4`Nuhr|sb`b>6#JI( zjj_xpFxf!xD?-a;^rVlWw@KiAm;ENug)~PAJvKNQpt0yaLekUvkEux3wX&X#%VXF~ z=BWF@fc$9y(aO?)(U5l_1JhNvCZ_+Ddv~dZfYJKU$Wvz>mzeJ6sDWBY_t4C7>HcWq z;SoMTgd1Z$bbe6ie;VK5e|+uMUyeYS6tK!1>sqcJt0N+A7jem9ITH;W)OPNeA?|(f zAtz5>K^#!!VKr(_WRhp+6|bj|PU?5=hQ(yud63N)=N|W7mH3ZnR3BtBvxAedT|8a^ zQR1(I!KDX^eO8o&$MKu9x|BF6^38N7YLtc&q&@ffzuU7T{67zpUC0}yND}oIW*|buz-#+M-GLBR z#RNBW3*I;U=j@Sn?E!l|$F69GfMbr`)uP@z%nN5qNlzvXEghSiV2T&LXJQ`lui_)h z18Vdc*_e38u`Dd_`)H%B+AAH7%gT`3>i4#s>6`jXB57zWlY1~e25Ek${MRe%Ig+u> z#AJ~oJK^%W@mTJpIVEpj5Af=7uf*2n8-dsIsJ_q1y7fYSGc~f5^+iY z%$zu2iV^G9`>2<(>_DPcJZ+BerwwC#JD~i6?}w$WuLyYxOyP!=a|}0vf7)#8nW8(8 z)~49-G5n=3X3Q%V{-lXL3pH*5@dfe6caa@|K`ftnd*q|;L`&pR>q3(v&7=4nYvE6a z!JGe1sB6dhGVzK|mpn||4}N$M++_ITeSA`$$-<_uHpU}GM(KjdU4H;ucKm2Q)#MwC z98h(2IC-N4A>$Fkb8~~DWLA4*W$uXg6xc-XQt<(Ete_c+2uT_vl3uGCfuAJj6DH#q z544l#dq07~g&PwszDzv(x;ddfd+gPPi>^Hh|#2e+50UXMT`t zw9vC;djxMOy?*Az>F!80+jCNfxx5OKThv+deE)hL)LAj6^s0E4J1OU5*;bLCW|1)o z#89&S1>r2Ri4dGpF zQ9uET&skDp#36}U?Xm0?5d=3@<|}$ICO7Y8au0r!0JrE%JVG_8u&9_w2HR>}^}1ri zMc7Md&a4+L*Nr?HBEQd_{9RM|Ld=*8O{hHcL&l#Wwiu( z$_tdf?76@>cg!hyI59`Uu-}c7b*hjvcS|QK)62>&5F2vU_hk*5frZ^0dlE8*!FbA; zg}ieR_`Vz593`F(c_eoKnu!pk0BBW|9ynEtvLE}NK5lebGAZV_ljWF|m+HWxs*8&l zeq562*s*aR>*u6THmtRLCTioFWY{;YBpp==^+RGGgqaOhIKm5bzfnXZzwsq@qv#yF z=d6=1pnxk}XhF5--r~;O)`t-~VDZI=)$W<}^YLTJdTy=v%JbLk%D3zd$hE0osQAZS zoT7~P`AyBH+G6(CPk}X$C$MnobX&E5(G%MfqZ9K87pZ9Qtw?Z*-<;Wo3THNGdA&I) zz9YW#NM}+;+?OWRuN5|AO-Jt{j6MaN#!YB>QFOf{`uuf-I6%TKB&Gk$l(aa?P$gIX{_o@knd6>j3+Axt+q*eA=GD z>VdX=6sv)c!5mcZIe&NimOF8doqL25L&kNir8zlUi1*4fX6>3KqWJJ*IpuZooWYe( z9LH{ZQ_D^FYTrCSdtW;?HNkBF3m>Pk+;QThoaA)M!_O4XNTyFmHAjM#g8UpN0k~w5 z+g`378n34{6)#w%_0258-ewqiSg!aW3pdK~ zl+kr{{EiFdzJiKzr4oea&gABi;qnE0K@VKiO`pB+n+Xd<@X*d9R=8G0c+( z!6q%ZUYukdwE1{Dtfw)Zpev>I6j#QzSiF{bname;zq|2JaJAm<5K>T#dbo0#gS@2q zH%h@HYOiovEJ^>7Ryh56*e~N~^fV|Jvm%uvCpViWTf3_Fpvkeb;I@YKe*`{c*s2j7 z&4r~;qMvBLI-Ch~l7qUk2RHm73^TC4S%T!T%VlUR5HT}yun!M&}dcCyb)t&dIx?w{VQhC*v_ip|{?9>RM5)EeI!wLihblc%YnlK+1 zO(`3Ue%#D|N!7GR^J;4Vb1FuijQE&rU@!BFRqKHw>$xBlDPg(ej6r_|Q!>1toTs8H zYUEvs{`}QIoksb7{B-B;FZA~vKpo7ZWXB_iH7XTD?VjdFY8^`t?*m|l+q2*F{t%cZ z9qI==eku+Sx>IM*=D`BVY~W}DioMKBMH({f+Ty+YXwlf-L=T^q4R%hdBUN2g-`zw= zJTVp~cu7w?ZZjjcABYIH^U~(C;JZ^LoLKr4Oq{l`wb$jUx~=kZcEw6J{+UxwNO4bQ zYSavZHdm7qNodexQ;_4PR5lxXX2CXt&871xVu>n(*|hMb>$DaVd$%I6p4ttpq!s1_ zm{aSkS1MV>r0kY6Ab+1xqg}<3K9+y68&}vW&;C^s$rtJ7?|mP5`+c3sivIIk5G3rN zS+0#ugM4DHj`)3p>K~(`@>jM5$fgV{vL;3(xk%jC)Z`A|)Cu^~TCy%D@Sj={PlB%l zwZ4qxlNpaHw*2Ha(SQOl#hD%Hh}$@zLS8P$fN1=H`#VTvxH=CS{Uc=p*vd`!$t$Nw+kGYd#?kA$12w4ApeqZndCjAv*(?dP`#kcMtfF3(KM5UYoC`lDCAuYcYELcb~tH!$v zK1{N!ixkV}3VvysZHwV=4Sy8RDsDmJ?I^;Kaz0nfpYqdaC;aLrEo>KdO~XF5`Syy_ z%HmPOsy^AJh&cYGxNpFq<>W}>4$b0}q;>-*K-C~88=w+S#l49s!%E4gM|_wh&3`$B zl+Lf=ViJkTkfyTQK3$f$^Fkj{eOxezVh`hPwjspIy4WtSrR#8RFTL*%%4Apfimr5e z(vh-cicG$reIE1<+ABro3z(Fi=-JIoHAq47Sf6s zTzLCRr+eU+5vZT?NZ%dZeVHzFr=3xt)=Xy(taV+Q)(PD!&e^C8^bX^F|W zu%5cba2c(I&%tZ&as>tkAGLT}(Qzg!)!kG99r72S>su$FX9 z8xQU&RJM6)AD6Ehi7GfSdpIicsdE?Mk*le}M*A9L;8yk5TR~v+N#L+U`V5w4YL?+; zuA&lvZEV6vEhWh!Fir~ zZcDIQyNEoEPihxTmtedTCx^XWj;#P}e>&!QE^DvAhxiE@fBZ7^@za450A|x11(Yqy zW~FQe-Q`>C#>g-JfYz-~J*Z6<1NOd%jKI(251MST9k^hAff^C{!p+irmuT+fw@2*6 z5qb<}C&j>cv^7}qdQ6MiT!edSl%^83Kl>Q+F-OZRx5k z%n>?3%HMr5%Y?lAOi1(qKapIB|0C!x-C46-5nk7YdcLu=Y@A1x;IuR&OLC&4p4}tw z#{ZBUy>zz~W7tj1Od8$}zAz=ei;io!$E@l6M)4f!`&?jC$CS4Gh+`Zy&Mw+n@eih9 z`tyC*TX&}MFM?Ow(*(|=440M$ShLh;>7B4`g85&XdgcCWbUV^#na7y!h+!qQ+Xq05 zSodzo$HFL@-0L+DJxZ#Ee}pj7(MBK)Qr45=uh>wfS|n4amWM}@d^zR(?j^-KV+6qs zk|wChxkV8(u%RZ{nYNxa9R*j_cTRwy*;5khTny=>BCpU0(hGC)Pl~c#Fr~Aavs)B` z4Y6#LYs$)gkygayHZY|f5Qgr_(*Cg@dLKQgh{6<+Epn+C!|8IWWrCHR`U?lPu2N`A z8;6K3`)HKNW@OOvCmuh269Weg1MaO*Xxl?UqD=DWYM^IdsivyI6W2d6HXbV8Mzsrz zi)~qHsQ<$0GoAtVyz3v14UErziu)?``0}uCO(O1qFy-d2uOFM zQ*B{3+0qR5e0~((6;&3tlPfoJNu z)&9$0^dPTCJKMMW<=~fO%ps)!%_1wQ`7y13Lu|)Mbx^GMebN2gH*d z=QeoCeL+@^ofp>zJwn5urI&1#VcDi9|EGa$)!EOAo9 z@fSH+pdA%&TpdzFh+zm>*iz4)Io8`GdCDx)m&s3hD>s0GTNSaMF1%JzF4tS7FN_x znAFsV%yN`@FsMD_{;&_^BID|wch1Dx{aW(P_8Qy3?;A9jB(chJ;l^tIhpYQ93mj8U5;0#eE0rp8m9T)wk2$;w`KCn^@& zSgrQx`MU?^2aoF{3aY{H+)dvUy~BQaIF{IsUHxnM?tx3q?$keV!3)89emxhaa3c#k zo-=vQkEE@8X36*nqMknxF{io`rGKPFA0-xuC$Q&o=UiGv#j%u>6x{Z33$S_VLP68n zJ%g6CKULdex0>J3+D^r~pqaX7uE8lhMiHn@w&a_Le9`-(d*B!$%J1qLpdc32IAmv~ zaTzQBEPmREHa30F_bxUp)=!Bsk)#>5M~>1APfS{;beQSynoC{1zU-6?hSY?T*YH)% zRxumdUR%{$)4fVqN;?+LM4+R`DZ!^}A0x2D|nr+#R6GInpr5 zpdZgc3O(qr!8z{4Zy_gfVbLIqod+pOK+g#(7rlGj_Fo-}(i}71D|-mM8rtcsd(hIg z^T)Sb0QiHa}u-X?L3XQGFt{Wlik|K)?)l9Pu&$-T?tPh|z#v(k@2 zF+-gj8bJ1soC0M@WY-~FyD3d#4wkb;;Ec~&x4X_6f697RGE1>L~@9xHglvAUk2Ht0DOi z;DJxk1#4I4OcJAmdJ8!ohmSOb_!zA&WtZzD=5>_->c|5P0*>ve+iE_rxN$n#5$zMX z`83$8-oN$IqSGp{M+r*HBcBcb0+R&fBLf}z=NV}o4P$Bi&Y5fySTa(mCMIG;y9fbH zxiEIri)eXAt7h+i(yRZ`O?MMUFKS}gdSiJ^DkC5X77 zL>zI5C22>n+Mf3JjcT03*HakYvkxr4?c*mJDWR~F`Bfe>cI-4{ z*f*t;Qx6bxhsdbV`dPiqX`2q&bal>}NcT+tePas9@A~vJE;lDP%}}SUULdp7)5spv zn7lgH_}{MUW2?Qsa=vc@_$$q~`^_I8Ak%oV#}V1%;eViBP^_9+lGS{J1K^RrIDc8w z^Rga2IpbOmP8q%V$(&l*r=@5mM=gr43^`0l8IYLv_m?bvQo95%b!jwjpLW%cXK9;X_;gEP9KY;Dep#j>htcl%Fh<^AVZ|x zWI7#Y28E;|jPE3K{Q#dxYsb}|t8RdgZf|)e%1){Vmmdii?K;Dukwk^GV}uWV8~gZ~ z>=}4F}4SnWgO(nHW%lk_={3R|i5pVdtq9#_9_{5yH|IyUuR?OaWl81^rark&pN7pLgk zuX)%AJYJw}U9@ls*C&)mFl)d0kV2+%PAT04h9BRA>wN!;=m;>Shf}RBg~Sa6McUpJ zYVAIKQvs|?=wVpW?dqDYM?7T7m{jm8P3$O=bzdCFWfbi(mT!i1EPZ~zBG2gKw0J+p zzvaD*PPq~w^RcqOU);+K5gUnH!V)j5$CzYFP11g| zz0auW4h9|Up5UzQB+2?XKU1N*)#Z@==Q*`4u3oYDc@a%u0C-oGR!LD(EEXHE+5wQP zrc1>&CcFQ_iNJjJUk?4CD0M<|e>lWE%?yMi9LN#;VWT?~Bm5RFdthZcuT(O}9Cb;8 zgQkh1f|N@g`P~TJfC5`p$LciISW{g1VvI%Vr@SoYalqI~`D#26(_W(=!YQZ^^4SLq zt{qgBCK6YJ5HlY2`b?e!wcGgr$__OpBle##x++MDj11_gM<{nQ4~E#T4#tYAPz?XM zuy7!G;^i>IksIKuLMLV2?Jj~X_85O@&AT)E&OOX)!Urpw8o!rnvjIVGTvPzJs>d>VfmY_nipGFVfqHkWzjh$PpkCLLV8RU0h@L zDUWpw=LmxxBh>Hv49w=`5uTMp1MHxVmiRvX@cy0P#|P@C?8*>6-y<~cCOe6(<<}X*zuuin&xJ+ymSsDF zd{6g(Q1{+XO@2}LXn@cmB=p{U4V_R!?@d5JklsP02m(?BvPqfZ>MS?S*F;AU(Um!c2DGki$Tljk&BPcg$Q!9@xrwk%Kq9l`N-?X z8XXtySM*(v5fJm##>LP7letOJPU!LS^AU^Z)v8OQ%QL-$120Jf%iIdksNeTqsYF); z4^@&no;svaSD9~IFP`bx3-yn+S-V=1UWImxK1t@s$Fc`4Icf!CztdiE0RwvbrnCPC zH2tuX=@_J8k|dIx|K?%b-Y4Fh(61rrx-ejchi{+@%YlksyXJ3Ef8Z-=_P`L`*B=dS zwez2Ty<-^Xk#jBU3$r@8o+fmG7}%qGbj#`+!^?5Axspn(an{)dOkVgK=6I}^`%Z6% z9-6;4TFKua*5heafd`*kLXKvxxaQ(*Gl>w})5k*o?ogAx8+LAwoyy1N-Zj7G+=3Mc zw{o!#kMY#@_{&#y{U)ZMwu3XBEVYH-8oN)gENAL~bLujkszL2x?h8i>aRWI1;zw*R zG4c8%+&4eF6R#|4e}DC#8%M^!7l3T~NYsQ9Ga(nzAL|P)%)L(Hvx&nPkZ?qqq^mc3=i2NU9u{0-< zuyN0Xx353uHRoLTr*J##l8NRjwy_sFzgtY_ig#GogO+1fqO5V=B|JSbndVQu6AZ>h z9VAOVa|mArqiW8SHcCABd{+qRb`SdOZoNCzs}k|pz*mPU`~lh&C1xn{>WRScskqx0 z4^Jv=6%u)#m_9pEPrj-g+~D|$zR1pG^^j-?ZTd88@RN0;qhRUkil2E(?07QD8sTnW za=Yj5z4EbREsT%xC#X^%bBwEG9w};#NS~}k~Peypt4Qh`D)!>xIX8BtyQwQH&TJ%ha zY{XX?GO`%qzLUhOb0b#DF9P;w>H}jf6nefqPGDb{{^#?m40yr$XL_~xj&o2s)>wOC74kPOy(;hT9dBBjyRV?P z&KUCf46!Ry=kobH{qd9ZeEBjx^*gnc#m37EruCfnDmF%53wuH67FNrasM}iksyOOj z{_}uJr-$Oi7~g0LM$}to)npy%Z~b;l>)nXb{LAS_KXY=Qt#A=}?|*Z%zCF~Bw`$O3 zX5QuKrv+(7lXh|b(F%MS2J!Ca{M=ZMOOm_FLRF{gyJ z>HDHE|mE>2bLVJcODx zAa74kle_=^2QcDH&GK*H+)()FQTRqA(ukPd!x;1b?Pu-(Ekr~~Nl5{`8mRt{9~cD$ z@V`1l=|%M&1A8;8;C(3ELHPd>A`&@Fn2IgB3|5$r4%x(v<@_+27FN-hz=Vf-PDbqM zt;&N-!iBH+A%qW37u|n0ndp=9ZumhAw_E|T<#fOY04bVJW$+dwrk`@Uehl$ZS9W>d zeZ~Q$BMCj>Okpa?ei7Mg5L1$>O_-Z7)viE$Xs-z!1q0r;kh31{<&nk5u+MY6c1LL2 zrzEUSNQ4L{hiEBBbw(Xzd7u$4gts77!dG3%&cn>|^U<~!7^@fGJtRIp_jgMw!uxO7 zbcVdC5o=_r#z{-Mz|*M+cA))Owb<6*nnc~=RN>7{S4UOkr_%9`tlv*(ecDdYP#c;D ziMgRYruk?i3OzgPCBE?QNfkLim`o9HYk1wqOxI(q=5xt(*17w+huC4QbYLQCra~w* z>YdvKDUk~DZXRc<$(x_2t`W{}{H9zXM*<6RNi9X4-T)GMDm->j z@jX0R=H))6ez|T7GdB3~wI882bef8PwKL110M0&WsdpPpyoQnxu6F5fexzEIn^4B~ z!6e-9NfrITa`fBMlbxvLmmIf0tJ5X$p`|K2FhjY1x9r47t2~|~EKx*WQ9cf?)=Lq2 zUfjP5U2LBLc(MNg+lTU_RXtKgrrS-f&1AVG<9$Ds>=xjp29J>>&&|y4sCbj_5@`NZ zv7NL&V4y9RV$(2``GY0zwK#m z4PDayTT}C6P~cAb55T4A@0fKZ(A&UzM#lM2O9#00pIrhXfvZbx3t)$(D4m9))9zu!w z!7C+}K9|Dc?iE5}G=kT4kQB?R*;PuAt=H%dan_?qMORl9m3Dt5=BFBXlIYBHVPFP56_>@4o)#JYR%e%;m_uUoRWr*j^*|^>sENrBD$BE`ZbE z)aU~9UN~G_Scl^VDbPceQ`4pKCeAk00e1#UWN@apzso5Mx4+w#Vm+pjy}rZcZCydT zh4Q?aY+_nSD`4VU3gW>#z+oqOv9MzX2N91vHNmQlS*K4`p0rfvs?v!zq)CzAeH~Gi zjHl@l^4VxR5FGi?QmROr=2Gf5*5qLt!RN+}iqt|8p8|ZtiG5E-*PyDTLbthiT=bq} z5|$sVi5ld^_sYRa^NV(MIG}UHg+D=(XNXl`@I2mtK0A|wv346+>v+mE;LQC3bN`XM zk~-#kl1&O;^2$msLG&`cB+5R@F{X+4=Qywm=LThpkz$?L|HJjqPoXt%RZyRB-qLQB z=E&h;1+n8A2d6t6XS*AK!_$aj3Vwnv(wqga@ClmKk4cn2d_AO8yOs2%Sl9x_se0`m zY_j45^thAl@3`4+-^R=_9!3XblElu9EdP?%BnA7kYSgudde&9)f2khEDv4ITzyrTI z<)mk?8h$+}1sky?dk)KH@gk)d7>pgFFG&!}7~Ek+3cL?HE<7kIQkzh}42dJjXK8y2 zt!rbLS`#)+ooYlV(z3hGt&XY)pC2_}Sv#bXj7rEo-I|A5&=`DwHIHo(Zx_hCvP|`m zb#97H;JB*FOS@txyhAy?^Ur(%GE z6qj;69Yg^?-)&S>+(|C--hl#(&PDqFBp|Zw!5PU) z^BV0%-|?rh@>RtI_jl>k@ukjwTxodsCmjTKH?d+=V2Lji>&)+i(# zHGd5sgC#dEGH9r(CCc>537M%jg6H4!@B)dEe(9#|+mK&LNJ;aXW^>umwHPpy{~CuY zm+`Eyj7*M9zKxp^`0(q4;XY+hE?6xwTm7`YKIl*=@;Ornw!rN#XV8nKn`A`2kC6Lb zO?$feg)lWQzLPl>Wm<`QLYE|_C8U~3YksY*y4%b)B+1MvjqsFa==lEcle~n>wbkUQ zfAx5&Ip=m>%FT@+eNz&DF;ulRRmsoOv+_Rx=UoPK6oT=)yN(9w*4Fkl;s<@-*M{Gm z_w4`qaoXeEHch?pqP0#(&~I@(CoR-F9@5t@f@^}*jjjWAi@@v>3#{!itMeG!yi`9R zTpE+T4R$2Ce|-wMjrAmOII?Wt96F?(KNd%EDDd2MU^>K)w)NhH_Mlf?ze&3RpEqp@ z5tkeo&+mteR{BOAW_I_8MN}%2 zOZATpNO$P2`laGYmP*Vwzcl4^P{`0)bk?ivFhMD@(iA55lpER}C0L)rcw5`?N$8Mmz*grZ|$}!m--rg&w z_OW|-5JPKjN@7l%-H7DC%_Dla$c7{@W!GeEi~hdDybvxx^(RiCoVf8FUH1#q(hm=x z^pnhAuFG%#xeNtZCpM&^en6SDw`LOrb#1bbWQ3q6lQ=CJTAY9T`3tu}i@JN5%eRUf}bmr@ej^?jF$tVpbYjJtP6T2s#-@R1q`lBUO z8=BLdn?q{#suv^DQR%zKvUn4}zh8y7#!(66q?e~@5c^RYfjWn|xxaGhI!PRiN9kv4 z^)SFzOaCkR7XiMWjpult3}(G^qdVJxF(sYaui$4poYW1`w35W@WKaZYH2|QMxBMK2 zWVLtd@Rqjcc_)!j=yUwXm!5~4(Z&a)aYc=@Fo zOWOu$Pi#!r>X7JW1&xY1E_cgxeg%QW;ecBzziE}QExPxJ!%7y0&XQJ5;R$)<6yx`{ zW%V{*b(dMeRXoPQP=x8}pox}At#Z?HFJR@u*e2wKGGygB_FIt=ExDf^hcwzowxAe3qeTiDk({ue*!p3+y9qb>5yY`g= zvGI`gQMaH&mFu-h|JF%ga_cvIDQ^nA=@(FE7fj)e*CA}SIMHpheAX4fv$&U&8FJ}H zkh|YmmP!xTv};o8MU2oLwR;CYCkOB^LPrg7QVvqs7w+Bet6aUF&MweRn>9%55i^l{ z7+y#1bEL)HViy?>o!m_87B+AqYYRCpAFmv$E27`BP2o~I{{tM8z}GTouQv#-V?s{h zo9B3Xla#@}o|!6^BdSs-C;T}Zr~Zt{uQN=FsT*thQ#RK~SxX&2td2mS@u;GRkQdq6 zOyM`2&cz#J%v4t8yP!vJbB`er`4lGhh0?Oo#ts+5B%dyHbEvZ8TR)w5XNMX{WZc)QdrE0Gei2s+@%?&>9$dM4(87`qvE~eJnRguL+hqWTV{SrEP7aRr`u6Ao&(j zNbA79uTl=OE0Hx0ztrg^!4r6XNA|{PCT#P`tYq@tR#V-lo9)sZ6S`l8j1ZKhm>@RG zr*wbUjir~0!^eB$2kQR-Y?^K$BywNY?=u(CQ$EGtp{dgfrpo3ftYi=VSpOaq1ip)W zJ>qAiEBLTv{$cW@89j&6fi2q`w)^wFTm`Ne3cr@YPr)GQ-oBhBB*I^10Zr=!BSU9s zeM~qZ5FpjsjSrt$;xHG-c$Y?98gINj!{W96jQ2i(X@m>kIZp_bt-^r7t79=_wklwM z5Xsb~q(vEe|Ex?RJ&Y|FJ@hu@p1rgRG`KdME?)VK?%S4_FnmK=vW;FKPiRCED1N|ti2?Y?<&&6JRffR1c_eFufuma8^sgrRCu#V0C0*-DK9mn8$A`=c9(efR zrmcLmf)}T6bNm`)>MY}j5R^&KmnlQ9e%u^4kcM<&dPZ^&%*9p~tklRm4|s|0D3K_u z=RB1jD`@L?>?vyGY(70w&ZCSLgq{)F#{m*v^snIt>Aw?(F^1x(3ua`h5q)&DGpDX|bUQqM$E#=x@C{Q-{0WwV!cgh{bJ42sr>bLg}Z zE9l!<`B-46(i)LwF;HzL$xrhfpmiHu7;yj6pEL8?5kLxHsBs(li#sR#+z?eFx*~p2 zPfJ5NleV7AhQBV(G`^MI9RyYJLAYgXQ!oiO3gThxE9?(A@G?xX^pa?EQAx2ms}D_? zv06S)v*i14Ak6u#E29AzJCAuX#N=Jf1%@aQl+6!!@0?MhQPEN$yrPiaIb9p)p|df`ZEYNBTr(v`&gB0)8Y z1?uII8q|wkzAp*+ac#oSG41!n8%Iex?tIlI3TelI|0}^XQ z!oS*^_yBfA5%~9=kX=+@UnZ(krk~gsHj#?D;g3Hu7Cl zL>m(aopYn*bb`dyt9$i`N__YYEcufu$-zD(I_*Oc%9T|B<~kY$c2fNi?79M{Cmeon z*xjot$I0n$>wwu>#Xblz8$9cO!9IH{LluVOx;@N&0Iz8E4O8`FT3QkA4092ZuObj`i=KXss)@_idq|g_y4j)FZBW8tRfe+s)2a`_-|%cI3aIaEaKcl<-JqCz zncVt7eB5}7>EG%}%?sNpTDO|Q$R`qu{c=GObSs~(fYTgj0lDqKle{mnqsPDJ9S*tF z60;yyqGUy-y?FRRR#lCpu?v?uguZo3KbR*L8M8{=7R#2A=HPm52K(Y`p5YhLST#jFw95vyo1QUP!kmgV6z+y%;b@h=zqsKwn(Fb+z z=?lJ!4Z0UmYOBp|4Rt`ZWIUzSq?oy7Z}LE-%@l~a_1tBaZ)HM_kmO1<~~<#w-S`o`v|0cDI(sH zO*g!Zs!qS?cWI=+b~Xtmy186Q$Z%Wnlt z<5mfaK5bUBSl7wNty7j+K9l~`v49sVgJvl~LAq7ZbZ{023_TDlR?noVO7Ll1>-;?mSxMhHr$n8j=-7iGU`TV zd*9!|%r5~~&bi9@#u4tNjSR!2XsBtVAY1kpNZ8`8KF-T$x15^zSJGE-FFh(qfm<&) z&`MqI7=w5eCmpLWqO3c)x|B~DZB_>$SIFPFFdUZ4RzB#tDbH;$@169ePqg z+t%aFI&v-TY&dOq!L(^ERX+fgDHU>+OmtHKztviJsn~qhImoKm^(atFJVfLP3wnuk zg|?RFJ_M!I`#fDlu%#G(Y`4n^1W&wFm;EKmek=H!nd%Xy@>TNS$5;IgKL%#nUI_Y* zYoj+7#g@QX89G_*Nw~YKEcp9vW$?2E1BaJ_-Y-QAHA}IJ zcoV2HNinp=a`Hsk>vpa@E38_7UHyBbrlSJIC)^m*5}`qT9P9m}1=Xb_)?HhchECBt;)I>!8*$zof4J)fy{b zF8VWKQND?J0~h&oV9G%i{&$6H)_|K3?Ij^eK^S#WfIIB+)M7EWilPILvtluZd}cRx zoo40n*!ax*mF3>p-YG00;GQ{VP(~*xU~SI&?O|q35c6bMEC=fqBfojN-1`a|UO}Zc zsFEOL%(Cj}c2=`8$n!SdjP+KKAUchxKLgjtSwli**tVljMKtDJnH?xWkwQ>L{>gz% z+jJ+MttBXx6@*t4@`XN*Qw+L?+Gwr(i;S=O}GROE=|ySDP4+GBr%E z&+fw@_OYs1&Lmhm;j+PJiv?+m;y<^d$fFg5s8ne5gPh_KP(!_LYhB9B-=CfR4ZE<$ zrS5toF=GQs=#_$HH;NDXV(>*eJF3RUXb$o&OkTXPjfs;pRrH4pf&C-pYF(nV{GP!B zsX|gS^m`(zVx%5OHPR?(`iGb22&GMDxKwituA`%!1-p%@B*CJ8%ApnSGlH_Dj*~L> zH~OX1=kdwefU&r}`Z1DljQc3k?nDzDh@E|+4Kze#EF&M%eTed0*8Igoi=e4%KZc!^ z4t?9_(E76z;1d)$5+2o1w;JO3baE4553R9C-KQ1;yFLh{?A%RvQgE5az~AW^IS+cH z;118#0o28(>pO@NdXY1nhP+o`&YUN^K`&m1a+4vviwG>vciE1RMsg;UD0gPva?uw{ zg2!C#dyWWQ(KeLQunq1eWD&!+RLOE+-sy2KFLz4ynGJ|-ATsG}%49WA#;^c&3q3A1 z_F#XPFh^5I$C-_P$m&kjlEJvm;9{lRtCgv=&wUupoXyR{hllGNU{_&G7(=0L{u&?a zb+*q^J1R;PVF0-_><=5GTr%V}kT--HIqdv0^mIp!1us$5h;XZ}Nm-$2i?5+sn>eC} zUC6*4uCfzQLS_RLN>1r@)J9r5&B+L@hOM57T3czdON52lJ>#$UN<&v1LZ-92af1L!tTX|c~M-KK8& zStQNSaY_GJ(~_iSNHBOKG&c!SA%*%;WBSPgxqe9Gs9mrFIy=@&629YOOxC6e#_vdV z-#EPA{utlCfr?XB=;$dHa;T6E$4TbC+*b=3Ap^F~T_Ncp3rB#>H)m#3kPUrW4j{Y* zfIlF(`k8IIu|br;Vwtxu z2pqEL@JVb(NT?^P*iCPseNTj@j2@Z$1HaHDM^j}*6)VaO7D@}*PgwlrAjG07d+8(W ze$-MWzy(=7AAw%<4o3{Ju#3$eV;&_B?y+C^Q=ShLuSmy78DI{)Z%(>8R^G{)x2qDt zM2DC?Vr*UaD&&Kr>t9vTyNsGKb2zj!(&Aqb(!nwWEZ=aGC-842#1z ztz}&4S1{wxM={DIQb_+ay30IMwe2^0uoH4qvDeIfRi?~}%=gqzCxA*rMNai`xI;)< zw}qOGNk%dt_^u1Z+|cNz(S*;DgXrkOcJC%=t(7{%&2Z z)=>w}%OF%4pGB%zQ=SClS)~LD$uj4z_qd)k*AN5u> zctu5r4hF``*^puPxD%ANZw}v_dhGc3ey)~ivr!cVg=@6qS;o#}T|8IsCS%k$zdQS! zfH0OX{u9oOpM!WAYqWE+LCqbpU$$a(4s+gsIg+{y1QHQBT$tC%M*pyJ`>kzBxD=p| z8}6#QPRSe0mVMfsJX+EEn1h7tXNKEeJ1%>AjQCJ&=lkC7(5>{W72y%p1e+%Q;U2w$ zSu*o&Gm+OVk6M3Z?F$5jfw#p?UMb@Kz;2kH5#C#2%S@G_E^AWWPJSWy&ZM4i``lNu zp67hO@PDH@Y5ImHoL^YTHrbk={0HEf>NOa32;ukaZBa~t8Yf^CzYq~kgc&B#BE_{7@{;08BPur>^^Z1!+n+58rpM0wfzzaeQ7z(iuJI zYx$O%CD(kJhMn|;gRhxCsMk0aZx~IHfxmSMMjul2pEx{YP8i6~?{7Cu>^_GDn)uIB zUT~?vl(A?w62)8YsW2et-_yzu`hMa}ITFkapuoYpMiB~%&I+;A>GHjL%3c1n=!bW) zL-LOS(na`y7+IzW&#MF2*9vbb_*owE%8VKssX^f|-AoW?Th^QSr`sP|i;7=QY+aFU z8T%|U4xsVrR6GIiQM|ZqF|4cgtZ|y)7S;tl!^uuzbhmF%B(yNRq6+9sAlDaE2CGt& zW52d2h*F>S$B$C73w>ysruYxwkc0WTF(S`%Zl(=8hr3g!qMGK?l)D>ok~e-q!fU&K zuA0cVCc)t#dc(PLZYUarry`Q{?voj);Tw+77iwM1ob*(RfP{L0VNI;l`CwEqPwMo3x zC|Rc+&NFj&FHw!lRbiimL*yb~PnPgh)R)IhXl$~H1=WsUKVtK&pH^=IpeMn!ew_ZJ z*3F8R0(5;JUuT)vI~BrcQGic-(qJ^TBb+dv6_`yNxJUGF#5PpCUju-uq$J{yrYBjy z)K%<%Ab##rtfV=7i%+S$qo^A`hNL6wVWJB4nxQUl013_(G~}P0L7Bpnj84hY^y>#^ z!&fUY*k=C^fJ*5xmgrY_@m*3Q2yle`IoOJ0D~o|n@9@p?hRf@3`g5hYZ0!B=p)Ga z-yj0$z_n|dG@#}_We!CBCvWb&8nHWf=^t_z+%$ZcJt&ZPf6MQQSW3SUj0UZigfdXT zInIh3IP1^^M(Z<(*f&M;9vVpkdAUr9J4Rh z<~>Q~-jwQH`cn+exGLjX#3X}qmr{YnI}A?PQaJ$Dk14<0s^b2a+z+Q1>kg(v9kEDy z%3at>@Uia%^V^B)x*l_UX|`sk>9wi!`DPtYafWc&`nN9L2~(+^LYdGwyh93WY<*{5 z{(FL9&=4>#9zc9%crVJ=e*0q7zT=24D2{WGtibF|W&lPh0y$?Fk>s1(99P-I@AR!5 zhtv|!#D55PR;yHGgqYY!LntD7T)}zufoFm!!01WVL)UQV^znaysM(lBP*6dnthRqd zq(D@w1%EiUh#g;diwW!1M%WTC9MCR!n%Z)%31~*r-B(M4mT0A^HiCDaSE|vCYmI#KUCMu6Q8GmAE2NC&tW(-7n9KIhGWxV+mUq zTTh;}7s5Jo^fY>!z&=PBJO^^$KP%w0gk&sySp99&flQIc4)>?}uVS>4@GQ9J)^VI6 z_%dk*N9}Kf5!XY&Ri|IHk}e)Wz54|$t=00QC(dKd!*c?$>bZGx_18drOu~)j>kGDp zP;j5^2BymrS;Pl+7i8aq`W1l`!!kQ#GIJdV9TVD+sUEd{(a%8g)HO3!jo0`6c5WNo zs4Kbh`VGY`n`-k7vt~~kO_mTkHsQezp(}Z7R;IrY0(2prGWsOnQ;bYseMMxlhCKdg zZlbKjS9Hm|PycW)pDD&90E3PfGo#0|5=UnqtP%}a_H^DH>u%Aor<(i( zRQA41L^3f6A&*UYg)*|HPRwK0TN1~HG%smCo%6Kv%?Q#PdutK(>Q&etEaa^I;jMp; z1B%pRv`7E&@@1t8eg?k0n%gu{>|?P$zggK6vz%3-b4jZkLPdWB*S<|$Dd31Rv~;pj zc$4Y*_0azGyY%c=#9V1}j6<4oVA0$WkAHBg)U$y8jxytJJ8HjEH1+jrxNB6`t+c4W ze6wYXdim0e083>S$r8mSRL$Ld_$2YUC95nj0&u!9_%xymB+$%9(bp}1cmieut!lVF zc+R@S#l~#35c+2@^641{K<*txF>Z6F2&rW00{vgoZ-<}$%eww04?93?CYoud9Dx^h zf~Br^alDn~$}L7@*xy-KE&2%to_$#TlEM5KJ1sEqU@$^;*to0?$|#RX;z5?59_2dU zCCCHTq#0wstZKY}AlkcBqgxBD=T*X*XP;pb0G>ckR@L3^{<&B=TD<7|>B{B;;hp|o z08`ZFi>DP-cl=|khJ7BUU&Qt{e?J-??0=<-1O!diPZZ-xqVd7TJm4gqoDuRmxXF8* zftsv&hLJDFOHMTiQ4d?LcgIj1MlCYD5B8zHmrdExPF}6$!XLi*$0y~Z>#j_Kb+{BkxuwnDHZjI?ms~50$7+aJ&baJm~3P~ zxG{U1>I++5wl6g>`;fNneI10}9W{S0G|O}$JoW!}=ub|T`bm>o82tawf#H!QPK$jc z2uRztEo@Qnt>1)7{>$1H;dCd)^%uqv@()b&;awHzv2gj;wNu>@!*6>ZNDfTd+ivW@ zAO(f#Y)-&-eZ4Qit(M+hK2p>%TINHWnZko`bKMqr%D?FH6C;YBiB;+y}h`y&nq zWCQdMLXN*m z_!|<|9lA`J;uPN8&;C+Cqs=>rzA*(uagnGO*R@owX_uj2887evHSVkcomAlyE=b`8 zUf2LVvKR*Dn7+6+@m)f;AMQ8ROK~3*_)8G;AxH%vr7-v|)_e{eaGAx!kbr!KezBu1 z0g(vwlLREiDtJ8>*}_1@O<{K#eHpVeWRUHuga-c$V{2!MqC1bxGqDNBCOlHq$^?^` zB*DLk6DLYZ2WrPER~q+0TSLTfw0i2#G07F0w|JKt7Np@w{~6*nI2ve-ytX#A&k*O> zCIzWZvF6zEw@E;{ZThX4bY4Hs#!`pLC9WA5sG_eJ#=&Lx;znJ$H2nvOCb$(KsDanocajGwBGHzoONS9}N36<^lDiXkDBUeo!E zx+V-p0aUoPuOtcGK?Wgt{yb3d@UO*SyOG5Vi(&f}HM5RL# zpc^{V%0;oK{{L>`h8^Rz9C?h6bGo|F^jXRKQH6CTW{%`FK&s7ry{j&O!Sf1$Qs3Uk zzs|VA`!#(J8CM=1;{s$Rc0g2uIf7UWzv>dh^Ayt8MaVUp+4Ojod3?Zrx%5h`%HXtv zo6E)Mv(F^z2%R#s*a@>S>HSLNCIdTAM9WR1`QyFHr21WYy6p3DC|oF{+(`9Uny&t^ zY!gX?f5+BmVR=3C%GZc3W~f;&kgh{3lWA4S(nDBlIs=Wq<161hSmWxzssfIl4Ezru zhKW~pS+P0>``#yLX*_k5elI2cq8^mnrZCeai~Vvf`?rZR3&z5K2g)=0t7PX$y^!ML zg9@UUMNJ7%izDjLTzRHjp*D9PY$jAloq$~uRHgc~Q>Vn+5?S+ik*+?PUEP7Tj4U6BL+{wGgK*>=o!$u;#Pe(^6VC609}LT8)~i^lRnj zt!n((^k>z|ocId#*Jh&_yIZ!Z?qkKoj(+ljjg$V|)ZVRv^6U^&t~vUg8S(O*Fp>x3wQTjDR2$+s)5{>bb3=s7VLjDbEdn=CP=vaSKH28f zfbAh-9@|7vXW)J!kIyBJN|x0dX3Sw=G|G%qk-=0$!xO7$$TAcp-G#~EwEa>iwgmw+ zzacj4F0scRsaSz+_$R$FLY2ocLl4U+Vgtxj-KRg~myDQco(aD7sgMYcpF=ucJCX&Dq;J zU9K4~axO|bzUT1)ZUMc9R`DA^RHF1hm6uz>`!{U`tq5={eXp%Ll#f}=L7;Et6GSYA zRn;q1GRmUt*AmAqPmn~&@WpmLO+|tt=ZXTs;{HMIMUMXp?LN$A2SgbjbQ3|ADcZEs zZfa3;$m40;2P7K7sovH^YayFgGCr|A21O2?IGp7;Bw~f$l_ut3^nD(Q+_^)FI}LI% z7|46NLrHh{SK7+|o=XCi)S=Cu#_fh$cwY8cQUE<5cEX;ep9paAX3S1Z-IiY@(}sO6 znN&FKXBMX2W%~d_{LOiXImzn36$GeMf7{Iy4~vFMZQRBC>Z@)H^cyVsn~U@_#1yQJD@Lgv7xpwT$j`bR$LHlVa{}*9YwiN;2dzlQOoc`eL#un}z3+ z;aoR|!+|#SYaY_i86zqL3$y;BnO>WbvD1u8!~llW^5`}YL-dSiU}>h}Lm>h;^-b>g zW)y02KKS=lo+3ZEA&1>_stAcL?$jMtONz#`C_d)X$5iM^y0jLm(S`p0iM$pB-Rc`Q zFNdQXtk(GiAHP<-KaMIxU}d>&#NprwyER(fPa_Ta1qQ^XoSR%f+-5xWiz)cGLTlPP z-2M!cl%|-*LBG>eNlSW86Ed^3YUd>~bVm{?SO~~Y1k;%Ghp4-d*sdGnf&81v=FTe4 z!U+B}Xj}Fm#&lO6>=m|7o0R3$E01^NJ1uwVFr#`M1~zmzTp85g5lQ#%dwyfl5j+kR z`|}fK;Gn|Hn5N3!y3grY=Dyecky7csncet2HWIlz3n53V^az#$7J0Fkn@=KkSa#Hm z?mVVL?&|wU%ClGwjF>$Kyzow7_zQnp6-Bh!M(fr z@Br!McEk^TKk655l`Y1)DLwvC-*I$L=E&4nmb~o-sLFdzCQRdRqEyt?YqAox_Q<+4 zc$l3fZki!_6husWZ+D9EL7_=cG+QVE_KoKi1M3J7#hZGLPrp+{r}~lCc!Wdp3-)40 z3stBLcX%Ar7fjN~Wx{)49&e$2>P(8>7(D{3&R_=kw&nN`0R#cMG3CBb$$U zH}YmQpX7Y-He_HBKOG*uL;KR#k0=LNZ)u?%x@tSZq`z7Rzt8UzwzIcHNX=A$KU-YO zoEwX#Y&SzD*#N5Xdn|7BLlogYV2$;g;=Z=1JJglovP%nEn^=P8EFie+ z(Z;=uFIvX###UZ7&n{j+lo7n6POTWH_;MMu_?Adr#CUU%!=p(BKAY42M4evf8NQpB z!0eG?vIk8s1{K-+i;5s!%nkCbK)>_jK)G57S8rUXQtB!NcjC2~?wej~9+oR4iaFaH z@=rE)Md@vW)-eO3?+gqtS_G2y0X=p$P&vXR>SuM`mLUcOF_USG1k7HW92kb=_bC@{ z-@9Mt51{>py;U{kem`~}z%sJR{S{mIURZ(YjzJInw#t;?RYk<%r;%xHLNujqs2#|q zHVxe24UQSl3O2qr<$qi)*nwm5=P12>&h7mc=T|TBxdV0;GU)?6_dgNpM_>BiO^(!D zw~As(6{ZMMsAS#ag*n`((g&XY2l(_3LlcI)UQc7fy*}LDG%djIkQu~2j^lbz&f}B8 zCTPi3{jl-RqeW%@^4*xzpP7t%yo$gVoz-yP-`R@*BpV*OF1~v%P5*Za?V3phF&p*P zf1t`7G=591A(p;=PT95IR=gkOr*RBRXIZGdq^7~{J(2hWSKK9`@kXq zTqR|IWzM(=d!AO?a*=k7qjEj$88EGPksZ>_#SmP`GHpUV18#(XhbF=ZxzwV7Tmn6< zBe;L9iSl}b@Q+L4y<4AX7Ahi)0}_x;R0}y&*9sd(%tQgQJyK*A;h?Gw#+#|c4cRUZ z0qU8HmT2HfAJSZw6O}g~EiJ`V;#1hF2Y;|2*1{~hzmC^>qiM>U+X-EHmPRS&I%*Vk zKK?KG!UQ~pDsUC0HTfm#A2TKgOD}nE*R1$;-QktPyYN|I|BJy0GXA^lR=ss6C0j!t zvM{X^EB??~rvBV)km;`6V)dzc`T&$K=9fb*qI``esz*&&qLgntkhTT)ZmSLMM3ach^sd?xL2TrG>H_ISB|2&sijZ~(SF`6#LsQ?MNy;1j;T%8c+8-I zuP)8=gJMmiwT2q31a3460k2Q`1_sjI8>Y_-L(fe6$$Ix>3%b%h_F+5HSys-V8wXl? zuQ9;(F|i+^8x-Bqr96-23*9gFRkvkKSU6540XCl4vJi$#vFV{(n$%aQS7peXeR>z` zF=q*uZ5nSy|0Gg_29Rc$!l6p_kJ15-E5I?=43Lrw%cgW?>9hEm#p1r>;PNLbBD|PF zqw9D5P_Ft1(J%fI3keuU(^IP%sAdW`Tb4o32fEKn%D!lu;)16o2pp(04otgGnG+0C zmHgh0FVQMLf&Per>SViciUJs(>DP-H>y))eJ7mc|J`q^ zF)Cr3I=T$H=g|H2E(h0-u&i5l3iJTYpEOEOckA#}!Z(MS;JO_~>7OPB)s`s?eQSA3 zZ*cshZZ^1T@R%cm8X$vt+2E&y=W!j7m zb#IeigpD4DE7<&UiH1LUl6(EPsZZuMnTne~c8$X4>7*E!^&$615ej)GPl|I})-jS} z$4(lHL!G(5h&0kq;=TaBM`ajP6EwykhVd-=BoAK{=H)?nKJgrSNckTi84fXYxMh7; z{(A1Ur-gcW_n&8W>)#`!$#mSE@7|S#j=rQnI%MP75^@1B7mZ~h1VL&t73wixv?w;*)YrLk8ZwN`B z7o`^^7M3z*B+wjm_W?q;u_5LZba@R`qR$UWd^xAu2pUttk>_Bl;k8#>9tH1%*F2hi zX}DM*LV%6Rt8LnyR2vJ~tr>hgCb$)9 z{TXQ8?5`m>WbP-~WRqzC^{-78eF4u1L2$&^m9!|zb%OtnC>I0B!DC-uI*N;v+~7xno~6z63U*a084xW6`N zdFCXf2vGP#tK;zIm61S()4RL^%aVJ{HesDM^kP{N#Y3;CzZ*Ls2elc=AbNtw7fyMv zg4kCnaM_c2ObDCnAq!bf#{=TEU9syqNk7%*lhlndIUh+HT8j`;*ZFrfjToIn(k%)X zG9@=PGYNHVuZ=ZNZN(U_AA-qwkCzTkz)-`4mmLDgE%g0bm`=HF0s{@+zf8(^+cj>5>MIRp1o|d0rK3;M}`& zTIoCu*On$aa9oh5ZI=Z4NAFcB9KOps^jjph(HWCQiJ!vBj8ueiiK;cph)vFV%8WhH zO%>ViGjxwUrC~v+9{*828-X;{@EfW(H97tU76l2fXm7E8ga;!v+wFH4D7`#u%VI@yBDO|>NhW|$`4gbRM4|1^90l+*G5&wA?TKAiSUY;ZD(7;P7 zdE&WjKjE=S;>kld=xN$}0Tip$dt4 z5-&|pk0~Iy)wF%pC6DAFbpckUa42ihRLtxRW6*I@VGzP#`AFgOPMO`Wdkq6}QhWo1 z_38vPqUoho>9lS@5#C6Pj_5x;rsAu%>jmKbrSt&Um>k_i#>*tuM$JbiHD=(gn~!tO3A{vQBh z;HD+GCTc~b91o}r3tDx3x%)%JK0dVFtm6ITXmgg#6dO zLe)0&H?mYbg$3ESAV#HH7QTSZs!FI|O4_l(|3o69Q4|#6C$*R5`hT(Zo=*+P=iMY7A3AS`153?CeiERPNXtDFb{_$I>n-bZ>B zk4?~b3VwtZXAqNsmMG^}qUT~EdUvT1*HjVEWia|tuLEEl5Yb`&tJqb!sNsEH<3?

m*t=CFy1c#WD#j=krznZW)&EjB%#(bmOg67qofc%Xie~sMNKGMXn zDd&A!S?Tj8j2ZPq_hZiQ7unR3w^b1_#@(lHa*F*1m2iFs-{`*1tS^M?5uo&^ua`C? zSLhn)7Ol_BKMm%^#Jr!oZsq8X8nazaDX7qwzJXc=)p)!J%@e#n=l+|0>Q1d4{4C52 z+&1$_cmHBdEher1?#zJFXXK~OM9^k{FlRZ2<@|PE5ij_I4TGZpldQ0m4}mMAz^_ri`)>z4-cCLmA`xO7iBWc%s_p2`RX6fy#=}7)q!z68Rm<5_U9)( zV-6;X>mFa=%=W*eq+QS^N+P0m$X)E}N^uwN{1lLiLUKTMu`opx5j#Hftg2vzBUxkv zCB#YsL1E)k@Orr{zjWv8=nC64#k9`nQsh@^j_T^TE^=z=XFxBi66QZE{oW-*`~~?& zY+$~>keup~b^Il>&#!J>=T@8Z zwc-GU!a(u?TUpm=0)X1!GaVo{oI_j-_-n=Mn5i$d%?+9!1Cq2GxwS?OKEeJlHj%e{ zLKdV849pVaw3c~u+*GT? z>+U*^3I1dbccm^rCsiG!vb2*-3~`dddNU%nuiZMZ4T4nq!Qmq;&AvfeY;1?&RJ|+N zIFi3ueHG4k#@72@f5n@PFT$^z%rAuf$;eQ>Ep(v=X;bTnt~1{EXYpzRQ*svIlOa0H z6MFI?TYOnw7F9Y#Bc8h)$zMP*9rx=|+;5d-BrsOj9_++aFG5>v7~&PpO?gjWJB$PW ze-F4g_S9@+mP@#cAM?ExbB!=kr-h%wl4yUOJ2R`8pLR0WNdmWtQRKhLFlPn}#ytC! zI}qXI`)cR#kUg6}<<{c(SP*}_csK9Rb=WjA@jk@o*Q4>`Ta2eekJz*m=+Q8nUduyqiA)A|m7Or{ zLaA8<_3@$NY@LCb-l9TIkRn`|#4c%X?#l|K?{8kL0otj|_1X1&X0=`M%I`N8(XpI~APUn;}%W#RC? zci1k5Y!+r2u{ayQfUi6$aGg$WoIyBt4AU4P$z_BM4D?!=S z)DtYV=^ePU`z%h}@cLzXH#jCiK5#GtsVlG$WWHOVrfN#9YRtf4w7<9VjmHcZF_j$U=TK zTtrQ7Tk8QU42@aaXF#XDy)I{rq}pUm(JFCF*LA2lf1fugQiX4>Ame=4z3YCl*fL`# zI{Me=9CpbVK(7<*>0(zT+IZ@qZ;S$g#32mdgrf4-g^8m5Ho-6aLNe**V?)aTz7=%1*ruE6uh47N|NO`JFHxwYHc zc`kC)oZz*S3(t_Jn5cb~+pIu%b8PlDlDFYCvqo^;Z|OfjU9*y_Zi~1?b8~>vF793* zN-Q^m9&>#Wy!4{Q8q>AFnN`!M&Htds!z){E39Fb1S96ppJaaO)*aZTINxHSe?Y)eXi8RzI&A|YG6L5dn(s3lUsx$|@tfxo{6zUE7;}6N z2e||cIX*8TY>K^QfWf*&QMo0@mhhJxapXP_VjY~^j*K;&|7#(r|5#C(IsO|13~o=L5`6nGKw+*h#G?YB_3X38j{Z zpux|sbaex3E*#A_hUf!>qAyI0O93#enollWzExQ^>h@tUm~`Mg18SKZ$Pj z*qO8nn0f`&kI}rd{1o{%lyu{?RB7GP>SZ89 zMp=M*T&|#dM}!gxG`o{*MRT5&#iWBh9lHjlP)!Ae`T( zo#jtGeEwZHtGy`(cHO}!Qgy1sl)5PB_H(@PU>|a| zRXrGUPil{`&S?Va%&JaDgvLYisd%7cU9bdmPl?O_?>4iB=#+^@n*>k5qB-aZVvpR# z$m&W0dE&E4+Bhk6N3CHQ?7efWOI~N8u}g*-iB%iNLvY zFI&=c+IM5r<{-O5*k${L3Wi9j@lMD@d^8m1|8r_yqz75z%1g@HGZA{)@!tjvRg--N z-J9d93+o&>zV-ulCF7Br**{>W+>OCgtP?O|4&Z$Ri`mMSWDofVSgq(g{V{=+bll^A zkclci_&b)`exw}z`pMwBNQ_068@+457jaEs5NF1$`!%snJ&(2Hxj+cwwj29s!D`1g zTVk6|b2y`kd>PdIs>tS#&uw!&H@1m=n_S{cnc^=3s{)KonX~eh&oc3`e165yD!Q`A zn(W6^Ejb)mqOgc(DI4k=k5*-(XtEmumQzmb3t}9ES`h>#E+^m+A<}` zow2TAJ{Y1cf*vaG6-mkCzHJk~w+&$(Dc0v0WGD=HW}s^cYF}{4+nz(UZ?xW7ojYrb zzP&RhQ7Z4o@_HF{W2qw;*GJ<%7u$wr&e(%D)GC8=H^!Y!ksAABJd(>tp=lpghOI~` zKLvHDx7d?ykBD&X5tJ~5CRk3|F)d4|@mrvjld-!tembZA4z9TWUJ>aswoxt((uVro zNWVa{XA<(@;e!i3c&OdFTbK+Yh_3j4k8+hafSqTy6T&jY?j%9hny)llP%hM|PxYz9 z7*{Hk%z}i(jQ#y&T1DI?QqS|o|I~0R;6?BJc4(&noqVsy?%+!&$*fME_n9+` zLxRj*D&pVV+dSjLnrXB#GS;CS_WVn^Z0x>QwsU<>Y@RsN!7JQ`A^z`SGF?2TbGC|SJRvx8lMT&zhsu6Ty3CUuo z^Uwo%st8p@Qt4GOwTqcRcpLIsduM#Fkb%KAM4Jmn_?wd~_Vcmy*@3m7z$AZwD)Kne znF38n#iq!NL2YCbsjhg~GRS08paIB1f28ZT*dp)gL>>>*3>r6g1~GeLT}3vG zlm9(IKxTKSaleH9mw}rj`2t<~tZO-k$O2Zl2c>IjhL3U!5gpv7ta?!^( zrM?ay>v9U=nH}8=1Az%~(f~T9SM^n}IY)s|?CTnFnB~A3W#!kXQ=zGsnbT+7wa+n4 z`H5dqGD^O$1P<_i_DVLo!=-flf6RQq4piz4cgt7i1_h=M*P3iJ`_>u9h|s)qHiph$j&=wtJB_kp1WFQQw?j@n`}(^qIOH=bb-#+yU>X9qQG#*l zDuaNA@$1CCs9oel4Lv1ZQT7i&u5Wms=ewK5%oKdSeMLfX)&I#PN{BhL*Hy^GK({oY zX=Wce$?}8*pzEj&V3hPg?rn)LsD1|{2xM>Fy)e89>dlgqY8B$zbP1P~9bsfBz)O=Y zVz^BWng6IV9aghhCdoAx-8&_u>w^FT{>r-o1O5d6wmfNK(%#e^0%W%{(!xTmJHm7Y zOO?@+Y9PmFCn;`3W7OaiIDjoCk8etKB*NCZzK&s3h2Grx*%{*b`O zLQF@y*|bt9NQXPRq?DGx#Xm`As~3O!OvE&7&H3`&PBWLSsNc_Jo>7`b<0Q?WWK4B3 z0;csj8>h?P6TgHnpxmojY03#R7AU)^0nZ@ONLuW$_19N-_qb6G7HTh>B3TCO0{}K5 zC5~@QKCT^zrtFuh?Hs>^4L>P%zeD@|TKo>_6MDMbGO3&sjh-hkh^6kzM$^$f*3A_s z&L7e@fxOeo&mAL7u8e-ujTm^DL)F??`~ZYjwK=;f6%XlB2jbW*B04sz=Qyx79Awe+ z3B~vK35on6A_OvPd&EFD6c%^oETQUl_YC0LY78Hm3X5{ge$8C|#G3KaDqrsAEquRZ zOSjnE%&jS{~A8?vWm74)0r?DPqlmh1sYB?kCW`=x#GAsK8rWkyC9*a=6{;A z_iH==0i8L)2d61U8udy&pOa<X)`6pK;w&d6N^a+! zR@dD)>A1Rh7DCSFvoZdJFvc0CCsV<^uJDEXWQoI zbT;56_1UWOz@Fdjcs%@0o?U{qY1{Y|c2SJeda~=@=BHjJb--cn(JI?+CmyMsRQWFr z9)K%&Q$ZXFxy9U~#g{m(Pprl)qTe0FF$l+2^5h@nii$pSHi`QV1@~#qmE9Bx0ih>Z z+Bsg;GZ{?lu{FGV+LO1_|GVQvztO>;F?3S68m{?IvUjXDLEY?s5zaTDs0LJ#%78nc|v1B$?OYwG<#-mhW1c zf1mkjsd|tG;i+Ib89Th)O}WinjlzkMsh0olg?oirR*ck@A&*1Gmnc+Oik_6$s;E#_a+8t zEYon_^m4dwR$S>U<|2O_Cp}Bq@jo#*Njb360;h!IL({jnI>R9^kD8jepOc9{$|c{< zN4&4=&_~TM&HLL(go#9}Bw+4HfE!Eg&YdyZ=)!HPrl<#5V8e8}I$Y;2qv?@GCPcN7 z2HVSLDbJbIAyS#QAt)HQ(Z!A_C%uXz)I+%}rrlIC=|zcn^X%5#rI0VgYfx)6aDZ7j zW#L5ANNK9sbh*gCJ2dxu&_yNYll&G;@$Uh_gBn*@1Yz*wz0&Sr6knHfOwl%Bv?`G> zZ1ft}8Y+={S{Qy?;Xn`^9 zA#GfFO7s0Z5=2;D5E5+vE(QPdvH8YY6zLB=HPeJu^n@o8lVJ;?befc1Fk43g>~`Sa zcP@h1(`MO|e8}SOktH64CyzGKx9z89_UF|47SErb(JQmIcwDYJZ>`Vd4f#E zkPT%71X_wbRuq@QX%8pW537LDy^=ssjGF`40|d)BGZyIim#6Z>Bj-eQn!F{LQ^&{N)FGta|wXPf!wpEP}V+(-0ACY@o`qlsN>R34ORbIuiK zpcHIv8Km`xT@z`=e?S0OKZ-^ej2LXQGMd5O8);wd=@C7g-YH2`7Y#TOYN3e#-$A4c_n(goWF423);tg-F zDL47bVBA;gQ#{D&fwV7+==_65ORU>uW;R>I-mW$}&!8Q0JmC4b(yerQS4-(&!S6m_ ze8Dyu1AiubkOa;BtE9_atIrzCVSI>A2 z7Togz%oW|^j;M9_$FLU;Mw4^w5;Cs@{k;HrLN-;ebJ<>C`$T9-SJ?Z&;gckOy>|Vy zqQscwIdz|V`7o|4T2C8S;xA&vON%@CFAC(Ex(479!(n;Je>iu|)?c zxO}m;bawdPsexMMODpWp!GL{~)uS@-%w@TU6}o8S`+s6aLujvEzf`T!!Tp^)R5R3w zKtPbmV!7_1dzuM1djbxoz%a+cOiyx@80;7fdYYg=&;qyhbNT-PjLH?j%w#&N_^s~l zoRv#WV%j4jEVy9)!}Qr4Q2z3?_Yj(0r-H%xhPdFn4v{+4t3;`_)gMR{Y-aT&gT7MD z=vUU#>y$q6(?Hmn7jExw)qT%SMl1JBy`tJ$?gyBtV+6@S_IL;qG#X^<0%dhDu;ZwB zU5YAHka0wpfi_4ixq|o0O^K6sjjJMNmN?OSk%r`~`sF$MK_k9l;0K9u5XR~J^Jcu= zq9jJMn+voV9)BNcR;!5Px@$RQARY)RsO0khbu)ea(v`1;PL7Y=IG>iv<^I~N3*mF| zV%(U!oK;#diksI!^d}Gs@I+k}7FpvQ2Jy#)Agd>kfO zh5DiUBFBeZBK6#PnC#X)H!n!XqsbYcx+lJ2HQj#LUYdnT(N&Hi6X%Z+1O3z0VqZGf z;RkKVST7 zMGe`YF{&L?G(T=(04#fxvTgjmy|H2STC-Be4gnd@VQq4<6^>7FGae^BHy@#HrX9}oXQYP zP=wwvGFX!Ci+lMj;h9wu9Nj#})H=4;LyEx%m0}(iCFLjHBxMQrk1J5=_^6^Yc@_~l zs@~W)*T=wg(Tfjsv|z_f+UM~|K~APE9+a*wj-Oq^mXss|l=5suI?X-T#->a&)5z*B6W&j^pSB z{&j3UM2w|CDu*7ouiO=_-Dh8-PaIiZ;i^CUPoJntnduplIYPdic7{n}i52YofAof9 zwofc{jg=uYlz%`jl8_@~@DJEo2_Q`@{sYod`snUU9@8`7O%$tEGneHC?MSU3Zw}XQ;hgL2Uk5;Z8Y=dxmjWH%PLwg||iOVmFmleNe`(zg#d3{sCZ# zohQ!kpGDBrET?*T9N8*^`5sujA!~4RUy93EflC4NrYx)GTvDUlV~-@P!NYTSy~~XO zGS2s{D=?sYIw1f!AS%ka-dWb+O0TC3eR)5kfqTt-bkdTlVaq;O}jUrxfE!%7qSaj2FF#Y2t&8c~5mqduL(Am^4kOWOJX7fg3q{ z>R#K71`~Cdp|kt)d=|ieVC@grA6qR%J~pU5_rX0Tp}>vP2dQ}O(?N_p69mOCkrMC5 z$QFt!p$HG$;!LPe@rSYk<`MTg51)Q#(?tVAFBiOGI;b9fEvG*yaGxxqwQMzD?{8gv zB&GbbbB@_07ds5!9MLJ`WI4XJ!$h*b~%CRI8eY}TdJZ3Q1KB#< zQ@2iP&K5H_5iIr%tvPw0>lM^sA4>CU`}kf4`OI`!`K3u}yY~$wk65n~^>W52GcH}l z%imhXLUomhu$YwXQO-G}8mMltLtWf#rrv0CYF*&3+LCMJ1$V|yV}5p@O-4;4CF)VE zdHGSL^Z#ptK4#hpIl18V-`wIM|MurG?icls1MPfjhF8=`{p|F?1>-F<>TqQdw#g0t zP0U)O>5=ZkAFIlRIpRh5%~?az%t-!E-i;6$w9CA|7>kELWV(yp^%6NC8{GQ$?MqVQ zn#QLR*&hy;} zbY>cdhSZ%89FIA@f)If-1V}8tj96yH#!w*Fn@xzo)}3X*p52?$F;!7)&%`Jv4Kb#l&zeSP93 zDV-4JTMrLAoP)h3%)Xa@KiiwU{r;cnRvXj#DN2)6fc5C zPx7`MCQcgVQ~7-*w7v)u0Z6pUfy(pb*ehq++~*^SO$!UYh{LDZy6hoyV8iOT!-|ey zmPWlEr{W2lTl?t`gHr=b5j2)p3>MGyDwC6D-AvH82sg+58MTt(R=)=7WQm-63k+^3 z=KX>Qc%FYuPyfTV$8gYXkq$H1%Z~)(;No}$lUX)g^5nayQ zhbftzXGx#6EIL`RxvpgrGf!3Y+@&zgoC$!ujpwkupO+WA8!!D3`_i6o!@!s|Myxzcl=~Yo0$8{ zFz~$-PffDRS`g>9)?zbOX}c*UtIV0D+;y8{?$~=5EI{hOmQ1?v7QY|E|!!ayE}kUQkdg{u!gx=#^G0O%>!p?w7gpv!WkC z<7#6-y{nV|1e-9e>)7l zvZ(b@M(CK~g!$q%y!Y>PT&R+1wuA{Xw21-ez(Rjv!kuzM8Etat{}bE4fWIR|B9Dg> zOoYw6{k&htZ@TP1PwN}+l|bO{BAplF=OA&(97%}exzl>!n@Uz zIiG1#%Py%4lyQr?rV?*a%Lx|Y<|R@CXofA33uZ={U&%;pRkh@Gx8{pE_oc)lXnYj0 z`9qvoHxfSMkCP*NPgYh$uiCT5F6LdBp5o#1KO)4`KVqp54{621Hu*?~cW=|wusR2+ zIhXx~MN1x<2z!nW5+pyFC>w=3P;!3Q6UzD)J89>QSn|^%#F@aHE7E$;>A88KE~{_3srFZO#6e6x6F+52ujJ-9{%IQ@-WiMWo+HPK8a>l~w4 zVw?>7$~CvWCT_UqD(#Pm)Dg2E8gq{&s>yd&7)um-`>yNT+CedYt^BFPoS0lJ=g5AX z+#2&2$89J^mD%TN$9XXh)ovk|5#5{?ZlbYK3Glk% z#qNR~o5$E=gahD7x?DnFZJEj>Thbw`skMS>V?H#hHf z-C8`t-2+_5K)+Z|*|1JmKT!QJH0w|-flXg{8=>z7oNcFhProvV+rRkStebUQnz4@@ zRy3-wIx|qU)2Kb7el+zY;WxUDS7$E#!4RHjG#bNZ^h!0XmiQuFS@05#2NWb|%X>78 zK^;9UWZC5@Ym2wF1 zZ-b@qIj}ljVTD4T`4vI!@yrxQI zA7DWFqc035c*M|BI)&5xyEF_9? z{tJp}P(5hwzV9d9-^npS1t-HdXxBW&Rbz6-(X5r!ZiCP3F5j<4)A;2h=53@ZkBD`> zzl)KOppmn}SZ2<-`KO=ruM@H-s;v{)ZLv&GGy%odwu96nr#|G_veZtS8Xcjo*dQd6 zdJ$3gHmW?U9w*d#xQ?zH*eHlWr!Z*Mdq(B3ykg#BMnCY?;zwVd3y`d?XD-wT{t3Cg zfXr9YB7oP7le!1eyqKkK>kg*4KKDn5E(Nhd@0xZ$_A0pnTPd2n|I@$)ZJ=nb=)`FZ zcl_OL=(P<!KQk-s}cim+6fTv`TMAm-7Grn+h9gb~9V45?(lm3f` zN#z2q!7_zYqogD)MM)fJfLEz-nkbuXyh`+9%AgHMJ{Tzm(LN3_Em`g{t2FCNV&AjM zxeUFMO64eSATFGi4p2LFPwyEjV(L7T4cA4e^SUj_;Gx5v*v%QK7?@M?TP625hJ)RV zh65;89fNXtP|O}q*3qKD%KK$I$yBW$ECg(uFvOl6lK8aX8jYMy5eI9vNgKmsWAgpo zyi|vg7Da2iX8yVap z-Ur>I+8B>Xm1ImY8Ga}h!#g^wZ!D`C#g2VqE!X({{PQ@-jwjHi(sUi!i$wVQb0zN( zZ7|VOb>}ocSC8L}oNAb?cHtNJ>G+x~YgK!kDLCf;lDd$TxNj&^{wTWJA0!LL$ApOc zsT`&)27Ai$GQX5`RG((=RKG|kuK-Zi}c!y|qkEsz8YAZ`8-&MmxZOtW@ zkmcV)El_<6?%e|ODBD3J;wtV)%h22lreXC{k^pL%^oMo50!?J~ zKqY><&5 z3D%d5V1sTMb9TD&OC|;bd1e*7MagZ9L34CX+jgt8+4jD-?53YVx4&rLp^gydb$8=pmE|XCN}oS;>lz= z)lkaq{2hc3_mC>TK{$<@t4^l%Ax^U#kq z_Exj#I|6iP5hwjwf4nS?>6~ZjCdud?RGtJ~U)+G! z$>%0B&_N2>c170-)5BrQb0{~7m87)qi|4YyEF9NnMuSWr8dmh8?c2?ijpCi7MWn4cZRF1NEKCaOsRrhUrA70D|j zP{|nJ<&7~cJzn^f7cxu6zK-o7KI@~Q!szhX%~XrsuM|R=HZHl=37M!tuRG`JFDv6! zu6pqqRm3HIE*zYTp1yI^Z=)cE-$SA`j?Ed%^T*T>LO%j^(Rhtd>`%{S?gdA5WS-GK z=M#$u)YRqGqGjzA5UV>Bm>}8%+)z(ocrp*| zBj1Q)E1&7>ap=z~tB$kL^bLTkV^a@F&_q$-&@wHK`a^}cx`T6;e?*jciWow}L1)8K zNQ3JV(Mb#h_e;5htOMHMxkE4l@6pc7K;>(Pt^5(`>at)xL&#D=*D*YPNh~~(F}O~r zy83opF%+ykwiKzE5eQ5UYjQMo;A6Tmk)CKjvLezweaZ8#12K@j5>;WjF6a5CObsk* z^Y*W743eq(Bg>o$k~v3s4sG?M^_J1KC2GxdwdASKtuwu>2P=|YRv+ovav?e&iX+Fc zn+gNW1g7=y?9sx;W*6aQ7-i;1>}zbHSCOhmtH6Fhth8w;=UcyWaJC8yHmiSXQ*EBq z`Opxbs&yk{B(bP-%CaJ$UiMXEJXc4++C3*u(3>PSllYfr(~m5kj!xO?NYm7YQOXPn zf3DjRVjr*DH$YcT%G@(Ycql(`!@uP!UpWraC72gTK|1DZ=dX>|R+j_UXTIdiF+Kj= z7W--I`^jd%+UG!zdbYWXSA1FLz@u+R;@t#;&NnIw=r?_zwXG2OOCd2hT_ke1_CFFs zFmO;QLhgujM*n~?)k;&8#_4Vko)9J$dch9;pj>>gBuF;&v$=AiYcSd*W^0W#JrjS5 zHN406^tw=*Z04YW5#yE#F^M5qgx(b1GIv$o1idviFRq(3&KvVm=>zoj=l|b^{%=3p zWR`~OUbx6k_)GZ`%B82jj}9cUB!3T4hi7O@U`TP!tRU_9t~cuVKo+snzM!Chol`>1 zZr!h*u0aT!SR{06Xro>*zuTfgOx7X-r${PzDMpvQhvczLDX|@=X;7$AD-K)S*iXp5 zkaC2~q+K0bvgDqU5aRS2v(BoZMgkGDwSk55rRsB&x;+>xA3e!Zfsu16} zi-~SZWi9q6c(K$M58#@c1Cv(0&3Pe2 z4##xlwP$}g`DHZ=%;KV4tt3O=>U{SwH)Kk#X!vd*J?tkkYTj({Wk}b^51OvpyqR zhKuM11-^97zD52eW)8H-tg9FyP!NShow5;Y!6sxdBT@LeidS$$J{48m_<2 z6>~=KYZ4-wrf%|6prkC7-i8qlpHkxqzL(g!E0URL;sdSQs_|fnDdx}TUq=HiX~tBD}OWl+SfZFx~QwT{6)) zf)fInVtdVI)=p*VD2Lxe!r_>6jC4uig(2be92y9lGL)8J@ULy&pORTu&3v1NGMLyY9W_UGEv{f>*_kaBs6fdq0GqR8xezHXojrG3XO}fIDm>2&5%BO8 zQ(l#n>lD;EgHzJLev0pZ9q9azuLA4fJ#;SfUA#*SiyJghiykrD6r06$c?nGJ=2qno zq`vOs2IkoHazJ$lxyXsdq$!UWysWm+^=;6;91)~DbE9Av)O*2d!H7YctV0Giw_QY@~)9`{-f^uER& zIN?)Q;XK_=uU!n^MtCb{Wsq1C<(wV3BnQ=!0bBfi(I*GVZ(VPMC4ty0F@jdLV|a!& z`XEja{B$wvUbiPG?XXFtrN` zY?pFjJtxC$RY^q&u+3T%qrnwjw4jfw!~)t*KbQHhGNdl+0=w6bXn$CrX4K0z1oe@H z<^~g#kf3{BGG->dYBw>Ji+5fbX`I9UJ;=9_rVa9Wz7EI8zMu@n&FK;{th0MHd5XXS zZdXi?5(+y1APL1Kd6^EfmAKwxzOh|I+q=q9dWzV|N7W$9p;M#tG)gted$JE(;DATmPvN9`uMql(l33^sUf1Ms$`=5bkGu~m`!Zbqv z^g6~`0Qw3Y54J4D#8jck{gNVA{WFm>9@6Q!Vd>cp>A33zrcC9(IE?+xu$YulpWze& z*ht#?YzV(WelKYvIA%BT+Z8L^jfa10y>ghYbPGuuJkd3hg(_P$+d<=UYf&u9le+1T zLHR$a*pI8oT(cI_`>KWmUu^_I_Du%EwYhz-jAf+On)+_HB={zgSlc##Fgz%n0yx-B z^H0aD@txHivlwH!qDTomx9jhJmrxKwX0#IyoqD6b2fEX@pnpZE3(j^R7ze#{Hpy!= ztZ5TC7Gg_AKU54-3z!XoNr|L;fbJ-}d2fn~k9%|q7r1xbJ0J2)^2CF3jjbH5Hzdw{ z9aL*)?QH+lXamUA0rrrbxAv_yk|8%U)>(Z2SQtO%5UD}-#g^jily68h>PhRn!dOl!-UJ3T}8GGoN*N3xT%>qtg>y*Ok>1 z%X)9wXXq{YB< zzxmwYoi8b8VlQ}DjvGi{ERcakwc?%fqkMqm1u@eGoTY3eq}VNBFtGvKoUUcyU*=-iuCnF5$?lx9KV> z-e7xL;g!Y=?J1s*@wPS6Do=m)zlk0SF#YAE$HtexoqZC#{_x}f?UzfV3a%J5_Kg2W zP{;<1!^8$RRq5=}6pu<+WretY)mMf;XG6;nlw4gUa4cM=ukijKB>w5w(j=*V^laVe_@#4LuVJZp(aR!tb+x;stPmjGU%V-5{-%O2ea5TCiw7 zV)4DwITr5CLQ}~!S<5pk>wKFo0@|Bv0|eAa-d=;eTl-ELqFLZK~TfcH>XL4 z(z-(~Ao!;&*lW>nb&wij0&CX52*>)S1_{(C?!nuNntbDAYSzKl{frqI$QT;RM&muS z{WGNEtGRre)gb{SUsuKi`IUF&w2mmIH9ztM?fH6F?HVQ+)Kep7oK%qPRMn}deB6;b zFN8ZM!-(J!nIJQb%4R;_&yc!wArQU|b`!St&%4H_f2mV(N_&u@7>)t0n4*ZYEc^PH zm|lfcESJ^r+*$K|5_sY;S8lc?K_fjc7ohA+XALM|30g=Qj!m9eVOw zWLMT)fz`rhVJ`rv{bd2P_^8Me8^+#`=9`wPGLHZ1A8YrM4A%+t16$7+Az9We(YM*; z?GLMsaulzt&Ya)D<{caqj82=J9_`F~>%;ha=@-@h+==tM1A;cKwWT8i_G% z;58K{{ATdH+4UK#0Y@=(w&^Q~p*>B#)O8QCZJ%o=;S+d!_`hksZ$_HHlwa@;?`3_8 z<26ugwaMo!dRsDG37nJ+-0E2H9^?lV8Vr6H0)O=@t8_n)zM}u~r15{JOJKp2w^KhD zYNtAV+1@NpshleYuEQQugB@QiH#J<~amKQ$J7AT%QYEM?yus+c`tC~hrn`#mdW7h} zg9=5HyJvxW2BtfcdENjxWlM3%TcA7khelDokrxVSQhG=h!!oz=<@ZW^y{I$5`D}hS;@N!R< zTWePc_hOH++*Jf$flwUd5sD#uw|KPFc^U=QvU{@@T~xh1(s&R&59qMlD2>-v~%f4Z1J* z^LIwm#0y1NxY!p$Z$LZW{y(&xXHZjb+wFG>Ngx42=som~p-3+w^dbgD0Tl=x5j7N* zP6DAyZw5i6DvDA?5k&$-Y7|sdR8$BW5fwyGkoG@$o_U}5J?Gn*Idf+A>@WLUW-`}( zUu&)3G7Qs}ZT2>{Jw1_hDc|lV?aXlp7%1q){i6Wl<6#X!UavU7Ggro-j2&jQY|ES@`v>WRb8`_7#7z3Sw2_i=L1z}LQgr|}&YA1~8|f>$1C zGu(FDMa1_fwDl35AQ8`JQ3#)g1rv%&Q=fe_T3)`g#MBe^_?!Uf;vF~2xs=c((~6^$ zO5Anmv>nEzLRRxUHwk%SB7>)=2u4*;pOL1+zG`^yAjVF4KHxx}eto6rO};oq@AQ{( zw-FceDG3n4W4>^t!mMAD)A-6y*c4yjC=tWER&fx^!Z-x1>TFS6V*9Y-;9X>y%c${L zO{gp`+B#x?jrNNC5lF+BH>Ikqt4HF(rCTA157sjj+(jIkrwh7-ETMf>J`mt;KC$bj{zMca~`mI*PPjZ9>Ce7FB(u!)5 zQZLZ;<^kMWyH#aAKFMlc3{$u5VK5kCu4zisVN>{yZ&wc2-j!N{QUjiCb%9ZbvU`vASE?*C-Gjav|P z;hVtZDoA{3Uk=&eO~w|RIP!WHMBE-6zM$pp^$kFQepeb)p0dI+|K5}R7C}TScxb(X zS~5w{qNsB^-C2x0#gutvznTtxGZ~IiOyPUmc+}-U9Dyp|a7_WQj0pC*z>j7er$@1z zUIf!GTu$Nmam~tkH3|Pt5e$F7dM9Vw>Hqmf^_5@vkEkFetm*|hnQU5ZXL*%KE`Y4j zy9G`Z$`t-0X>A^RNG@#zjx^L?bv}Iju5#9S?DRnQmB#y0p`ttmHx*Zl;x$7q`-Q}L4wI}R}&0T_=lF3g@aqBsR zq08^-dPBX+sL^rGBKk?_*=B3>BNxc`q2jn{8741le`xuS+Y_+iWBYkzI#_mRrq=i~ z-oeC7j7JhG`cYTzyDbIw(s}lEv;7d}pZRq@>1{1M6ZXX)!S+@emJ;(I-ejeQOB;eb zSL04&X1@>Gs>Ob^LXa_zP(}BNZ&=dxKI&tk zdGVU+^;xn&eJvatN`o68-(B``liyrb?3PoQ>5jaDE|?TaB?=b$KxA_nZx0inxL z`V(KcA>HATJN8{cS`st`=}FvOgI5n-SktlIL?wk?LXNU!a840cDV@%9batpC#P`dg zneX4SvrXi#X5T-vC2R-m5GGu49+ST%qaCBB-`pPm>OYIM@*TK*J)3-3tIou4KUi8F zB^LtaZQraA-axlwXBNHg;E$f2u8i#N&#B-CZDb?y2RqVX@kSfbX*gq|#Iu#RzR-IG zBrdq12gYA;B)U@DwC6rtEfJUImfC!U7f<&S9mtcV^P-Z~>X(vJ=fFv+?^z-OyjTB? zG}?HZ9`%sAz!%3RF*b^>wGWW~xz>oo7U;EE}y)WZYmjgq>czF*H zk5N}XQkv~D~BS=Jy9^jUcOiD3*5DCWSs$bq$wHQt)e9;u>cP>85i+@U}k zWlLdZ7@Y)^F_`l~Q5_8)d-HRKdKe!><%PWhg4*NlazR zWs?+L^k{d(;h)^{SxBNL$>%wPC6K>K6?O}M9Gv#}VIl*hR^TLyH#+u65x5OLcL5Y2 zysVpF4Ihu&Yp8X3jaIsptWqm;$#4V)ZI7B=xP0a9oBMVlSG1;4dY$19^nZ+$%6V(YpTJgyS+nY4_GPPV zk&8j@3X#`CML`OuUd@9uN}>k-6iy#w1G>Fo%=!t%pKQQHbQaq@lQstMwOA>LKjK}D zk?~eDFLM$T*kQrjAcx0R4&4w0u0!JMXF-o9`9P&wQav^(CvvTFc|XgpVfl-oKoI9v zZnLndG8Y&AK?bpTLV-d(TQ^zq!Zu{5qL_ zm+ssW4N{_40v`k`DPyEf%G8sM>CPnLRhK!S?FCcRwf=+RK>9aHb>yX**1GnLe(yE^ zY)S1>9Ta+giz%YPhp*q82lxw=6tYEj@(1Gr&Kxel6n6Iumk7II`{q^J5zot<+qrCoqpMUugO0(`-FZQW4bF8 zgbq5~t?OVds4aF>J0LC#`ORbn;`w6FbA`(5zYhOrFb^y&k$XtVx>)I6G^9BkgHyNh z0+SOh=N?ga<#(CVPUV%QCf4Log`$&_X$k?cQ<7N~n;I|Q&Hu)URUE|@DTQq;9uSw3 zucRLB?H2+DI+l-Rm%OJ5S)*?jHk69;qq~t}nVAcG&BpHaM{&PFM&37{2B;i?%vo_J z19F#ziWdn0U}HL>$B)7yQTcI|&?mgOazG<^LRQ$ZTwS|>a|fLAg48H@=*O)sw$$}~ zj<@O|VxAyU-toy0mf`y}(=N?BmL$F4l_QX!d!OmbuG_kwT zdq8!oT59EkLVZ>q_n<>G|K-GrlTSL(VzTgDF0DB18uUqx+`QPp8U)*sC3=n>Obm*P zdItSL`Jm)9`aUKn7}e2`jju{=tT*Cr#&6UnJO+m}`QlH)2EVaPWv%MLawvGj?P1aC z%(8KKFlo?XI5j6&)wzEOGjuX4TjI9-N9G-y#k_3=F~{b)QiS4wKr=_q{o4m2{3(R@ z9VM14p1<~jskBF@xLnfUlwT-NUL8e`PTZ`ZbYdI*IijBs&6%mdZ-I=NAdYNB#euCs z$awGSERZM4ix8&B&mFXz3mcOU2Zlr!f2S)1^Rr=};~$Ru-6o~WGd8rlKXFsd0_6_( zme^fDPv#=+h$TbYNqJxbhvV^lEEyn_0a7@=l~(W+BXFhJ38?%eQhsW}~a^fk_Qx=vTny6tt@Z9BYp zVe_IwLhjE{P!K_dC*Ftl3HZ9>YDk#YBKbG&P7Cypyxz0o!wo>*me#6) zNGAG3FkyTAhRRvk&$geE*fD`|9FHL=Lc_b>SQkQoLA~o~j$IdDi>c|@Rx@J@hCM165sfgUy%(1Vmc)WD09se&Fro&$E z(*ers&u|%0KHl7afWrax6Xkv1=<@frvyuuL#Bva=i>|8P=Z87cgOoO$*wzH*iCb)j zD-zYWpw}8t%LbjT%slrE{c;ihRnlhU#_R=`T^yyvO08C%_)@=h3W;Em-3HRhxCY&4wU%cLFQ?~Tu$Q3l%x@7?-zpFP3>Vt0 zB+1a>X+})OXaM#xzcg1_F2q7gxZTx#{Cs{@POSt_3)UBZ4 z5e}`BzvXgN@TgS5t(Ds+KuCoUmBkK2!891r&wC3xSa9o1lHOB}hB{1Xa@I$zn3BGG zY4-67v)gx`zhcw0B6sTb^fkd#&&BxqvX+|`|G;~Y!7}m>NxqXg2Q~o2ZiL=@hHHHJ zjen1%tUIxWGLttmp$L8<(qI$qNj-Gy>UOG%)xYS&|8_|O@n+v$cY0(b&jc>Etr9Pk ztaME)7b~ewZUU4o$3pLl0kVM6y~CH`SJX}r#((?Jl2#Aq0d=&27@BLkuqK_vZn%p~2 zkwZq0NSl(Wpl_Ecq|VG_tq-7}(BfeO^&mI7Rw!6?HzDm->DS^}-_tLqZ`WI}#bZG> z-`9l1A=$TOqmmzE%(kPN>Ra`kg7L|S@&!c)S{ zfq|vChH?;pM{4h^w>UdNRz|MQhKHLomk7Jl0^eV{Ya`_W6%NvJSzA+o? zB~w`(^Cb-h%~hzOwF9lY9Q%*CSvITW>x8X5ns4#tkcl(hhwk1 zCfUI{qYvV^J;9{!w!WnYX1T69+U0-MzT5DqbgO4YUoEE!%@r|JH zlxHBfgJ%61U;3GD)?G*NF5BTve*He5iDvJP;N$S<<{nnNqnqqte;X43h4tUDL0CT= z^l%J%E>ygbDFU3ir}s+~0`*^M9LwA+!_~01Up%;S#caoosCknj%Oi2Ys{iNZi1;rx z|CbNH=c9jsk^kDq@5vfP8wUM)sJBk~Yk}O*#W)9d?HxuJHQtAL9PP=U!X*dWe$gM? z!xJL6saK~Z&GoX*t7O&z5IfK}{kJ85kgLE^1ow|Ff+{&ZJ#qW7Q5{z5eY39)T86k) zVFbXgjdSfe(2}u;{Tx67nsK)3hO2Zh4iIG=MXc9|2fyS*JYp^h*HT(1=`C_3eDv#V z{)>92Kb%p`_vrWL^$_$oINS~bjlO4qp|373N5~B|A9^nvm*c)-joVOcIIgtmk|^57 z7ITS=5jq^Snc?jn`4LAA^8%*y-yg9p#yVsSirulS9tV@8oz0$dM6C3F7%C1eaIh;F z^5?B_9{+xFk-aF*Bnpmzr%VY`l}qO&q8~svA#DF<1o6(t7>N#3qIlTbV84SL?}1xP z7lpi!95Uj5kym=i+A6xpux`h;0(QSS@|a3ec8ib_yg3hO8g`=fAwUVN#?uqbQ$v4u zS$K7dKI;vCdWsU{+3~mG$Pwv_Ln@D*@*qiDt|ITfaCx2lSM{%XYDUUgHAw>p>+%0?Sw>k?#%KA_9EWVQ{7gX96DiXoF$YLMovH02x8*NZVYdWW%-OoTg zOETMG`=XzmxaJY8LF^pbOz-@a!&H1FS}5#R_!~49cl-$RfJ|5e=5rp$NKpKWWCM6+ z1YL5S!85C^L(sR6;18>7^S%eu6;Z*>elLxMRodz}T!vS97On zbH!|Q@2A4XG5H^iWk|=`Y_Jw$egZH&M60!6I;WgYnq4bv#G+8PuFo+N=?9mBFY`y# ze=hexl*jN&X^>EPe!6rIz|yp?Vu*%kCL0UYrkc@|my-ajtU3;HM_|(0|tD%Rl*}}E^SX%BW00_~C-^k`#gR?{9 z7G8~=WXB9q`+Mf_met>a;DhoHUDdDnF6c^&Q_W9h`y|%TWYb$hr7^=UXG`jPfkY{C z#k0x=_51gCmdpb_4fin*#}GO`vK64$MEk>SAEU;?{5%a+@}wMlKD50G7|~)4y+dx& zj`}7$2nxy`02|5^zi$RA3l*od!|Oh*eZ{bDoMpq_S7)@c?)XQDmbVFAMoAn1Yz4`n z`sbP^3Tt zCt58&JEig?LIhf36NMAh7RT+N=cvC`o>=;epzm81Dz zjxj-+m;1dt=?khNNH4lBBS_*LtFKMSV<6joa)ek^^V2<#RB6ss$&V>EIB1R;xGHeA zne-uWfUWn*;_0Up(%1_{8L$NxwY!c3EkDA7CHf)~Fc_-Z6rRfex)}XD>(<0^rO5)- z!Q-Mktc@?r0jF)#V-qqiW%)hRs`_=kHnU)rU+nioH$O94QH3dHWfxs5^)qmv3P<|o z_xBv(^w@cxsMF@F8bE*9c^k3a7*mzpc)Hb9RVX0#s0p0ur;;|Lwk9@ZyIbe}mvj33 z2yx{HBb@hQ8}n9i>g@aWt=X1`4+<(u_r(aEYF3ZzWRdO<&Whe6O^=!%#`lHplwGr2 zstfkl|AhI--CO?42Q)W2b3M$OLlHl-e#WHM+1;AuSzGEwA$u!2D>@4{`-2=^1^2H1 zkIHM3W_e>$Tdm@{nn?TH=i((zQ`CpD9vxu#pAt@Y?d--)A|ts3XNm;qt=%+u8{EJ;Fhz2YUKeNwq;VOd>SI2eh;o??GK zw$sk?5vSfLrUuA3Kx5&M`{ivD&xIpB{(>NNzm#iL(@xGgfPPCaE5(!U%%L77og=Eg zecz-mQY6b#eIEm=6*b0qYlf|Y2CB{W>a-);n)#(=@t}jD+Pv1WV>6!kA1-dLtB!id z%(4)~cY{!}t3+OKySL@zz@fg)il?BIdQe)l(vtP}s+;-#Yy4@2g5mCb9n-cpcReM( zNlI>99qTs{rZt}@EDJ6E1Kcq;b$r+S0tV_)*Diq@D8!j=R=3EQl;uy!P5rpq zU@{lJ>u^l9+jC$SZHOOzIv;ZJ*qWI4$gOVku+5CQWA_B=URx|nVfABXy>8x~4l=&A zYUqB)L1At->Tr>6KMprxR>^yJ{}*aqckln$#X=rSzC$-6x55x3&hIMy#%Z?|6|j zxQw_n6AS8!xFEzsLi`=X!Bn51%nwub z5XDfX(K;DVK-dGmIN@yrvz){Ggxq7H4lC|l7Yxl&3ba_n%Arz{RfqO@z?@7UtepW@ zI#c9``WmIZv+o*iKb857`4VTM*e7>$MVfb83gEgMgZrElP8+Z0>uKG6}k28Zx?A9)yM{>22vhf zZ2v$z;&_oGTR*n;Qk1!o{|z>Z>$8_Dnh|O!r@xalzTSoz{jp7diu|59E2Vzn9uMyT zG=&5Szv`U^dCvbi)NgjWLoJsL;AbXM?;))cmbK6pWCOvAh=hS32jGGEJmQe;Rq4)s zxoH_+Y#-W)_ct?ybT4yi)jtc$*kzL}ez-{n_n*wfU!hQtAd4ssgvu||y#QseLVqvO z8l69?NM&S;H=R)RN?&FOYha5`qlU{Q{7~`ti!sb`*^AQ6}3Knepig{_Ee% zaVgL9J8V;#53e_}j8beoh|hZ552S;RG+E-LbFK%A?>PL3xlC)3fJ&@HVp0M>TA5zz zUL_MlA*q#=SUJ?si%RwtiPIqT>mmBXtJO~)gs4Xz4ZNx+V|<^Q;6;xA$jeDi-Ql?m z&53#a_kBEH(%}1ja{{RdCzr8^a1$s{WOF-ENIAk8;>HjWeDo!;xwwhK~Rm+-2!22<|huySnh?p@;`--qPvBxG$4|YFbPgOCaB2TwK=0;FPSy(e_ zgCI&mx0WziAT&}A!87jrA&CHg3sL|cOwLYug!ToIW4%>=)s44 zK7XlKiZ|ClvCrkQh^RU*URBHEZ+2`hf+uGoYTwKF9^(%b&w~WU_;&4X@IL5gDH~*` zq3?QFKkA%N2b~W!4z~-JJPVf5!|xgJ%DgyEw~6E}zowLpkf?i|T`;2w&yMZLS9bb5 zcm+-x2tRUBIbnn$VLMz8A8WWWvCll6B7F15pmZJlHBRJN!~NN@-*twzRJlMM!Qnhq z&2hAz;=YRSaTWdp<6@SV=;s1;MG%lsQ_A@5QA$8UFKrQhlpCa(n%a=?F>QH_#~siW znAc4%x@+A85+0w-;z#clvT-@s&Q}~TLZ}?#c5dN@Ej4hChrc#C@3c&Bo41sHJGbS8 zwte>q?FF~0n!&l6pTx%eIHl8zkd19%hDj33t+4( z;(V=0KIJ{Z1^%T+!nhw`v;}B~8lNKs3xk5=DOsWWo{MN*Ctud^>%)WU{sA1M?SN0i zf>`@yWeCMxkj@KSmk^5-bJLNWp3qHpMPOfU=Fu_?Q3|NR-4k%hR6EDoQ6ewlR>E-X z*@5x>&+e~%T-x78AY}ehZ=)k*dL@|`#Fxr`&V})9xuhiT1s+d(N7DvkJ%fIx_RvmY zWm(ea<=LK^W3#5)>V7T5pKFyHmczPhu@cbOZseI)E^~u4-_tB;|AAeWps`gGY;dUSL4{ES){n~twLEm_kxq<9^yoz}pyhF+;d`B=wzWxxjms&PXJOkdhfx9b zKs{=J=X9FRqj@)3DWY z^~M@c{V6RrWaf8|vx4j`mk6YxVx&*kC{15VJyZyhPv8uCOW|h9YsUO$2Z3+;oHV63 z-5M$_ zshN#)gD?045!$nRs}Pdt8_BR&xNHR>rYbck2ageKzRf1_tl|e^rM7?9Fpjs;DKBdr zrp2i$DL;c#VzyZkZ%*815LGUXWS>`ex7?zjFpuWB{iF&622yOl7aA+siy1}fW)Mi# zy-D%8hpheYQ%u2y^c=$`uIrlr<-4~67meXatk77_ijRe>`S6y3eF;yE$P{&cGd(() z4#1ne*sBO|u)i&x2V@eRmh-&~z%uxH(6{p0ZzwqYdOus$`8)n3Ihoi%I8$Hmf;uyI zcT#2;(3w^Mal!mC(UtxfR+Wha4Q^|Nm0g|IdK$AGP4Sf1!hzI1CKXxE8&9%9?_sbQ z$#UO(gv$`GTUPkro<-e{-2&gYnId<6HfU6bd*(cJA4&1r>ABYZ$Xm{aaT#B41%e0t zb<{7Co^8{+9O1+gUhiyuuhK)eD)=RX@U>P4aWW6#c?<>Jvm0-{O;A}#Zi&1e)|LnmC94i=lwZIrsQ0 z7HgJeL+(sv)&h}D5}AJa-~MJ?yshud7J*i{6|W(qSDt|WQm$1P{NTGG?rmsU%io%lDe&g(jAKUc5sqT0 zxvCEXIlV)0OM_z_6Dioq^g}oGZ2xB9ydH{~?`+$7C<~@u5}lH5xTX{$-9Q$Ph+h)B zh1V$vFhmQ=broTY8|8pi3$!Wk8PFf|*3TgiwN2{Lj9`xF`mt;aIG8`lgWd>*TdnHQ zW}g`8^zM|PnL)++lt>i{Zj8ly z>MEp5JAX5}mv-=J&fwDMjPUjQ_uH)22=3O+)4<;B+{*M)leCu+fTo--&d~N)L>vyn zp#-n8r4VVUw;y}*-2E-sBhs*2Iv_0o%T?BLjlpwWE~~wq#AHNP&r`=$t(QLo0Gxf; zQx(0_Zwn;+seV7LC&>lQ0x1s}JYu|+N-w8L+E&NcE8cDAMWT$Gsg1am^fhg@q~8Fj z=EODJtABuIDx~6g>Y>GuCbSK>pdb*MCU$TdXm@!0e#ZeX4(J8Gp<#g?{^5}yjLJ}$ z*1_cQb}Od#8y+XqG>}o>tooaK7dYNhJzn6SBePgsXWU+0yO7x`AM{^QHoRfVeRP2* z{(~Yga7GH2!n$O?1TbM-oY*`>e|;`0=gc@M4kR;DX1t-11f zyxOpuBq5a|{?)>`&s}Mf9w>av&Eh-#rKH{Pn$?!97Zi|l;nCN$Qu{+vD-!CzSk`r-<^7o?&9(db$5`6BJB?fi3*gg!0O)#~{Wo^-#G&D?{RMR(0fRLu z;)&J=nqKmQb&l@)cZZ!b3@x3zsAt;Tq&&wcdLHayJsO<+>m+Tbj|<{Feodp^klhqF*(l{;k56duTb)ZU%prW}ymjeD@L{jCFN!*^g}a`} z8v`c=>KMPImxb*qzT_ogVf(}$jRI3^*1%lKD$mWK-+b|sMBp?_hnl=5216m&bO(AT z>4WH30(#iidDY@D^%Gdxc!+`v*Z`xaT<2u-kvhG7?<-m8y}y z3b|E5vXK29^$8X(;WYItg%k^$Mbs4P^`5_9sSI)+V5qG-oVkY|UX%$mTS%BzX*C&c z@&Q8YTv{8Rg^BX&Jlm#$(wIDsbG8|)(>ySLFMXvHM-HT85-kQlu*p3+J_IDB#GSd&1+LNx6Q@&MUw)n=NbFr7*o?Gk=dImWg4`!x{`Vi?6OaGP z7_Ndxxz<-2GH#x6GVhPv*grsHLI%zrO)^1sU!T0XH+q1ssK~3R9sbugb$#skvBT8f zRm1?c%2S=!ZKw6&F#pYzKz_*MuWlL&**ODN2K?dTKe5pM#_;x6ADLq6$0lbFI1ZDZ z_q0Z#@o|W#JDGZG66!Nwfk0l+MlcgU+C z5p?92<{QwtH2R2sMsPv5l-o&-ThVzg6Eh)%0qgx2`1$0bK9JmulU;>yPdnl+9nqvc zP{9Z87rdT>zl9Ol%1U!jP``ioGFewdL)--;qStD7)cy0L%*U)-hmlFsiX(%M=HH}W z_+iL%(@2pppdK(ljN6|bPMsFvOS2}vXjdmK4h0W>Mu_9?eKUX{_SSgY8UCOso?hzj zS#AChOd?yq`z!Bp?{!M2T0xSXR=l7dN#IYJlKAs^k57An{PdVd4cL4I6N81Q2NCVP zX^hK9&NPT|k*V2QO-tB%8>E~n)S`}U0__S8w-Z!rR6Y60&*JeRt4G{a1-*T^Hgr(d5J+OG+-4} z4_s9OHUt{Z7StO4Q9q^zcVxETPaYQ@emc(xs6HTfFC|E&GtLDqqR#x}L9Jj9o|qd? z`DzPX?q>lwg@t0O%Hk2blbMAd7+F`Sbz#t2uzze2UxciG>4HYH z0zCN$KS`;(U_eMx&U&4qUPv9U-Px)o%VzVJH=tX8!U!LND+Bo@qvAhXEpN7d&~6^(gB&7*&EWMciUWnex%{Fz-;5G`dX;S;DLfU zLp-^$1dOb)&w=ej)*b%u?IQm@1OIAC;qB`n)=i4bFn^_dwj@N%ygYZgP%g<^-z`T zROdt&cb~mE-a`8Dxa5-fv0bKfx>WBOF}BU@Z#v@M-sd4PIjQV-`_Emibc;`BAlh4g zS~krj-?ptyiSR`;7rVdlS4@bnfq=2(9;)a%yM8g5#FI%`HS|iL1eV=pb&SfeDq{Jb zL=Xk3A5=qeCX{+cck-Yuk|&J@I`#_6K*U`l)zH=i++&xd=kiw(cG}7BCxp)pWagvH zj^LHR0>R}r`<8i%?`0#^Kj;3bRQU%;%U|pz8l%IWO6WW+Ssw z$x!dQXTRmn=@0!WoU*opn#Rcgyr3%h&5yeu>IB{O&VY?|O4byD_Y)+Qyd;Zj6sk@x zi!V-n_hGttd1Ls6met7DvtQZG3#Y>c0V)H3DXqTQO7Rn2go0pDp3HQTw3!d$fQRTDJqN3Ug2c|$jXrsF z+Kmq=__qGHs$Piy2~^gq4*NPIyBJ|%zJIg);P6Y{Yzuj38|^WH<-^$0xMuBazVB>4 zqxUQNm2&A3O%K4*-AFxdxo>{R)(ICMDfbO+%*yPb^}&zAfbww*2OEYFE|VRm!OTxFcx7>u>1+7x$l!(0d@y;^F49 z_4VaznsPfL-D;Y=8b?a14Nr=)!>?9<6vDV399M*fqp+&uis?XPq*XlK8{Bt|8uop_ zSz8D}`uuRC`ShF6h1j@s)ziRE!uSLBy|3tOc+n2QTfPA-aVCTon^lz4uV3N5fcpf` z^Iwx18o-~1b-tcfFA^KBA4*?A*UAdSl>w^)42@f@hlg+C4vz+6vwgk~;N`8l4lD|$ zYd<)5i!uJ@4EF}vTKR(2&VT$j8zWhP$P+qda?ETYCk|@SjMaF*9-nooZJP%8)ZP@M zrXrZ?ahPn|T8mWNH6z|s^t;gvjCN5B5v*PMf@-RyT`AQ%KXz7+0xdl1rpS}uwjtL) z7*7Aa73g&?B2I?EYpQ&zN83aEu%|XPHCsB&iH=v2V54sMvSg~|QhR);+HU2g#ZhAW zjzt*6%0`HZEi0e4c};HLhfdGho<$|Loz+8i+COR|zFD4Ey%N{+vyzbwKHjExX5Qnr z*d%jbCmEyJfzZ6TQt){*z4vJoqV6~1;=%il!%G-mko<21RQmmy03W1BPQbzOkh8I$ zjmB>bUgrSO)|GZUM^T>?+~nn#%!vK<(DLbj&%u9{zWfVW{jXS@*zTmzKLFlZQQ*X{ zgLCRpRsR5rpwLl+L>pyWX$PaEk$XXxs7wusKN*KJnaPrnRdjC^l#E>ihU=l7a}%Kp zs<;$x{--)t4f&@I4ZhLAam3k{%Tz2y@gK6G+dpiM880Gn#04RD z^K5N#uHnj&=bEQ1knt(2B^8k^+f^_Jx#{P8BT{U6{k;&{mx1&j^hf|p5pfdyiXXSP zh@*+X2Dh5gN>`w@GU@8yu9*1bM-Arj9(@@j0jfX6`V8tLX~Puy<1)f?zuX8_|V)*7+HMXx0nh(y=v@XJEItJqp@q=3;IEFstIfy2Wsn!>yz zBL-p9cvI!RB|}xfjMs(#AxBTWWM8GtD6$NcT86YS5!W0uf-AL!+ZZRsc8PvdNdK`r z=z<1dcj}13Z>m^Q9!AkDaKU8ap$x5vbEkWj?KjOHA5^hCL#ijD!L}-eF+zC9-}Y2~ ze_X z^I2!xgHY!WdxGi(lNdbu-P-F_n7K6DB+0Y5uN`3b?MEuADerP%?^i*0AdWI|xsSEh zIjRRx2_f~F-;bP6#dXwu^Ux7`oZgR~A$mP+IQ8E1kiJ7#YY=2KZCy6?#IOzpId~W- zPYj(q9n9$Zs^_u_;{P?Wq3-9rfBUO;ljz5ww}4#!fo3*l92!0yZplyQwJ$Bb+M|2f zinqcgYCH>764cobUrOwXkW!h3D44QDc>37LXSIP?59qdfm#c?R;E6Ms3KK*EL&PX( z%s~K*fZ2vIZC_=P-+4krM20>J=s6qmS#UrK`@)(DVDo-&Tj?hd(`wYK2AuY517$~3 zmf&}d?4JQ|bp*+uCb>EEd{AdX>Tb|p|9?xDyv4nP3)#6U^($oj_@+Ox%SMnxlCA!I zSPahS)3#;sS+LH$lgADOm7uil^X{`=A)5--1@L$51TB?+F@qG8*w zm~jPsH!34q4=Kz*$k@?xYGL|3rj)PNCma|s2hc&*Mgt-EmjS+?E*{XkMRR?d_XOJ0 zLKC%Z!0}Ir)hVof&1QfgFRXq7&?B17K4KyykK2Uvh6gC7t7u;QM^&wMdk$L>9j>s= zl*f;;L~?C4CKzgCJ%r}*77kdW2vcbs&rJDpotw!i0NomHaZP!Kq8RR9u~t40-#{)s zAUK--$;&;?QnbKRFEv^m0=8UDJUXy)RtgNbdR)#sf%gMx?-qoNvMzj+C|Zfqm1^0{ znBodG9c9PktKRkh2}(f5S_KgC!IEa1mlZ4B1WeKx4af46QFwF+YHI@|A?)z?5A~B; zOBbj!z4vdWMVETICG}O{-5=e|Fv@yE`KkQP>O1m66x~d_7Hj)f7{Or9f6YjfA?-Uj z#`7)vO5E9k0BwxvGSc!lZoiFpb0l|7fC~qemlC&{1I-i!RILO^ps}eQ`^EMh0v_JQ zfq@4aZ`Ms@N5TwtS(I}NWZ;Qn}2D~ z)XK1n*f->YPDj+1i#iCTF`)NBDMH=EOZ)vp0s_f@LG;U3^FW=SqMUvc(C;N076+L06g&mp$N z0C$mV#qI^2!rWiDRqESUWe52oSOp$~sMv&L;LAq5#I9*}UX1VU#~Y4F@9!Rq!?2na zg)EKK@>`epoBRK*sYj!5D#vIvyd9}#AB9(ijZ3M|JsxD2ag2vpwxOQr3V!iZ1o4qc z22el*aWXtpiN^M2hUHO!rZ$&F`nPtV0XB>}u6nVVgal68q6ry?1PLgMT5#38V%6lh zb{ug8TFAAW`vDFG?|kya={9u@4DO1EV*rbNj+$9T!V$IIQL^p_%;lU6!2b4>4g+jk zW}?HLnSj&y*l{pCOUp7#c_(RyyXL$(OoXr|07{85dA)fHDM_GNYt~-5pPIus6V{_k zdvG&EVx#yW%~;B~u|~Tr9om)<-u3;ffp&W@B>vm_e}G9iHBH<*Wh>ah56P!Z{8H^# zlf}C;iDE~Vz8GO^byfHoRmmZJ65fdWw>I1RctZNn{HU7lPsqWih2i?}U)2^kum`Ll zeQg(IgPEfs9O!>`?6uTc%ZiyASv#{8fu8gVTt$jDKcyzguz)N=%KS>`O-dOfmG@!g~qNnbA0Pe*!NpgmO zZ8EO6Mp6N*{OS_|ZCAtE-RlGQh`H_^k52Nd^*e~rTcZr(PN}>Y{qkZil#QNNqXe9oeoDDJg^70}bao41f}yUk(b0L|QWatEsBBeyE1#dpNksGV${+InHM-*Z`o50tZLu_f3-?#4knCsK=%lBR?Hifum7!bi;s?B%N?>S{ zCWY(tt$(i-Ps8nZ6H%ni!czU2tU0))-Wfz~D%9YV{c!m-G;~I}7~UZU*c*rjCvDF? z!$gnYFocmQf4-X-{*PgrN!H1k}-n@!W@J0K||F4$F3k@yurG#I7+p*zN`^@5Q zx4cF&=WztKA7v`OWK7 zc%4m?9ua`(CObJ{r7Ins1H_GFa%XOT+h&mZA@gAoH8{#~k;n01zKM>dJHJQza(x~3 z=}TD;N8|bOhSa6q7t1>%jXE)NRla$H9NkyeYkaK>Q(SP&3o9|cRBS4p5m=Ho)pQMgGUU&07 zf&p-(V5-J!p;cXNa(?wg`2}lKG_2T|+Q?EQBB!thVMO%fhR5%Ts?kr=v~lX4H*Tw! zUCGjNK&@<}^O&y&VQ8`#-eBc!%w3fIQd8EMoe;Jgz0F0ICYr6hr1D}dFXUlQjjwjt zjtMs;_5M$dS)Z#U@S?48qnxECFos*JvO9f=tjjlcO7~`HB%TRS{!cfGXcB@!v&(Dp z>G1x!9zqNGT*dGnBVp+^%3SKl@OeANNf9ClL{5OsX(&G{i?KP!g!r7hre05BqV$!V&thiS&JH4*{I^?$Zz|kxIRsgNYtH zZ6?rp@;A1+cW5*~cgk*aX8?-_7W(f1m8cOEE3ss!VlYBUJurD&XIep5j>O>wVyU=- zYWn01Fh6gH@sk>MATj5Tb$MLb?s5#Oe%OO!+{+vA${~Hz3rt8~@u$ofZs|?-4H~*Mf@bp$E&RyoE zQz!tw|BQqeC7n0TI;SovFun!_>xDer16e@ zKCga|77%6s=xbe+9`eJ>Nzvq|EcvC6a0Q-H>xy8$#|G^VV?P)6?aQS!0DM3r{!nxo zjpw5Ofjqr1gCbMZ(Ay{J%~4D|sdpt&oSpsQnpiHWxqIGPnA;^z*hjo(sOxtNJ!%pd z&-R=y+WL;pNMZO4&&|5hu1?9-`xD7X!t~3*hRN3H`xf!dvj(=XatfIU9zad#Rc|+^ zYPg15?ei6a*uj4(0zL=gZb~rstNTe=8~_=%=ne$f6L16+dYgC)AB-T5b_EEMCtJAb z)$tTAaqWC9hRt|AB22IRQ=amwY;F=b5O6jGr~|-Psl?xu+3;+}vXuSNJX5ywA%7+2 z8z%A;R2df9S!Y-}^}LMui!~J$Ymq$=ELUR)W5sBc(Gco64^~!&G~Y90P>O(443OX1 z+wbK<#D_T4wQTHwk{nHh1wJq=D(dgMXX%6kF_U(g`X_JK5FK`{NoJjI35P*5!OIV$ zU6Cv}VCUizKvhItsJ}=XP2GIF-pwVw57)5Q3dNZI*+JB(F)-qzc2~<9Dsk0@rzA!O zbiK`j4~0qEI+0wTji%IJ{^5^>U@DsU^J?NMl1wwTjmCG?4^M{F9W)wm7K_1OFppA? z(O4m`9{j2BBZ(?rLJJtsYrUfxD5I9N7XZDXyhO_=WQF83`YJ~@!xF!eEh%n~BSi@hfKfFqmzrdPx8F`M6b?*MovSTw9Alo1V7bPJO0+J-$>Jk9H>swG~% zm(>*|zYU~w38~0#uzOuq;*MI1`&SHtP-$GLS@7>hOdccQ)ii=Yc5SbJlMGlEyxp}f zaSDn@NQVvXpi+3q;>`s(zlwpy)AlkB{l8R@C0N4Dm{WI#%s78Fb;iE*m0yKelCCFx zJrz$`PA^fNT}*Z-*wHA8hz~)N&zfu{lK5oRPQ72WVCU;{qosVrU9WLu)^Dc+F>x%shw#hjy zqUMg2yTHsSGK~8^~^o73J``e~e6Itspyj!tNz`O;{M2TRq(?uMSj-9NWk`B0N-T2{V(Tl+f zwvl4Q)Yw!L^Sh5c62;HWHT>bP+Q}Foh_oPt4Co%T3r|Wdt!cz9*>O7Lm?B337~nOn zS}d~^U6uzwjxTm|zq_Q%h)uzv5-sa0 z%6Wl0!b%1jt*nExa&teT$7hW4!kC!~amuPHcAhM?B5z&hIHE4GFqruF67<@!tm9#+ z!)CZvW{l@z;S8iD$-ada z)R6Kg(VR{)w!bgJ*^!Z=`!0-2NVw}JN2mpPP?K~QznRHDth@eu!=~0Lap}T>z^#NA=r? z6)4SB{K3zJAy29{d;&*pI+X$=jyoZ}4d^%(FlSw;SrRJ3%)-x|@JuLHed$qhF{Pv$ zG++|~Bd+narzyI_CHAwGtnyJmmw8dF4#XPK$8!uRkUoGgP+a$L<#9wXlmc_DvN#6# ztUFB-qo7Gqis%(nU;-rR1&o>>uqyoBbe0=E;y-;;^W;v>TL<+A3dWg;R1yq9@@0kiH}dQdrAH7d-L;$H#7h}Ft{CTqx0O^`ax&yR7O@yWs2gJ(&mXuXaf}u1%XJ#H z!Y@oQc2Tt&+&G=Vx}M6apRE4@j4li{6%s(%+je>ON{)}CL6n}4kd5!6hRD({vPZ)RGvXtte$-C_dE)k*WP1GxbS$jC+%4g( zp$!EjVpg-vJBqo%O8}pn_1ygqdNMp` zejP$G0uOma+qzHapPC+#R!BFehK=4sbo~dgIik+>eS*1g6>TjN)g10MW+DlVBKDwF z#0{l5)61(`#5*AIT$+%#waS9^*vzW1vjR!#Lnv@Z)K*~xgaha^B@Nijx4(ft-n3V* zLNw9q+PfPSpeaUf-aG>}<=`YD)d|v$BThWVWN^VNrQ}9L(%smOFGS{d zRJ~P$z<&WZinM*f4_=VpQgF~J19}3U{8j2r>Q!51KlF~~rB}iTLIoai3onR+rZ5eq zZ^>8jb@YKjRG*LLRyDKXFBIMZNnME^KJIfi)nthxn9}p=e`5nu>N7!#NarL!&t_{Y z`5OkfUy$-a{j=y3uaf)4&kl357aFlhD}oY#ATC|g@rRNygVPX4gjFwh5$c2e9zmp4 zl89`Nh-IN~IsYPw#W!R;M2=O{Z4(m8z%Dlo(5W?8g8*L}j z1tg`%!b^1!z8obrdgI-hx=FlGS=5WoqEN127+S6v6>Yi852jZDz`>Mq{{b3zqb8k8 zh-ryOhv4)irP*TV*f)g0);9Pe#Xmn~_1YNqZSAga8Nn zbXd$8gsK{*P*+voHi8Emod1i(3qQ&V?GaA+xEqX%et7#?Qgo*7MyV%TCXc!gD8Lc} z^|aBMTtsM1K>%paztJ^B`TgLSJR>y+se#CNWM}PR5I=3=mS7(4=WnDMf{>r)OceL_)t2l%_v5hhMRUeD_&TWly%Sx) zY$V`$2uu0&WT;RQRXNgWl}epD2|-6$x2M=i7mQ9qya1y)SvS?cn*BP?sH1K9^txM>~iUnPFm+a>; zz*yri^v?L8{oqf{7K%WNgC9P`7ciZ`wzf3~5Gj4Z5sc&4Km^C*Y6tqBUj*~^L3Tmc z(1+W=n4TZNE2~4s)+VT3BMj7kx|8FL9`D`fy`RSOuG#`Q6Tmd@da>TuzLP0^Iv8e$&=9h|wx+`&Nb>CVQ`>j=Fcy$otp^{!T(@LbZoW81_Qx z{I#S*i#K=9{T6vF8$}t{w0*BGa+!KYyENyX8T-EJ#NRJZd*$&3ZH+ve?quHfZ)RiY zLA5z_31DBW(wAJN*ssL`j|Ox|yw+~l*+d2q)E2kzJ)h8XAt*E{CF}o0>2(kLyGXre zntFeuDW|M%38n1Np8ttlVC@B_FLG}n(rR!{!q1EQ*Q+BD#X4Mzl>UxCogjtR#c&FMEO=d)w_HmD`TovzEgXiS~}apFm5XoeP>A zQu1U*J;cRkClVAdv+v4e76~K_j8!K&(+4+J)n1k8_R#k)9wey&0E%Uk+>0h8)I&?@ z3zx}24x3xOyN>z>jzW%{dUy|T8u5QmNQnNI5tsXUgt}?Bwzu9RgoAa(A~dbe1m$v5 z0-;rx9RJFE{U6{L7&b@zl-mL3&YoR;=jtH_yVQCs4AFAi6+I|-EAl~Lk(qz&IyIvG zgb~n;tTO@4!zj)`ftp8eVx~e)MBd0f@y&b8?JCZ#IBng%gkIx9Jfk!Labg%dh62U( zhz?+Yd9HOkkhc4eYcPLOx_6EE&VV z7Ow5(4F68RS+unz*O&C4=Bs=?!Z;Q0cwA;#q-fIA*9<2i;cxNdetWrA#QEc+J(B^d zI-Ae&o@+_R_5Gg?t_$Wc~~U zU{DWybq26AEleQs+}l}a0>()R4b`PfUUKv&K+Y*-?e9Ep)B#r}!#3MsvOgWL62J$e z@MX7rbgdMIM*l^(di>EcP5P$l8JK_!7b6DY76#;oWPj|jI}AyJ=9SS=;7(0A)aPA4 zM2%MRFBdHn)YX|?W_2dcxxN%AvWydxS>h)o3Cp~04Py93w@j47!lnnY-2xJ(?DDh8 z+cp4-jXVkDtda=r`R*m3 zuV6Q>RtRq$|JW2pxCj}ZwBVqEF?tzcjh|9FNV@JIs0fjk>Cx11|6?Aidi?nDHlXIs zb7nuV(P4ECObIfOlpD)mOcyrHU52#M%_ugI5VeE=8pVNl;yPjThIQ%A*ASZ!OSCmX zBA-t)c`TXZJT21{2Om6MB@~;UTunjaac8k6Bj(OFL!p4(5o35^W8ERzPZfLy4@n4e z9`GYaVLmL9YMMQp=Do#?iUh1(x)9&b!jeBjeVZYroxAl1`$Sd0~-XpL+=THjzH#;huAXaM& z*ND%+S@JFw1sk!XHIabAzjB~bAb^3FAuJ!6NLj>hdP`posCjEKCgqyu10yF1K(P|k zx%BwE%?;`fB6tFD0}fSLary@i@ zF;8{ZeodJXUlGdCkQI~kMgOiUE`e3cz3mLDL{MeYrhfd)Vh2{9NE1*Uyh-49LT*`h z!Y}c`b!fasAhc@$^2)OBZLwNI;@>QSO&Mxc`upPyfQ}5=qJr z$P~5GUoo&l^F`gDzvuz-+C(&XerL#CA&&7i4S$&%)vTCBW?V#4FEjW$FNyP2|K+|Q znbS&+rE|O$zf%DU3I4Mq+~39ZKqGoo>b=JpbaK~L`G}wu(iOFf6vrGagOFugxbZIm zBEGlh=nUd9t6uml#0b;0)Q`0^ttqY(5CU2erqT4?GX~W~s^0NaCWgQ{ESiD2T&2+B zu+Ye7m3wJ~%0r~bTJkvvkpO@thkY|ClET6?hZGNzf^sM73Lt2JGZv6d3(zd+rO5pg*)ZxKX*$f)QjLZBit+C>zO zMT*NmnsP~rrjIhj@D|Uk{s2dH13|5vKGeTds5nXS@?`e3EyXS z;*T&EOaaeMbu?$pH1>#9U;x-x&(3mVd=TV6z{%bC(#cdzR-WeKgB}g{#ZOKI#3rgCxrb!8GISXbXu{40jkcZQjBu){NT}s7$ zezi@}mznr)u2JAb%pD2D;*A4k9)loSNlVu`u9>pns_g+HJl0)&SO(R)p4f`1RLSs2@qMq za~vcdsW!!*XJTcS0W)uzqZUDsdrNq5>>tILG)?pk0zgBH@H%540M)i##1M+;4gKw0 zCt-5jkSM&LcIXc!Me`jS8v!g!e(ZZ5cWlGCUyrvFzkVp~NIjn2bjr*v)9S(IV4qj_ z7sBn1l`iUq=p+CLo^qJIaG+^wFF~c#)`aka`nl`=QpI%53~)4e%9(GeNA+>sSaCDa zt&6wjmv?@y`J(ek+qGw)P9Q2?#qN;-?zBjl!m%z*V3=7vao1bc!F+Dj*RTvq@@V=V z-~l;*B}|OV$R2Z=Lq0+N1d{+6AlrU;Ec}&=hd!{1)Jb8Io=oxWQop$x)jk(J7QhmY zdLvu#bfo}jcydBa_Rq=1NE(pn>MPMqm3*EFVr~T^YQ}q&7g;F|?Mt3;u%A+p6EMfn z9gZ5+m=S(l3hczlfkh~mKf@M`2;Wj+)P7k?5|l?cl*X12)zsIMkUt?i5*TaRFs|r# zx$fR(Y-a#n|AdtQ$0rY5S)U@fEo;193}!ey#81`$NIxdA$$$Un>#A74sNN#oto`fc zalv~^V~ETfJ#r$M$l&}N(hQ&lF(uBjG3^Yu>ZA^PlSXHF9>b$g?BXJO-Fwe5V{D(i z>Yv?njMI_)1fzLao=9&ep|5IiTsr6gK@s@07(tezfwOS?jTIsa|4t3tn&O%5sz0Fk z!*&%_Ypsccn68v7YsdOag!~`M7Tyg1ND^=pKydQo0T~Ctmw|niWHOrI%xlnwu zJ#3Jp%ygX!NttTtNecc=17wif(o!^!NONmG@U{TNeO|Q^n1yUpH>Zt{E-Mt9o%+H| zw>pR9yrk#l3U*oii0km~a^BIEa}f9F^xw+*40>H)Zo*!nv7*V9XXAQVYrL>5K}zUh)2#$uiGIXYFWhFjZkKQ3>Z5XQTI7@kIQoDR(hoE zD@|ZD2`m^RA&J=dMw=xwi~FH?3!2YG+V@`*Ml355fimskaia=ZP(|RmUj9aMF!nct zTCOw@;(%d}%`*BOlbl6x85rUXu#W1#9B#eu4( zY2hS!gzZ*(BO`O%2>J=`JB7?l1iZciwy0o0XEpVZ^>cPm4natr+0L;533~=^38@ZT zG=^wm88ALxj^EG0O0Lp#+QpEiMSi&CH|D99X{AcRuc}26W!JKKY6q6*#1Bxb{r~`7 zA`vit;S4tqlHYe$MR`>Mw@1mCvHGi*7K

ca)3q6EG)=Jto;e7KuhE2jscJ*^d)6B#yiszUQXF>~rsd*t zmpot)mogBW<3DF*Z^OG;rNUaM@6Yc|!DWb&)B{OBr$gxathyMqgpchY+N-WZK(>a` zS%ni~`0vMmvx=t0G0$!6if49FxgYHqgr`+|mK9a0@^>r>a>mFRHvSylE{aj3O;!!> zTy+2IKjK|;OMorGFBViZH@|?7znc3@#ZI%t{$Dri87x0D`QjPbzfL;e0w_u9TPLpC zvkTl`3dWP4C|ARxCE~|TQ&w*;oj&M;1P|o&baW*hBXJ~FguO~S4}9DWNO-5pw}pCQ z=ec+r50g-E#|>iuQ(Ph@tw=C)T)j}y1tz3_SW*IvxxjjP5O7^YW7_WS8{9yfU~CO~ ze3Q)a@-^(V)0zPwTRgp!{2bDM*Lv&GjIv;p`I}8m8)2>043$Sut$hV}i;|(k0|0zg ze~)sE92@?{Wi)&$`trp)Lt67rh zwb|DhjRrNuvq61arR1LgLZff}DB>I3cOG8c4&0PW2=dRp;+*I31SmoKj^uqM&oSjS zTyKfDBr)Rww#`5!R8{fF|$eOGZ@19!>fmkNigKbyMlMgL+zWG7xY zF!2@h-RSnY*onKd*gOg>d|UUX@fX#kjFGvsZO)Td)hxXfdbd_01Q~tdYS~(G&$f_H zNayJs8oiDrl>6> zcVHQGCY_E)^85D;_NHazC(QsJs?l5;?lj8BShwSdjPxnde#AKS%c}i=>{J8Z-#@2= zvCL1~w0io6TCkw>g|}M$8#&1JxRekW`f*6uI>?Q@B%%mGcut%{gT$=b4v7#di7k^f z%9bNJ6zDUDiCjftB{8Obao@(V$n5UZNl|0%K}53ER&=>8bc_Wm*tL0yrRsTi!rCRB zX5Le*-)L4v`525U4FuX^pNx(mpTPmrgQRSl%TC`EW+oDz5#nX;a;$b^z7q2ejj+Rs zD2a_L<_EIc#bPG zqwEpM*e(VF#x2=~yVMzqiI%9G8}7u$fmgI?FpokwlH}L?qR`|bfM&6s9NCkY6+VRi z)F_ut5Pzjt*L_0Tl-#xWn`mBM!so|gS{zZ~BnR4A`Mx!Tza@U$9Ad=^IU{==$J6bM1(6er- z5)yr{o>h2V2}+>07H5sZC+t2v)WO9(Szdf$>S&3H(T1!6H!9T=9HCmpS4SdAKtATH zytGkChx_mP2%}0Mt#ms;_8rkcdfNd@Y>Fj+OYUu5iKrKv`hBfvPYI?6A6)Mr6{YT-K-J2o~m(Doib7ViVg$ zVM(HT`rgk;=XKp<^_|wU1Iuj=Y0mxhAc`f-C$1WPJ$>o7!lqj<{aV-azL~c38|IwL*XRhoEvp$>#yn>q`KjGFQaw1fcjq(6OjmKqh=$p8Xq6+2X;bQv&q3bh1I#u!nDoLCu;kWSL3f+^u2I9q* z7tMh3imx>vvs!a!+jvSU+V^wFJ+^liic<5lpTHQa=L;B`iV2v*zU3C%nHRSo#My%{+;E0*5-Q^_H_w9>vG0a#5`d7Ro5xyOO_y-)P7;$3*z@;`|#Hr z=fno%H^YW<%Pr?K5dr9$mtnjXnde8X-%wP|1Hzvtp;Z) z1^Z(DGY4@ZUnA2a1;yb^9cdn0F<&4@$**sHnXod$VDSc`*&MLz3Vj$p#peGB`ARe$pK`pQ2G+gU!uRYZfs4`$jlb*16j3>87yy< zSzb|2m{tALAN9;YYM9So1C-Flxg}2N(b(?um;8mw-?q2tgMGWXs^}7Fb6u z&;$m_3x$bKf4^p{t&tyBVuaEv!BczpHU20j<*oWV1KB8xtn+(!`w~AJM$~851}Isd zO)1$1Mm^B7cxn3$~Vd#`ppW)6CxFwE&@EqyF%5N||u z#rkaZw=pjE?>U~BIJ7b{_87n%NO*~QVHfZglxGGq4(8}5TmJP>Vn_wiSLGA z0+Vb<5osGSaZC8KA3C4;4ljpmg3!|U-c1SfChoCIS^`^6R;ThsVE??^GuJuus0gx* z%yNV_)_mRD3xdV_M%XG06r(0>MVP{!%K;Wr#Cubyh5{B_!!~s6de^f96KYaZ$hMvW z);ccwY(olZ2$1X8OZv=P{g(k+tdtTvwIQ|nj`Qglq zW%Ed>0-B{yFfX)8eWu&AjQ31dxY4^O6{o!uB87`m`?+T>qbS`bs6Y3C>F>zkThW^9 z)PLbCl$8r+)B*#~i{sp8&JR%{htW^LF4|Ah6(2-k;4iUN$^ERe5UE)uqc-_DeT zs*uqU(5#-eeR_(cA-WqyG(@ft{Al|sh!3CY)O7zLhm` zJRp$5uO0ncOAy+Xf0lL_Lz7c3INE=)ZtXiYdU+rLN?hI1h)y1}VW@ktywYj}Pr7Ss zlzYPDw&U_S!!^LW#bx{sV&A*=(ap!EqNJaRWU2S16Gu>!@`0u3<#@Zi8(+hwU1mUe z|GQCwI@#t_KDVA4m$MaRf~t?&OLg?+1sT#$_dQYw!asjFMT}Lz-8V+(?;~@6HMz8A zru?>1_DzqG4af@Ng!mOwGoO1~EBw2RP>c_x{a1LY7e&2UbId-$Qpcfxbdm}rq7IR7 zdyBO2*kMPSq|`U;U)%&GWK(@Kn3i$YVQXrGbX2g*1@pa-R&C^e-vZdba^d+qWv5>1 zR5_c!vTFS|z*|D%dqiZgjymbGmvrRKjsg5=DFROMY26Y$lYi>k9 z&+$tdWv!LKZ7D*X$)1UlQB=sZE>6ZRAi@_;eLNKW7j)r$RVdhky?s`)>fBjt&D(G( zQO72Ii#rH32Y-^k;a$(+Rv8phy5=ODnCk|3d==@Uel=Nh);8NM^6Fv8@V&1tpEGUS^&N}_G(*pMDFxx97vhF@{>!Jm zbCwVDx*!S4{Vz296*lW~GC6i4tPUF#|9Q~B0MhMxKcExkXT68wD$Tw1id;xPiLcFh z=f}G}Zqd$A!t)!KPFfDOl^D%LnHpQ{zfLqRH}Pv?juS)AHiWqHM(MgAR;s3@8% znx1;57yJ$5Cz)PZY8IC%M@AW1h7*T#aM3tz>20>7G46+zWPHhvjc5ocwasiomxODi z9*yzNP5pL|^k&NKKLGcOMIH^I7qa_KF#7Qy${ti20d$TD_D{^Pwu+(k0OQ{V`jIY# zYNAzb(^sqAc^{FKvNtu4ta=O7{kCugI$lJD^9QiZ2PMySI~ejps2=`nv|bEo`B5Wy zB#A=p=TTH%WjVZ&x_lK8CRCteuK`pXax{%*^QteUY z-<=%vi|71R1(Ee}Y8u9QeHw%|Fa3s#8@Ov<>Mzl#wVR=5mR|;u^RdYrH-Gbp28S$u z#VXA2;bFzz%b4B=8pwRyZm$TxbB*1&2u14w--r}?Q;7|DW<^^|0DaYhaZG~m8GqW` zKLn}$F-sp<2&v!J0gg94i1Q{6t)5JGWV1vGNi?9E1fAaZ+I^X-ZHo^fG&4rw+w zSI`>={LGlLO#BK{UO^G^EDyX(Ii{G!_0edASHOcxDNT4f*>S9eS=e7@k|@QswVcX5F@wSJfuEhvrjE==5wq zK_z7*nil^MCzsP3FMEuT_j866=S_*J$?VO^H(Dk)PP~x$DFNB6Fr#{wM{bfbw9KrD z5yKaJPl+n<-st_DSskHr6Q8#p9UOd)7q_VY7}pCL$TeZrlS^}3FYw>E7jHJ|e16ux z+AKvr_2t8l9dIZS_K>nD2^BmsihUT?tK>Dk_%lG_^QTY6g`@fO|72kAJD3*Q9_i8Q zV_%$@NVS${7TjF+pu13y(ww-h*~vqiV;~`=RT|y{b2bAaDGh(Sd|3)zmd-obb~$BR z29$GK0?H~&`82E-v)ssS8K_XYRfJ^jpYEUatw=RlRF_%z0I?3kt^Fzldu^gZ10$hL zaSDqZ;{+7hp`C7^7uVh%UuaYN*{b6Xgb0?1y;}N`|2;2LUiNJIM_EtJ4K{DPW*iS_ zRmu_MpRR`?Nc(Trjzad%M-pD#>u!Yt%_2Tx#UEYRl>jyFSV>C24B^^47Yz{$zv$|ca=w$*5}h2hbOJbS-W^u#ujx6#0(T=(j?|*<0M+R&7pIo06X5A-O-AEg%Iq~X>rIgPVQNE3- zpmRUk-}~!dSu`{!$-@~+F0LiLnmB&kt4^}`L%gy9lOkGwe=_*%lcP4X`TKh?WWqt% zsBVfV^oc+EmwZ(2=HBY9eB#4HlkJmW{14OJtQbgfQo_f86f4TT_Xm^>A)>eAdu}PFU>0 ziUq3PuqkhOk$_1a?1l9CX8 z$X}`l<6}XdA>GzJEpNRG=CWF}65}Z&B66tpqpq*5s`1^_{K+dyuAl$)Lo3;~HMoU7 zlpg#F>_8rJ$u1B53S|@#;@)g~AEXs7N>x_fe!y?d zoSM(P9MM~=POu=52ffeGsDJU|Of*lG%WXsbcKVCW%^h--Hu6_Wrv>@6@1+OrW1-#U zdO_utfr@qosWV!DmyPDNDwyg4oxIZUg&!mtp!l~)ltb!gXxM?p1_Rf^u566(bXS50 z46fdge19_OOS@eFTlm@jN`*#SVV_Zd;UW&b{VRm#w z;Lg(gv6tYKpX=yXm*3UQ!08v)%iRbz|L>RIqj0r^k|ARd)J=!Hfc%eXSx3^6tzx2H zz^^yzHHFT<;y~F6v1t5JQq#9~_R)|O9Dun+;9&lV^n>y^lEXL&T;4=lVU=5Y*BIXj zya1KnwA$Vm@QAgr!xN)Nw0V0*mjG!9JN%VQE3e8LCYw(TKb&ehYvT;rrIfTC;eU$t zP*V=WuShj?Xon4Q_*%Ch`gLZcG@DWhEIq@1vBVpy==(g*>7jaJ}H81eY?o(G&V9myv??`xye_Zdp~T4V8GiH>!I2tlV9yu|;zUW{D;c32^TNvO@L zM>x%L-SMBsha8_GDcB?&&{A$*t;dV#Nx(@+kgkl1Y@b4j+{23f6Q<4L8Dn!8KY!qJ zce%kt^;7QWEkPIEH5)b5mi7^ znp*Doudb=s#5oI8ox2@FC4#s-=I2(RL! zK99>D!ImeiHVR$KM1viR;VZN9AF4~kzSqKyf|eIsNe>l$vsy%i8~uFppQFPz+OHQM z?cyRL#MNLK1b@E$wnLm0{-bDVxFlm0IZd_;IPoPv(0Yf<2LNQtwvn=yvCLyK?fered%?)pN?sGM3QmFum=e-OpOooKgsDX z1YnR{rdm^n72V%Y4A$30r0Eap!M~aluDz742n(IlIA*T~!syl(y!ok8bJbWDHdb9KCAm12Ct22Yp0f7)kD0=bM*Vn zpG@n$Q~GF>Ge*bseyo{;R92@!XH%RCrKHF!`ldJ#e{X|T+mA~g7w=ds{sHehl9YLnH+NJhmx}3= zCJ~JXS`mYJ@U{1f*LID;kL04(GcH%^-qHTJOV@u>Us{M?u{m+zpf)WI%$@9n$k&V*rGikqC05~9A+3@)hp z$BT8g?y~6MMAP|6?$YT9rqQsEw}1&3Z5H;_|F>_tH76q z${d@H*DaJioCGAD0IkXj-nAmnQ{k%hLN;6@A=Qe0gzERlLcC9GPGSMMREF&NBoS1j zgQvN#=h0i|EjgQ8kF}bNcU&cY0TzUe}E%l zMZ6qyLJ8(!TCg%oHGz|=^_TIh$UiN|^8EMrrVb2tg0q+dk=d+8jNaVar$I+aRCMv3 z3J-^HnQTL^9<0#uc=6P@zTBaC|1xavByn!c$8`RXpBwd@zd6X>v{j|i{$cqTO`hjz zrW;rzeG7f+en)gkp?vHmUsS&%^6(vW{P-^ggUJ#RK{)gLEp2I9A6RXGLqd?V?&DzZ zax?*!5I%6>`{O~3)z0I*PP;?}p|`K3--*Fm{XHCBA*nsUCVtwVl$(Ro-#PT6zCiaC zYu{UkXff2}V)lL#=XaHmE({Dc3%y0AcrVs`FOZ7+5KjO|W6JQTl4tr;_I)#!ck)cO zU73Xg@+;^%Xh%tn$8NmPV9I_|9C#Q>Bv}cXd^93N0n#TLS`hD|@QJfYZWeR)az9gf zW(tF6W@-d$XBiNdCb-!yO!&KplLG^S2D65&=cRFJ`#({gC3(ml zXSg9fI|mdwnUD&$-W$1S3dN&^kLnFQzOLrczpJH8Rr>x^B;`#1U==SO%puf4JI}ua z82j^jP8go^DAfh0y?>&qXtd{fv0&iT_kRI+K!?AE{EgRnu|ZlMdEl5j z8l4P+0SbGcIGIo!X{-gnHMR(zv8LjzK0NMWS@bmZtcf1Q@?)rluHSnM zP{1VDx84=$Dnu;&nX(g7lz}#F%1|5~ItI*=1-kR20*Yf4^ZET_Ge`r|m<=f2sPg!p z@uQ`re=}}C6fcWuJ^VQ#1I~z=_S~~jYe4LB76u+|d4D-Z!KJ!8t>OWYHtns%J$1Tn zTmn?7?0xv|Xt)4&z@85|s7Ca8PRvjM8eVky#*}uPA?sKUox?+pz2Ms$fCiml0#nI< zyN3>;rptP`q}r;XN)KUtOto9B=#h5X(*t4eb~mP+bXWme*34p2dfXj(rfQmIjT*tw ze2qg8DpLBp#L}xsnr1wB7}Z%%OxL!Teln)d@l0Q0U3Ch0ZZpspw7xrWYYl@^vAMem zZxdX}n$)Yao6138*G_e;fbv-MzyKr)hPcdMD6r#F_cWWG5RW{sa9VrYe#@F2J#b=| zdQUR|5(}qvkB0d$PHQ<}j%eOrDAV}$mx$j?*Unj^Q8(S>3*{0&TgF0LO*$XE4Ip`V z^5>AoPNXig#M992!Nj6>x^s_DH?2$xKtHiNh0+Ei(XWp<1k(;kVCmaeZn01RJn#20 zHUTsTO!I)J28GR3Y2MqiN%wIPk_~$?O&6Zuj2zgWPCUeLfgi)BZ6oAg_+pwh#E+lv zj2lC(yJ`I502Q4a{oG7=>#@%UB0P5F9qRYhnd*02p1mvk~f_hr`ZG!=Kce0jwLwr2e z1~wEL{#^3l-{|_l2w7XZE0#a>22tT=4t?X3eQZylz}hNs_557y zf-9n$_cd~~ThfQ({23CC%Lu6)`N2)STkCrqaJAL9PbOid_R}rI>UCweExR6Wi zJ7;xp0-}~NHjziTzaRu?(JOIU8Q`h5&U(PKl_$rQILz9#WN8OXcc#EA!TT^?3seZ| z4!rXiEgFjOtzgKN%9Pv82qV3Rr_ONRQKWn|>l(TtXw&X_-a#*uvGalC0-)FBJZm1=_!?bPn6IelFiWaY9mU6G^$e04CrM zhmTpbmcR^euS0{{2%S8Gmzb%tN0$YurxF$5*N}dDn}LKafm~8wBf(q*+eiVqjRQkP zO*7+r$6*mtMcnIg^No=!wVylt#)eQ==Uf8-FOi~H20WSpAV;^Y$8o4FFj59qAp-q_2>_&&~o8MGv$M*AW|NSh3fT)!t|eslV!L z84L1&i+#6wG!hMxbFNoSQ5RcdM0t8WzcI=rStyqLe0*nPMIf4dWh1Sdgnmr)60+^x z*z#nR@YUGfVlDMS7pHDhThRDX=Z}Xe4qrM;udHEZODv1wngLB00B@O_2c@>Vsri}U zAe`E-oV3@J^zW13ta9~Ly2d0h_F1&~@tZhpk4XOjMq{Z8 zaXU`nEn2D`gWhUR^uGT9IF%!Aq0u?tIF5>#oxA>+mSUwN)A z)(0kPhOwe81if)83J7@F?;#6k1<3S`4^QJLu7JGqdxKVVD@x(6uL|k$FN_k-DeFkU%T$z;Ua@zG(&d{tHmCed7=PD>F?aegS*kC z9}lduC>1?nQUvL&px=XjF+*q37R?xo)uzIJZadp;$4B7E0uPEg6|{c77>YZ#F1=Zs zl!1DDr{@J6L&Iz5c!rDAmd5qOfIz+u<=Us`UU6s?aqA*eNRF6b6kym(8H5BJJJSqV zxNIxSl_?<@8XxG6Ak=?ZP(I zSqNs8RoI)^aV9p>9uq^n5Q^Fk;L#%}7_I^&4H|1rL-FSk3N+aGer_d1KE3$FP(Va> z{;)WF5BJ1;0-DsVm^TJw3>4J`>dTr;(^mH%2SXHllrSk0`oyikso#CUDIp#2t~{rk z&|o0cmi4(najV~)poYt^9@^&+i{ff{`^Z2IiY@7Jgc8}ia20Meg*CiT0&91{{`djh zj}UO(MR#8>SZ{?#dT(RgFje_gmRS)_cRblQPy<{&@1>Pe8zc63C0fZlh>y@NE zPk?X{MZ@OeFrY6`=6a-ToyN@>U-i70}!mP0R23#_0UdCi0aiXnId~5~E`f{bI!? zjg+l5iE29`#7n)he^0e~I4Z z03DP;yftuDIs}_#Ve3}{g)60j(Kj~bMaaOmA5Cb*6GuRZzDMArL8<5#4(*eI9?}xLvN{o^VMiM_;49@xY>@? z{&3Y<*HHL>4k{O0k*L;0C`u~wYYGwov$m4Vj6ktP@G)GF>MVh$#`2XSgdBRy0Z8|w zdZVlYTUZAV&(4fSV5mXo#PO2q2o3Y=PrPFP0J4XCGXqMIl%VX?z?29fvv@bqe1Lb2 z&4{j|b&f1iv{#$@VF>Y55W4(eSD5+d3JD3v;z#q87O3>RHBR6aN(TEE+{0~iUvY(kUk8iU zVnhqNhgZur_`vxw7U}LYBDPDhPt*PK-KaH{*O%TCAVk%N@pfiDexP4RFmk zoCtL(^x%^z!A0672Xq;M4|@bsso@Q+AQ7F|i<%Y4!WLDV?Fid~|L= zLLtL>EKu-mA>TQ&uV#&^8u64IjZB;Kt>Uy(XusXVvIz;(!}R1oZ1G2rL*og+8fuF8 z*W}ri0U)Ds^tcv+>b@>&hh2Q0KRE4ffd2qN&0tXwy4&v)syozO<}px*^vad9noCl(;%Rs*1}jv*Nf!`G~0 z9#kE>KJXQG?5}&bzZh&f6%g&^#@jBoFhE$PP`4WRp& zh%N2-^^cnC7nay*Zo@a~zZ$N*#F;%YDq}G=eh@N~tznq9$bkEL9yF;VH(&9&gCiLyz@j_mp ztl7c99aOOPGmcJ?zdrGzuc*BP0SE-EZ=}cN8St~eIlZ+;inyCV_!8O8WnG2Ru120w z6}#3TgJ>P<%z+ofcIp0dk;FS5(%h?5Bs913Ia0M>X;Z+bbxpbVx$a_ zPcwM-!^+LsGSNF7>Nf!8WsKU)cVS(2KfH;A4#3vtN^N)v4HpFjj<>abN5&Ca5L1{Q z3%-XztV1djcU#W_563PWGFU85dB&Ap8ay6v?-U)uIO z#ozYHD|gBBaIb-_jqSogbn5!Q80duT<=(4?#uaex3=fm~2{AksSic_{m=zMOPhPT` zoptaYXLF{1_|=QPuR_sl>A_RW_apb9olIH-eDrPcv8 zaMq?Rq2XTBSmZq`?|D#t5!Z5oHg9u-+(3XS>yvxNo=Z;^!^H1_t3Ozd>E4FS*PE%{ zfvw<*=}Lw6)$41Nf=Nt4;*?Lv6u(S11YyT_4#~n0K}dj@sbXvq<~|r z?t|MsvLYHO=sC6>*Wj7G{>6y;&Re4un1D53FUA8)Mp!@9%`2tvuZy0?sa@v8U^yd{ z(F*CFaxTX2uHNK<>0THBfqF~o?+e-Ba(l$=d3NomILZMonm6c@WU5hoc^r|1#_F^X z)!4bXi$4v@PG*CrL1cdeX>rg9bsgPq%ok#nhi9nb&0BUKls+50knNq&6Tvr?fONIo zj*nenDp}H~7=bg=1D)?Xi&iwIaXQ!L;?zM!t$O*U0YM1&Pv_pT8fm8CZhp&##fsE; zj(n2Yi&HWyOM5j*xm1ZPU0|>-FaK?_ZIN3_o?Ggyy&WJ)|P{e$))aa zw@u0*g{9DW8qGkEu7dqDi>xG-^MbXm(11QSGZlM&!yKhnUT>i%qKb$;}{7Lt@jM~w9sA}euT35^<^Ey~y zGgXo(G1S(%yP82>Lu))@mIq3`YrI=6wB1xO*tJeFJ$K_bfI<^`J95zED(l=}CWm^3 z-8MVGBT8#aTyMQ%jRPJ~!*Y?-ls)7e3RRt1!>wOk4~|=ow`BvBnWKp|tAO@E*=Qrr zqT&F&A1kWj9VuY#H6O-ZAPBD`^u$+jZ6>a;#Y%GdZ?9$+D%u{jT)L>EKm_*8EEa+g zvS>%m$Si#)-+S*DK-756wE4vgZSzyUt~;dAzPlerO)(;+2GTllYXv*PcgL*U5L-1& zM&SbMtn;sUB-!cu-c4mi*0pb&i4z~xS*!w_%Uu2C#Alc^6nBt3743KR+*DdrmZqQK zGvIvu&K6-vY)um1c~UeM8iDimirKgCx93;{DinMD{a_VQ2jZu(peKsVV3Tw@_%mcGy3>lZyT2K>)i24cSzu{icc^|`Rfz3t9q7#gr4!I^oF3AK zRRDaRQyLJUzMoCTY3K_R!wr%i!w+9sWrFb!DcxgFxP?p(B@?A7`tDH<6h)Pn3lE9b z^ER9pkLkZylLj_?zFZo7mQ97zf;@zG+J3SFr-SnASOIpnU)L{isMX$!tsldcnjM+} z`*2k{4SD^U9R%Fp``>t$GzHV%o^EPTqv6R-?&#KO(bqI99qG!UrK*~a{{W1LYr}I` zrqIg$pI-1mNm41;$6?m$=%K-Yrg+6m`Sia7vU^)}{-fgbhPch(;Q zf|Z}Y))?3X(pC43)29UShqGGuyL-F?r%AVPn-#&%`*UW4n_jKM2h`W&EOfN=Oe3I+ zd5Eeo=bC3vS6k^{7{pISdU(x%Y7N_}=HMtaFQdWez)CjK!e4GawOX{)YSe3d(i$3t~)ZFX)$G#9F~vVh|u^x5z6 zg2GfANr)?aEyApsxMqlN)aS+pU*J&tFd9kXU+;lKujLtwT_-$~uio&OEe%rH@a1fP zH(b~tER5ATn=nsU+Y@(hkeCAET{u{?){iCu)%rSq@=XWa^>B!y*{?aI>i5~aD}E7v zC!CJK0J*l{-n|&s&C&O3?>+olxw@YxtL&(OSd`=FOs{n_k%-6 zk@38*NXawI-U;jTw!t*|}dXMq~rXuWyVIf@q1sqBKA-` z^ZU7UJ?tpxGeV}&I*#aWa}#2r#kHfWaK#=A@RYsxgGS!y@u>?S;7%C`$gYx1VGa%Y z4}ioZzQO14_?g-k83>Q32(XB10Q$`e)Pig^zWdDxfOa~0h_T%4M?CKtjiBu$+p_q< z>lHoEI@^xX$n-$*r{fKsc)X9dG&>{Ja+l)cTcj)CK6~6$l)cXTh0VOg-GIewyaHVs z*xi$w%@G60G`-uEIS&h=vjlS0+~%{s^n5F`SJe% ztS*S@Hw4$?1Qco`;&|f)X!13&^!HfVjSz?IpB}KpAkvpd!Er^$@C?3Nvfz+smGp-A=Cegxk>wA2`WGB%Jp1HYF#4 z;Q4aGG>^vToTbbsM!$YBfVWPR*NySVLJ#mBgY$`uw*wP?-#gC7WyXh|u@UeoOC^NI zPJ?HXt;bd(RcmhSF)Afjfn4~IDF%V@jnXPT(NC!+RJ+#Oy7jK$tt#tsgp}8;U2AnU zKOb+-M__F`&p~nlXu-xle|U}FgS5|E%M{wUL3?-8c?jC=H%I=n*xS3lIARTPuLRbF z!UPGsZk0CcEJS-+Qd8)}(PTT;bhNu&FTGqMSXHOa3aX`Zx5cw6ejpl7`;gL|?BP6X zC{n$9lg4Zvp1YI;aZ*1iln{k*kdpTr86n{&pW^_fL9^7K#l+T*4dLM)Vr*g-ucYIL znJ9;!fPG?Bslfb<9%QEUYh9lHOiZ-i4}e@4QvkZ(_BiN5+q&o7!~foBB0&8?m^O-G&9al zB^9g7gb2|M@IG?5w2VTZUUPB-#d-UgLI9As%!hyluFPV10mQCFEpj`rKa(iYK2OKa zBC@oGhr@z}_ULy`Krb(e1AEPd+fRDP0vHX1te8tsX?%Y2x&cF!^ZB^JP5yVSYT(cX z*(3RIOHI>f!E|dB+H%s5dCZigGVE&a%*IG_z8&r%uNK#trO+v}5dht7A}@=qL=TSN zM^0iad6Qme#zh%f>jPNc8>{~ z&`tG;_mGNqJozuiBhm=CRcYzfck}BNrc2*#esDIr7@x%)3SPIM?kI`e4LmSB2XU$E z=LTLMKIh}c2y=Uz?$ZizqRXkP>()AK(|Txof-_^+u5W1ae-b#_ETiM&Hay}vt;0=U zobS(E2sqbqy@TPvCam-9&j2p*ho7+@qY5bP2)e+E&S;YXk_b9!gJm*oG#QJTL^KTVPO-2dVEM zTVKB$`o=AF6Mg;Ttbl$nv;zfqa4wp?l3_}NVZu0}G}~5pG#hT^)&QW;S6ajrwJqcM z@s!r`C#-F)yYH`81TCcn%e570^m@(gZ?x_+VW*x&?y*6Q06NN5QVaIh2oZ=`Oiv+3 z51bI!Ab%9dqN?@HiWl7Ri~#DpzGtU6c&%r~LZA;fS0E|U@Mh1vfd@Y(9BV}Io5x`E zgVuGTZw}PNAdw%7tdXhp53PJ;Y8D|vUmKW8oxV#+_p5M#07z{LQ}o2_?V7)Q}+QA?fJxiYj5y57K+Z8K8{N@5iQUrk?0O0-kq_gen1sD%mX{ zi@l!iGcZO%ru@E47IB{Woz~MGvWUK@P#>;hqR52Z^ui(~1IxsoHIW4%Aif{Xrlw7s z1ja#Tub+9K6hN~`J6&;Lb`%sl?;+QqYP~x7%|et(n%K$s$Ze!Nlug`5B#HS(<{g5I zste70;M94tdYFxMnm+`>p}R!eU7Y3;#fZBo9~V6uKwoZ``rI0{fjXLd9$a*(U;{0E z@%?Kc2etUTCb^DT z6W(pORU=KAfu_^Y*rtebX2B02YVJhN3WF2Egs|3W8U!fmbMcgu7Rz=MMm7+D2F}6w zq`_UAo>>gd&LXyZL8aNF-;*`R(s40O~cOIOVZOMKAsQ;dY>l?a+F- zC7_SF{FD58R@E&Hlo(rNo zZ65sk$%+azU*9%@K#kuVN;syYVj6ebhTzq-)IvD+ZB&vMbNI-)JExs_9Hr#~HM0sO zsbCZ12e2eAxEovnw^gpx9DHF0QtQ$2lX6#+)cD14C5wNUxHqC`3YB-pC@U!ghebYI z+uNiTUH;7Gq%B$n!DD9C*$JFNA!Do=uc5z!I7n5DRuTL6j@_={1fDCd+*C&whF@VT zs(!IoWff2HF>k#sPWji~S|~R7jl>pe?eIL84#z21yx!ciE~PI;-?J?fc@EL=$b>;O z^KwFB2mmmRPn{YJCg)P?NhgVorRu1boqn*@8mfJSQx!l`DSX5BW6lTRxw03LM)+_% zlEB@K-m#10KCkNG?5YASiQ@k7v!zC`Dr$orIqwPXbUX`_Is%KW;-vsL%^GyL6(DVS zFBa#ZH#)}8_HpTT29^9UIF}R_+so~m!3nUh(en zA+qUWP7Wgu$Dlmx=guoZt@x%EJiBA9;37neJM#F8kyLlS_8Qc{2G6axDB)J8tOcgD zw_iW3H#fqYrnOI4yY`?ajYa#$C5MBu`Zb7_$5Jqd1`xqrI;XsnlF*(vgwP_buf$B4 z*Jzy)yWPd8uIwcKxjkUD3`U30Z=V)qJuA1T&oI!poy|X3C?M1O;3}XUVZgx`jY~N_ zxE!${j<38!+!S5^0B0w@Ye~@@fubJ`;l67vfGaC~O@8qKCbcNF(8J!nJo!!=qlIiJrUEoj!lDzAe-~qNn7TMjfkK+_-*-Ocg zRTEV!FhoRpE7QgWl8Dx+fl0bboVcL&J!fIH1U$#oOUCN|09-U&ogXy#&2EjJJl{Bi z6iwfF4~$U*$(mx2AQz!CPhlbbV%S9Vc=);FYLVE;J#aV|mGi9Fl_fdB!g$l5Z}8^Y z6c&!|56FjQ1fHwSo(>sP1a7=T@BTddQGO^SaeKtJCEje~QtY}N*YMm`CWs9kOy zMDIj0kCY0%o-oiH1NW1*p4S33-EAf^Z@vwgJdxH02|_4FmweVLE%tc#%r~2M4_|+r zYRG+y-59Gv4-RNleKy(LuuTSpjwtdxVYS>H&I)x+WZ6Z;^19Btq4+;DACpg}Z&44# z)@afu{{RV*9=%=nFMNUB$6PYg)8TG&>i+C?v;ZelMQ<)a1fwFuo5f(O4m}r9Pps=? z0fwa7Tv!mL2o&cH>CFl;phcz9b8a*TZ1_FT@0xT)ZiJscdktK<9I`8$y+A8cN7qmrTz0` z>nqrRGxYJ^W46{bc8RO$aY*mA+67498;Ap^v(xm=-p5JH&p$Z}--?Ph0gG_QO@W9F zRP1z%zpOR{>TE}b3Pq;u1m5$>bdXagBh0(fg=0W>PPd#(wMM;3@ZDqpZnT&6qU8e; zbloLv<*qSYHqbnIa%?&uiQ_3Sok@HCG&d0|_7dHB{{UO62vBS3XSNI%T|1 zYeKd~)i7(a#wJ{{UI3otI0o#tOpB54F7be|cThtx~V@b@4C& z7n|o}!xWOCMF;iZ#lIFqMPPPPVien^Je!Mw4a7YVd~a!&h=eq>ej}We=aqFi%+yw0 z$d^`XVm9l^O%!+O-fEZbtaq4S%;R73+46$gPE+~+*yU^Hwt z*NmnVGzY`a&I@UiM6mcc=G0b!JU;PFH@8WBT!4B=mI3L>hfz=Fc|LG-8u=3dD=kg| z!xaeuc9h1Kx>3>nupI@4aUaFQ)Jvcg_{etf2EB86LJ~!V*AsUT0NF|dl42;ffIbiA z!xK#dYpGu++lriqq5QaoPC?*4oVtYx4@u;p=Jg#dPiM|;MY~Vf`Y}P?9C>QsK}3)Q z+sSoI@Ds!C_8CAYxprSkcQUJnx^HK2X`0uP>t|nhkzJ_2T^vLP$IIs8l?(4fC7ztv z0Zmm}cwD99?RH_HG2dcIH+`9_bps!k0664!Hfw_g#%f7BvGNCuNC;B-1~d6^bjI$lAw1%y z_yWDUvm0qacR8l_5oo?Z-iB&ssF7(qb-dMnSA$xwKV8MtR5qKf&z#bb)9@E-@ZuNT z-CpsjyXF#2-RfdWk`UDLzpUPcp%5C)2$R~rx9=0Np$bf)HrfvU2j?ygjWet|{x6); zg)*?;yz4fkbneVR9&0YZJunqVeEWTv$H+aTOa)+9;RMZA!3vk&zl^H1uCJU>`415j z`sbqhn}s)mP+Q5R=DWSQwvlS{Qj^4gv43%irTFJXE&`} zIvp3U&*a3ayz%kr@Z+FThh2His;kKF&kQzfHLdsHJX6+m!TUd06J8}eiQ&h0Vh_*N zn2{4gr^(=QRDb&u25MpR-#a~-KQ*}8A<7fbdOnECt$$?fhhAs}7cL&T6c z?7`TLZuk??4j5F4TM9 rMJ?AcEeaa;25b@8rmibm(tP5o7(0gZV)JSj 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

y1SxGZ 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 : null} subtitle={t("routing_forms_description")}> {isParentLoading}; diff --git a/packages/lib/hooks/useHasPaidPlan.ts b/packages/lib/hooks/useHasPaidPlan.ts index 9111f4a43b..7ad90ab099 100644 --- a/packages/lib/hooks/useHasPaidPlan.ts +++ b/packages/lib/hooks/useHasPaidPlan.ts @@ -32,4 +32,11 @@ export function useHasTeamPlan() { return { isLoading, hasTeamPlan: hasTeamPlan?.hasTeamPlan }; } +export function useHasEnterprisePlan() { + // TODO: figure out how to get "has Enterprise / has Org" from the backend + const { data: hasTeamPlan, isLoading } = trpc.viewer.teams.hasTeamPlan.useQuery(); + + return { isLoading, hasTeamPlan: hasTeamPlan?.hasTeamPlan }; +} + export default useHasPaidPlan; From 3ea9faa4140d1af3a02f5b3c54efa36f79b5ab51 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 13 Nov 2023 11:58:42 +0000 Subject: [PATCH 002/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/ar/common.json | 1 + apps/web/public/static/locales/cs/common.json | 1 + apps/web/public/static/locales/de/common.json | 1 + apps/web/public/static/locales/es/common.json | 1 + apps/web/public/static/locales/fr/common.json | 1 + apps/web/public/static/locales/he/common.json | 1 + apps/web/public/static/locales/it/common.json | 1 + apps/web/public/static/locales/ja/common.json | 1 + apps/web/public/static/locales/ko/common.json | 1 + apps/web/public/static/locales/nl/common.json | 1 + apps/web/public/static/locales/pl/common.json | 1 + .../public/static/locales/pt-BR/common.json | 1 + apps/web/public/static/locales/pt/common.json | 1 + apps/web/public/static/locales/ro/common.json | 1 + apps/web/public/static/locales/ru/common.json | 1 + apps/web/public/static/locales/sr/common.json | 1 + apps/web/public/static/locales/sv/common.json | 1 + apps/web/public/static/locales/tr/common.json | 1 + apps/web/public/static/locales/uk/common.json | 1 + apps/web/public/static/locales/vi/common.json | 1 + .../public/static/locales/zh-CN/common.json | 52 +++++++++++++++++++ .../public/static/locales/zh-TW/common.json | 1 + 22 files changed, 73 insertions(+) diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index 11b24b5953..e3475858f2 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -2098,5 +2098,6 @@ "view_overlay_calendar_events": "طالع أحداث تقويمك لمنع التضارب بين الحجوزات.", "lock_timezone_toggle_on_booking_page": "قفل المنطقة الزمنية في صفحة الحجز", "description_lock_timezone_toggle_on_booking_page": "تقفل المنطقة الزمنية على صفحة الحجز، وهذا مفيد للأحداث وجهاً لوجه.", + "extensive_whitelabeling": "عملية انضمام ودعم هندسي مخصصين", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index 167b8c5ee8..3824fd365a 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Tento uživatel ještě nedokončil onboarding. Dokud nedokončí onboarding, dostupnost nebude možné nastavit.", "view_only_edit_availability": "Právě máte zobrazenou dostupnost tohoto uživatele. Upravovat lze pouze vlastní dostupnost.", "edit_users_availability": "Upravte dostupnost uživatele: {{username}}", + "extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index f8c03a41cb..29551d3e26 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -2076,5 +2076,6 @@ "resend_invitation": "Einladung erneut senden", "invitation_resent": "Die Einladung wurde erneut gesendet.", "this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet", + "extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 76f03f4f5d..d3404dfbfa 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Este usuario no ha completado la incorporación. No podrá establecer su disponibilidad hasta que haya completado la incorporación.", "view_only_edit_availability": "Está viendo la disponibilidad de este usuario. Sólo puede editar su propia disponibilidad.", "edit_users_availability": "Editar disponibilidad del usuario: {{username}}", + "extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 6aa814498a..fddb0d6757 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -2071,5 +2071,6 @@ "add_client": "Ajouter un client", "add_new_client": "Ajouter un nouveau client", "as_csv": "au format CSV", + "extensive_whitelabeling": "Marque blanche étendue", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index eafb188fa2..c1836ec4de 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "משתמש זה לא השלים תהליך הטמעה. לא תהיה לך אפשרות להגדיר את הזמינות שלו עד שהוא יעשה זאת.", "view_only_edit_availability": "את/ה צופה בזמינות של משתמש זה. יש לך אפשרות לערוך רק את פרטי הזמינות שלך.", "edit_users_availability": "עריכת הזמינות של משתמש: {{username}}", + "extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 2e7295e798..a49c7f6376 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Questo utente non ha completato l'onboarding. Non sarai in grado di impostare la sua disponibilità fino a quando non avrà completato l'onboarding.", "view_only_edit_availability": "Stai visualizzando la disponibilità di questo utente. Puoi solo modificare la tua disponibilità.", "edit_users_availability": "Modifica la disponibilità dell'utente: {{username}}", + "extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index c8c2724d64..4bc7b1941b 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。", "view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。", "edit_users_availability": "ユーザーの空き状況を編集:{{username}}", + "extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index 637de29779..cfc1f0795b 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -2098,5 +2098,6 @@ "view_overlay_calendar_events": "예약 충돌을 방지하려면 캘린더 이벤트를 확인하십시오.", "lock_timezone_toggle_on_booking_page": "예약 페이지의 시간대 잠금", "description_lock_timezone_toggle_on_booking_page": "예약 페이지에서 시간대를 잠그는 기능은 대면 이벤트에 유용합니다.", + "extensive_whitelabeling": "전담 온보딩 및 엔지니어링 지원", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index f41834c897..b4b35f099d 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Deze gebruiker heeft de onboarding nog niet voltooid. U kunt diens beschikbaarheid pas instellen nadat ze men de onboarding heeft voltooid.", "view_only_edit_availability": "U bekijkt de beschikbaarheid van deze gebruiker. U kunt alleen uw eigen beschikbaarheid bewerken.", "edit_users_availability": "Beschikbaarheid van gebruiker bewerken: {{username}}", + "extensive_whitelabeling": "Speciale onboarding en technische ondersteuning", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index 79a89a697d..3cf77aa92f 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Ten użytkownik nie zakończył procesu wdrażania. Ustawienie jego dostępności będzie niedostępne, póki nie zakończy wdrażania.", "view_only_edit_availability": "Wyświetlasz dostępność tego użytkownika. Możesz edytować tylko własną dostępność.", "edit_users_availability": "Edytuj dostępność użytkownika: {{username}}", + "extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index b1e7df6ef1..1cafd75d87 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Este usuário não concluiu a integração. Não será possível definir a disponibilidade antes de concluir a integração.", "view_only_edit_availability": "Você está vendo a disponibilidade deste usuário. Você só pode editar sua disponibilidade.", "edit_users_availability": "Editar disponibilidade do usuário: {{username}}", + "extensive_whitelabeling": "Marca própria abrangente", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index 1e74a7094e..635ae3bc82 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -2047,5 +2047,6 @@ "view_only_edit_availability_not_onboarded": "Este utilizador ainda não completou o processo de integração. Não poderá definir a respetiva disponibilidade até que este procedimento esteja concluído para este utilizador.", "view_only_edit_availability": "Está a visualizar a disponibilidade deste utilizador. Só pode editar a sua própria disponibilidade.", "edit_users_availability": "Editar a disponibilidade do utilizador: {{username}}", + "extensive_whitelabeling": "Apoio dedicado à integração e engenharia", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index d6b2688dbe..9301cb5bb7 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Acest utilizator nu a finalizat integrarea. Nu-i veți putea seta disponibilitatea până când nu finalizează integrarea.", "view_only_edit_availability": "Vizualizați disponibilitatea acestui utilizator. Nu puteți edita decât disponibilitatea dvs. personală.", "edit_users_availability": "Editați disponibilitatea utilizatorului: {{username}}", + "extensive_whitelabeling": "Asistență dedicată pentru integrare și inginerie", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 903a114a5a..3b39179212 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Этот пользователь не прошел онбординг. Вы сможете настроить для него информацию о доступность, только когда он пройдет онбординг.", "view_only_edit_availability": "Вы просматриваете информацию о доступности этого пользователя. Вы можете редактировать только информацию о собственной доступности.", "edit_users_availability": "Редактировать данные о доступности пользователя: {{username}}", + "extensive_whitelabeling": "Индивидуальная техподдержка и помощь при онбординге", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 0689b88ab3..3950f77e73 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -2098,5 +2098,6 @@ "view_overlay_calendar_events": "Pregledajte događaje u vašem kalendaru da biste sprečili dvostruka zakazivanja.", "lock_timezone_toggle_on_booking_page": "Zaključajte vremensku zonu na stranici za zakazivanja", "description_lock_timezone_toggle_on_booking_page": "Za zaključavanje vremenske zone na stranici za zakazivanja, korisno za događaje licem u lice.", + "extensive_whitelabeling": "Posvećena podrška za uvodnu obuku i inženjering", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 46823ec73a..5ac27373e1 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -2098,5 +2098,6 @@ "view_overlay_calendar_events": "Se dina kalenderhändelser för att förhindra krockade bokningar.", "lock_timezone_toggle_on_booking_page": "Lås tidszon på bokningssidan", "description_lock_timezone_toggle_on_booking_page": "För att låsa tidszonen på bokningssidan, användbart för personliga händelser.", + "extensive_whitelabeling": "Dedikerad registrering och teknisk support", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 70f03acd5b..b2674c3cf3 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "Bu kullanıcı katılımı tamamlamadı. Katılımı tamamlayana kadar uygunluk durumlarını ayarlayamazsınız.", "view_only_edit_availability": "Bu kullanıcının müsaitlik durumunu görüntülüyorsunuz. Yalnızca kendi uygunluk durumunuzu düzenleyebilirsiniz.", "edit_users_availability": "Kullanıcının müsaitlik durumunu düzenleyin: {{username}}", + "extensive_whitelabeling": "Özel işe alıştırma ve mühendislik desteği", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index c01c0e3829..6047a46255 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -2098,5 +2098,6 @@ "view_overlay_calendar_events": "Переглядайте заходи у своєму календарі, щоб уникнути накладання бронювань.", "lock_timezone_toggle_on_booking_page": "Заблокуйте часовий пояс на сторінці бронювання", "description_lock_timezone_toggle_on_booking_page": "Для блокування часового поясу на сторінці бронювання (корисне для окремих заходів)", + "extensive_whitelabeling": "Технічна підтримка й підтримка під час ознайомлення", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index e55cea189c..77b63833c0 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -2060,5 +2060,6 @@ "view_only_edit_availability_not_onboarded": "Người dùng chưa hoàn thành việc gia nhập. Bạn sẽ không thể đặt tình trạng lịch trống cho đến khi họ hoàn thành việc gia nhập.", "view_only_edit_availability": "Bạn đang xem tình trạng trống lịch của người dùng này. Bạn chỉ có thể sửa tình trạng trống lịch của riêng bạn.", "edit_users_availability": "Sửa tình trạng trống lịch của người dùng: {{username}}", + "extensive_whitelabeling": "Hướng dẫn bắt đầu và hỗ trợ kỹ thuật chuyên biệt", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index b918bac358..398710547c 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -268,6 +268,7 @@ "set_availability": "设置您的可预约状态", "availability_settings": "可预约时间设置", "continue_without_calendar": "无日历继续", + "continue_with": "继续使用 {{appName}}", "connect_your_calendar": "连接您的日历", "connect_your_video_app": "连接您的视频应用", "connect_your_video_app_instructions": "连接您的视频应用,以将它们用于您的活动类型。", @@ -288,6 +289,8 @@ "when": "什么时间", "where": "在哪", "add_to_calendar": "添加到日历", + "add_to_calendar_description": "选择您被预约后添加活动的位置。", + "add_events_to": "添加活动至", "add_another_calendar": "添加另一个日历", "other": "其他", "email_sign_in_subject": "您的 {{appName}} 登录链接", @@ -422,6 +425,7 @@ "booking_created": "预约已创建", "booking_rejected": "预约被拒绝", "booking_requested": "预约已请求", + "booking_payment_initiated": "预约付款已发起", "meeting_ended": "会议已结束", "form_submitted": "表格已提交", "booking_paid": "预约已付款", @@ -456,6 +460,7 @@ "no_event_types_have_been_setup": "此用户尚未设置任何活动类型。", "edit_logo": "编辑标志", "upload_a_logo": "上传标志", + "upload_logo": "上传标志", "remove_logo": "删除标志", "enable": "启用", "code": "验证码", @@ -568,6 +573,7 @@ "your_team_name": "您的团队名称", "team_updated_successfully": "团队更新成功", "your_team_updated_successfully": "您的团队更新成功。", + "your_org_updated_successfully": "您的组织已成功更新。", "about": "关于", "team_description": "请写一段简单的团队介绍,该介绍将会显示在您的团队链接页面上。", "org_description": "关于您的组织的几句话。这将显示在您组织的链接页面上。", @@ -599,6 +605,7 @@ "hide_book_a_team_member": "隐藏“预约团队成员”按钮", "hide_book_a_team_member_description": "隐藏公共页面中的“预约团队成员”按钮。", "danger_zone": "危险区域", + "account_deletion_cannot_be_undone": "请小心。账户删除无法撤消。", "back": "后退", "cancel": "取消", "cancel_all_remaining": "取消所有剩余活动", @@ -688,6 +695,7 @@ "people": "人员", "your_email": "您的邮箱", "change_avatar": "修改头像", + "upload_avatar": "上传头像", "language": "语言", "timezone": "时区", "first_day_of_week": "每周的第一天", @@ -778,6 +786,7 @@ "disable_guests": "禁用访客", "disable_guests_description": "预约时禁止添加其他访客。", "private_link": "生成私有链接", + "enable_private_url": "启用专用链接", "private_link_label": "私有链接", "private_link_hint": "您的私有链接将在每次使用后重新生成", "copy_private_link": "复制私有链接", @@ -1213,6 +1222,7 @@ "organizer_name_variable": "组织者姓名", "app_upgrade_description": "要使用此功能,您需要升级到专业版帐户。", "invalid_number": "电话号码无效", + "invalid_url_error_message": "{{label}} 的链接无效。示例链接:{{sampleUrl}}", "navigate": "导航", "open": "打开", "close": "关闭", @@ -1287,6 +1297,7 @@ "select_calendars": "选择要检查冲突的日历,以防止重复预约。", "check_for_conflicts": "检查冲突", "view_recordings": "查看录制", + "check_for_recordings": "检查录制内容", "adding_events_to": "添加活动至", "follow_system_preferences": "遵循系统偏好设置", "custom_brand_colors": "自定义品牌颜色", @@ -1531,6 +1542,7 @@ "problem_registering_domain": "注册子域时出现问题,请重试或与管理员联系", "team_publish": "发布团队", "number_text_notifications": "电话号码 (短信通知)", + "number_sms_notifications": "电话号码(短信通知)", "attendee_email_variable": "参与者电子邮件", "attendee_email_info": "预约人的电子邮件", "kbar_search_placeholder": "输入命令或搜索...", @@ -1595,6 +1607,7 @@ "options": "选项", "enter_option": "输入选项 {{index}}", "add_an_option": "添加选项", + "location_already_exists": "此位置已经存在。请选择一个新位置", "radio": "单选", "google_meet_warning": "要使用 Google Meet,您必须将目标日历设置为 Google 日历", "individual": "个人", @@ -1614,6 +1627,7 @@ "date_overrides_mark_all_day_unavailable_other": "在选定日期标记不可预约的时间", "date_overrides_add_btn": "添加替代", "date_overrides_update_btn": "更新替代", + "date_successfully_added": "日期替代已成功添加", "event_type_duplicate_copy_text": "{{slug}}-复制", "set_as_default": "设置为默认", "hide_eventtype_details": "隐藏活动类型详细信息", @@ -1640,6 +1654,7 @@ "minimum_round_robin_hosts_count": "需要参加的主持人数目", "hosts": "主持人", "upgrade_to_enable_feature": "您需要创建团队才能启用此功能。点击以创建团队。", + "orgs_upgrade_to_enable_feature": "您需要升级到我们的企业计划才能启用此功能。", "new_attendee": "新参与者", "awaiting_approval": "待批准", "requires_google_calendar": "此应用需要 Google 日历连接", @@ -1744,6 +1759,7 @@ "show_on_booking_page": "在预约页面上显示", "get_started_zapier_templates": "开始使用 Zapier 模板", "team_is_unpublished": "{{team}} 已被取消发布", + "org_is_unpublished_description": "该组织链接当前不可用。请联系组织所有者或要求他们发布。", "team_is_unpublished_description": "该{{entity}}链接当前不可用。请联系{{entity}}所有者或请他们发布。", "team_member": "团队成员", "a_routing_form": "途径表格", @@ -1878,6 +1894,7 @@ "edit_invite_link": "编辑链接设置", "invite_link_copied": "邀请链接已复制", "invite_link_deleted": "邀请链接已删除", + "api_key_deleted": "API 密钥已删除", "invite_link_updated": "邀请链接设置已保存", "link_expires_after": "设置链接过期时间为...", "one_day": "1 天", @@ -2010,7 +2027,13 @@ "attendee_last_name_variable": "参与者姓氏", "attendee_first_name_info": "预约人的名字", "attendee_last_name_info": "预约人的姓氏", + "your_monthly_digest": "您的每月摘要", + "member_name": "成员姓名", + "most_popular_events": "最受欢迎的活动", + "summary_of_events_for_your_team_for_the_last_30_days": "以下是您的团队 {{teamName}} 在过去 30 天的热门活动摘要", "me": "我", + "monthly_digest_email": "每月摘要电子邮件", + "monthly_digest_email_for_teams": "团队的每月摘要电子邮件", "verify_team_tooltip": "验证您的团队以允许向参与者发送消息", "member_removed": "成员已移除", "my_availability": "我的可预约时间", @@ -2040,12 +2063,41 @@ "team_no_event_types": "此团队没有任何活动类型", "seat_options_doesnt_multiple_durations": "位置选项不支持多个持续时间", "include_calendar_event": "包括日历活动", + "oAuth": "OAuth", "recently_added": "最近添加", "no_members_found": "未找到成员", "event_setup_length_error": "获得设置:持续时间必须至少为 1 分钟。", "availability_schedules": "可预约时间表", + "unauthorized": "未经授权", + "access_cal_account": "{{clientName}} 想要访问您的 {{appName}} 账户", + "select_account_team": "选择账户或团队", + "allow_client_to": "这将使 {{clientName}} 能够", + "associate_with_cal_account": "将您与您在 {{clientName}} 的个人信息相关联", + "see_personal_info": "查看您的个人信息,包括您已公开的任何个人信息", + "see_primary_email_address": "查看您的主要电子邮件地址", + "connect_installed_apps": "连接您已安装的应用", + "access_event_type": "读取、编辑、删除您的活动类型", + "access_availability": "读取、编辑、删除您的可预约时间", + "access_bookings": "读取、编辑、删除您的预约", + "allow_client_to_do": "允许 {{clientName}} 执行此操作?", + "oauth_access_information": "点击“允许”,即表示您允许本应用根据服务条款和隐私政策使用您的信息。您可以在 {{appName}} 应用商店中删除访问权限。", + "allow": "允许", "view_only_edit_availability_not_onboarded": "此用户尚未完成入门流程。在他们完成入门流程之前,您将无法设置他们的可预约时间。", "view_only_edit_availability": "您正在查看此用户的可预约时间。您只能编辑您自己的可预约时间。", + "you_can_override_calendar_in_advanced_tab": "您可以在每个活动类型的高级设置中按活动替代此项。", "edit_users_availability": "编辑用户的可预约时间:{{username}}", + "resend_invitation": "重新发送邀请", + "invitation_resent": "邀请已重新发送。", + "add_client": "添加客户", + "copy_client_secret_info": "复制密码后,您将无法再查看", + "add_new_client": "添加新客户", + "this_app_is_not_setup_already": "此应用尚未设置", + "as_csv": "以 CSV 的形式", + "overlay_my_calendar": "覆盖我的日历", + "overlay_my_calendar_toc": "连接到您的日历,即表示您接受我们的隐私政策和使用条款。您可以随时撤销访问权限。", + "view_overlay_calendar_events": "查看您的日历活动以防止预约冲突。", + "lock_timezone_toggle_on_booking_page": "在预约页面上锁定时区", + "description_lock_timezone_toggle_on_booking_page": "在预约页面上锁定时区,这对面对面活动非常有用。", + "extensive_whitelabeling": "专门的入门和工程支持", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 946db6ea7e..504154694c 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2046,5 +2046,6 @@ "view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。", "view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。", "edit_users_availability": "編輯使用者的可預約時間:{{username}}", + "extensive_whitelabeling": "專屬的入門和工程支援", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From 199f41fb1a0a950923fd485ca3cc212a3873a3b4 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:42:27 +0200 Subject: [PATCH 003/119] fix: add teamId index on VerificationToken (#12339) * fix: add teamId index on InvitationToken * fix: add migration --- .../20231113131945_idx_teamid_verification_token/migration.sql | 2 ++ packages/prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 packages/prisma/migrations/20231113131945_idx_teamid_verification_token/migration.sql diff --git a/packages/prisma/migrations/20231113131945_idx_teamid_verification_token/migration.sql b/packages/prisma/migrations/20231113131945_idx_teamid_verification_token/migration.sql new file mode 100644 index 0000000000..80331f9a96 --- /dev/null +++ b/packages/prisma/migrations/20231113131945_idx_teamid_verification_token/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "VerificationToken_teamId_idx" ON "VerificationToken"("teamId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index a1706169b1..ad21ba81bd 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -335,6 +335,7 @@ model VerificationToken { @@unique([identifier, token]) @@index([token]) + @@index([teamId]) } model BookingReference { From 63f2f6edbace035aa03dd941d004f6c106c4f7e6 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 13 Nov 2023 13:45:46 +0000 Subject: [PATCH 004/119] fix: getTeamOrThrow ran on every request (#12337) * fix: getTeamOrThrow ran on every request This creates unnecessary strain on the DB when someone is hitting the endpoint a lot. * Add rate limit * Update packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts --------- Co-authored-by: Sean Brydon Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --- .../viewer/teams/inviteMember/inviteMember.handler.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index b9d2a5c805..79d560050a 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -2,6 +2,7 @@ import { randomBytes } from "crypto"; import { sendTeamInviteEmail } from "@calcom/emails"; import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getTranslation } from "@calcom/lib/server/i18n"; import { prisma } from "@calcom/prisma"; @@ -32,8 +33,9 @@ type InviteMemberOptions = { }; export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) => { - const team = await getTeamOrThrow(input.teamId, input.isOrg); - const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(input.isOrg, team); + await checkRateLimitAndThrowError({ + identifier: `invitedBy:${ctx.user.id}`, + }); await checkPermissions({ userId: ctx.user.id, @@ -42,6 +44,9 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) = isOrg: input.isOrg, }); + const team = await getTeamOrThrow(input.teamId, input.isOrg); + const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(input.isOrg, team); + const translation = await getTranslation(input.language ?? "en", "common"); const emailsToInvite = await getEmailsToInvite(input.usernameOrEmail); From 621f7639ff04e60f2fb0a81a4beffa601dc069c5 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:29:50 +0530 Subject: [PATCH 005/119] fix: display organizer location after booking (#12088) Co-authored-by: Hariom Balhara --- .../components/eventtype/EventSetupTab.tsx | 14 +-- apps/web/playwright/event-types.e2e.ts | 97 ++++++++++++++++++- packages/app-store/locations.ts | 8 ++ .../BookEventForm/BookingFields.tsx | 24 +++++ .../form-builder/FormBuilderField.tsx | 5 +- 5 files changed, 133 insertions(+), 15 deletions(-) diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index d9b2c066e1..ba36876dca 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -8,7 +8,7 @@ import { Controller, useFormContext, useFieldArray } from "react-hook-form"; import type { MultiValue } from "react-select"; import type { EventLocationType } from "@calcom/app-store/locations"; -import { getEventLocationType, LocationType, MeetLocationType } from "@calcom/app-store/locations"; +import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { CAL_URL } from "@calcom/lib/constants"; @@ -247,15 +247,7 @@ export const EventSetupTab = (

{ await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=where]")).toHaveText(/Cal Video/); }); + + test("can add single organizer address location without display location public option", async ({ + page, + }) => { + const $eventTypes = page.locator("[data-testid=event-types] > li a"); + const firstEventTypeElement = $eventTypes.first(); + await firstEventTypeElement.click(); + await page.waitForURL((url) => { + return !!url.pathname.match(/\/event-types\/.+/); + }); + + const locationAddress = "New Delhi"; + + await fillLocation(page, locationAddress, 0, false); + await page.locator("[data-testid=update-eventtype]").click(); + + await page.goto("/event-types"); + + const previewLink = await page + .locator("[data-testid=preview-link-button]") + .first() + .getAttribute("href"); + + await page.goto(previewLink ?? ""); + await selectFirstAvailableTimeSlotNextMonth(page); + await bookTimeSlot(page); + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + await expect(page.locator(`[data-testid="where"]`)).toHaveText(locationAddress); + }); + + test("can select 'display on booking page' option when multiple organizer input type are present", async ({ + page, + }) => { + await gotoFirstEventType(page); + + await page.locator("#location-select").click(); + await page.locator(`text="Link meeting"`).click(); + + const locationInputName = (idx: number) => `locations[${idx}].link`; + + const testUrl1 = "https://cal.ai/"; + await page.locator(`input[name="${locationInputName(0)}"]`).fill(testUrl1); + await page.locator("[data-testid=display-location]").last().check(); + await checkDisplayLocation(page); + await unCheckDisplayLocation(page); + + await page.locator("[data-testid=add-location]").click(); + + const testUrl2 = "https://cal.com/ai"; + await page.locator(`text="Link meeting"`).last().click(); + await page.locator(`input[name="${locationInputName(1)}"]`).waitFor(); + await page.locator(`input[name="${locationInputName(1)}"]`).fill(testUrl2); + await checkDisplayLocation(page); + await unCheckDisplayLocation(page); + + // Remove Both of the locations + const removeButtomId = "delete-locations.0.type"; + await page.getByTestId(removeButtomId).click(); + await page.getByTestId(removeButtomId).click(); + + // Add Multiple Organizer Phone Number options + await page.locator("#location-select").click(); + await page.locator(`text="Organizer Phone Number"`).click(); + + const organizerPhoneNumberInputName = (idx: number) => `locations[${idx}].hostPhoneNumber`; + + const testPhoneInputValue1 = "9199999999"; + await page.locator(`input[name="${organizerPhoneNumberInputName(0)}"]`).waitFor(); + await page.locator(`input[name="${organizerPhoneNumberInputName(0)}"]`).fill(testPhoneInputValue1); + await page.locator("[data-testid=display-location]").last().check(); + await checkDisplayLocation(page); + await unCheckDisplayLocation(page); + await page.locator("[data-testid=add-location]").click(); + + const testPhoneInputValue2 = "9188888888"; + await page.locator(`text="Organizer Phone Number"`).last().click(); + await page.locator(`input[name="${organizerPhoneNumberInputName(1)}"]`).waitFor(); + await page.locator(`input[name="${organizerPhoneNumberInputName(1)}"]`).fill(testPhoneInputValue2); + await checkDisplayLocation(page); + await unCheckDisplayLocation(page); + }); }); }); }); @@ -293,7 +374,7 @@ async function addAnotherLocation(page: Page, locationOptionText: string) { await page.locator(`text="${locationOptionText}"`).click(); } -const fillLocation = async (page: Page, inputText: string, index: number) => { +const fillLocation = async (page: Page, inputText: string, index: number, selectDisplayLocation = true) => { // Except the first location, dropdown automatically opens when adding another location if (index == 0) { await page.locator("#location-select").last().click(); @@ -303,5 +384,17 @@ const fillLocation = async (page: Page, inputText: string, index: number) => { const locationInputName = `locations[${index}].address`; await page.locator(`input[name="${locationInputName}"]`).waitFor(); await page.locator(`input[name="locations[${index}].address"]`).fill(inputText); - await page.locator("[data-testid=display-location]").last().check(); + if (selectDisplayLocation) { + await page.locator("[data-testid=display-location]").last().check(); + } +}; + +const checkDisplayLocation = async (page: Page) => { + await page.locator("[data-testid=display-location]").last().check(); + await expect(page.locator("[data-testid=display-location]").last()).toBeChecked(); +}; + +const unCheckDisplayLocation = async (page: Page) => { + await page.locator("[data-testid=display-location]").last().uncheck(); + await expect(page.locator("[data-testid=display-location]").last()).toBeChecked({ checked: false }); }; diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 68358d839f..0634625667 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -427,3 +427,11 @@ export const getTranslatedLocation = ( return translatedLocation; }; + +export const getOrganizerInputLocationsType = () => { + const locationTypes: DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"][] = []; + const locations = locationsTypes.filter((location) => !!location.organizerInputType); + locations?.forEach((l) => locationTypes.push(l.type)); + + return locationTypes; +}; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx index a10cdae5b1..dc73cbe2da 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -1,6 +1,7 @@ import { useFormContext } from "react-hook-form"; import type { LocationObject } from "@calcom/app-store/locations"; +import { getOrganizerInputLocationsType } from "@calcom/app-store/locations"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField"; @@ -105,6 +106,29 @@ export const BookingFields = ({ ); } + if (field?.options) { + const organzierInputTypes = getOrganizerInputLocationsType(); + const organzierInputObj: Record = {}; + + field.options.forEach((f) => { + if (f.value in organzierInputObj) { + organzierInputObj[f.value]++; + } else { + organzierInputObj[f.value] = 1; + } + }); + + field.options = field.options.map((field) => { + return { + ...field, + value: + organzierInputTypes.includes(field.value) && organzierInputObj[field.value] > 1 + ? field.label + : field.value, + }; + }); + } + return (
@@ -447,7 +432,7 @@ const ProfileForm = ({ control={formMethods.control} name="avatar" render={({ field: { value } }) => { - const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value); + const showRemoveAvatarButton = !checkIfItFallbackImage(value); const organization = userOrganization && userOrganization.id ? { @@ -474,7 +459,7 @@ const ProfileForm = ({ handleAvatarChange={(newAvatar) => { formMethods.setValue("avatar", newAvatar, { shouldDirty: true }); }} - imageSrc={value || undefined} + imageSrc={value} triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"} /> @@ -482,7 +467,7 @@ const ProfileForm = ({ diff --git a/packages/lib/checkIfItFallbackImage.ts b/packages/lib/checkIfItFallbackImage.ts new file mode 100644 index 0000000000..961d106df7 --- /dev/null +++ b/packages/lib/checkIfItFallbackImage.ts @@ -0,0 +1,6 @@ +import { AVATAR_FALLBACK } from "./constants"; + +const checkIfItFallbackImage = (fetchedImgSrc: string) => { + return !fetchedImgSrc || fetchedImgSrc.endsWith(AVATAR_FALLBACK); +}; +export default checkIfItFallbackImage; diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 849e3f253b..4231d45dc2 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -36,7 +36,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const userMetadata = handleUserMetadata({ ctx, input }); const data: Prisma.UserUpdateInput = { ...input, - avatar: await getAvatarToSet(input.avatar), + avatar: input.avatar ? await getAvatarToSet(input.avatar) : null, metadata: userMetadata, }; diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index ea84cd7fe2..c8d13b716e 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -3,6 +3,7 @@ import type { FormEvent } from "react"; import { useCallback, useEffect, useState } from "react"; import Cropper from "react-easy-crop"; +import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { ButtonColor } from "../.."; @@ -120,20 +121,15 @@ export default function ImageUploader({ buttonMsg, handleAvatarChange, triggerButtonColor, - ...props + imageSrc, }: ImageUploaderProps) { const { t } = useLocale(); - const [imageSrc, setImageSrc] = useState(null); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const [{ result }, setFile] = useFileReader({ method: "readAsDataURL", }); - useEffect(() => { - if (props.imageSrc) setImageSrc(props.imageSrc); - }, [props.imageSrc]); - const onInputFile = (e: FileEvent) => { if (!e.target.files?.length) { return; @@ -157,7 +153,6 @@ export default function ImageUploader({ result as string /* result is always string when using readAsDataUrl */, croppedAreaPixels ); - setImageSrc(croppedImage); handleAvatarChange(croppedImage); } catch (e) { console.error(e); @@ -181,12 +176,11 @@ export default function ImageUploader({
{!result && (
- {!imageSrc && ( + {!imageSrc || checkIfItFallbackImage(imageSrc) ? (

{t("no_target", { target })}

- )} - {imageSrc && ( + ) : ( // eslint-disable-next-line @next/next/no-img-element {target} )} From 4e3d159401d39d003bb2d1251b8df95949f573c3 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 09:56:40 +0000 Subject: [PATCH 044/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/tr/common.json | 11 +++++++++++ apps/web/public/static/locales/zh-TW/common.json | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 8bcefe2afb..40b3d52fde 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -2070,21 +2070,32 @@ "unauthorized": "Yetkisiz", "access_cal_account": "{{clientName}}, {{appName}} hesabınıza erişmek istiyor", "select_account_team": "Bir hesap veya ekip seçin", + "allow_client_to": "Bu işlem {{clientName}} adlı kullanıcıya şu izinleri verecektir:", + "associate_with_cal_account": "Kişisel bilgilerinizi {{clientName}} kullanıcısındaki bilgilerle ilişkilendirmek", "see_personal_info": "Herkese açık hale getirdiğiniz kişisel bilgiler de dâhil olmak üzere kişisel bilgilerinizi görmek", "see_primary_email_address": "Birincil e-posta adresinizi görmek", "connect_installed_apps": "Yüklü uygulamalarınıza bağlanmak", "access_event_type": "Etkinlik türlerinizi okumak, düzenlemek, silmek", "access_availability": "Müsaitlik durumunuzu okuma, düzenleme, silme", "access_bookings": "Rezervasyonlarınızı okuma, düzenleme, silme", + "allow_client_to_do": "{{clientName}} adlı kullanıcıya bu işlemler için izin verilsin mi?", "oauth_access_information": "İzin ver'i tıklayarak, bu uygulamanın bilgilerinizi hizmet şartlarına ve gizlilik politikasına uygun şekilde kullanmasına izin vermiş olursunuz. Bu erişim iznini {{appName}} Uygulama Mağazası'ndan kaldırabilirsiniz.", "allow": "İzin ver", "view_only_edit_availability_not_onboarded": "Bu kullanıcı katılımı tamamlamadı. Katılımı tamamlayana kadar uygunluk durumlarını ayarlayamazsınız.", "view_only_edit_availability": "Bu kullanıcının müsaitlik durumunu görüntülüyorsunuz. Yalnızca kendi uygunluk durumunuzu düzenleyebilirsiniz.", + "you_can_override_calendar_in_advanced_tab": "Bu ayarı, her olay türündeki Gelişmiş ayarlardan olay bazında geçersiz kılabilirsiniz.", "edit_users_availability": "Kullanıcının müsaitlik durumunu düzenleyin: {{username}}", + "resend_invitation": "Davetiyeyi yeniden gönder", "invitation_resent": "Davetiye tekrar gönderildi.", + "add_client": "Müşteri ekle", + "copy_client_secret_info": "Gizli bilgiyi kopyaladıktan sonra artık görüntüleyemeyeceksiniz", + "add_new_client": "Yeni Müşteri Ekle", "this_app_is_not_setup_already": "Bu uygulama henüz kurulmadı", "as_csv": "CSV olarak", + "overlay_my_calendar": "Takvimimin üzerine yaz", "overlay_my_calendar_toc": "Takviminize bağlanarak gizlilik politikamızı ve kullanım koşullarımızı kabul etmiş olursunuz. Erişimi istediğiniz zaman iptal edebilirsiniz.", + "lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitle", + "description_lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitlemek kişisel etkinlikler için kullanışlıdır.", "extensive_whitelabeling": "Özel işe alıştırma ve mühendislik desteği", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 504154694c..11e8933358 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -268,6 +268,7 @@ "set_availability": "設定開放時間", "availability_settings": "可預約時間設定", "continue_without_calendar": "在沒有行事曆的情況下繼續", + "continue_with": "繼續使用 {{appName}}", "connect_your_calendar": "連結行事曆", "connect_your_video_app": "連接視訊應用程式", "connect_your_video_app_instructions": "連接視訊應用程式,以便用於活動類型。", @@ -288,6 +289,8 @@ "when": "時間", "where": "地點", "add_to_calendar": "加到行事曆", + "add_to_calendar_description": "設定您有新預約時要新增活動的應用程式。", + "add_events_to": "新增活動至", "add_another_calendar": "新增另一個行事曆", "other": "其它", "email_sign_in_subject": "您的「{{appName}}」登入連結", @@ -422,6 +425,7 @@ "booking_created": "已建立預約", "booking_rejected": "預約已遭拒", "booking_requested": "預約已提出", + "booking_payment_initiated": "已發起預約付款", "meeting_ended": "會議已結束", "form_submitted": "表單已提交", "booking_paid": "預約已付款", @@ -456,6 +460,7 @@ "no_event_types_have_been_setup": "使用者尚未設定任何活動類型。", "edit_logo": "編輯標誌", "upload_a_logo": "上傳標誌", + "upload_logo": "上傳標誌", "remove_logo": "移除標誌", "enable": "開啟", "code": "代碼", @@ -568,6 +573,7 @@ "your_team_name": "取團隊名稱", "team_updated_successfully": "成功更新團隊", "your_team_updated_successfully": "更新團隊成功。", + "your_org_updated_successfully": "您的組織已成功更新。", "about": "關於", "team_description": "請提供關於團隊的簡介,這會顯示在團隊網址的頁面上。", "org_description": "請提供關於組織的簡介,這會顯示在組織的網頁上。", @@ -599,6 +605,7 @@ "hide_book_a_team_member": "隱藏「預約團隊成員」按鈕", "hide_book_a_team_member_description": "隱藏您公開頁面中的「預約團隊成員」按鈕。", "danger_zone": "危險區域", + "account_deletion_cannot_be_undone": "請注意,帳號一經刪除即無法還原。", "back": "上一步", "cancel": "取消", "cancel_all_remaining": "取消所有剩餘活動", @@ -688,6 +695,7 @@ "people": "人員", "your_email": "電子郵件", "change_avatar": "更換大頭照", + "upload_avatar": "上傳大頭照", "language": "語言", "timezone": "時區", "first_day_of_week": "每週的第一天", @@ -778,6 +786,7 @@ "disable_guests": "關閉賓客", "disable_guests_description": "關閉在預約時,可以增加額外賓客。", "private_link": "產生不公開的連結", + "enable_private_url": "啟用私人連結", "private_link_label": "私人連結", "private_link_hint": "您的私人連結會在每次使用後重新產生", "copy_private_link": "複製不公開連結", @@ -1213,6 +1222,7 @@ "organizer_name_variable": "主辦者姓名", "app_upgrade_description": "您必須升級至專業版帳號才能使用此功能。", "invalid_number": "電話號碼無效", + "invalid_url_error_message": "{{label}} 網址無效。網址範例:{{sampleUrl}}", "navigate": "導覽", "open": "開啟", "close": "關閉", @@ -1276,6 +1286,7 @@ "personal_cal_url": "我的個人 {{appName}} 網址", "bio_hint": "一些關於自己的句子,會顯示在個人網址頁面。", "user_has_no_bio": "此使用者尚未新增個人資料。", + "bio": "個人資料", "delete_account_modal_title": "刪除帳號", "confirm_delete_account_modal": "確定要刪除您的 {{appName}} 的帳號嗎?", "delete_my_account": "刪除我的帳號", @@ -1286,6 +1297,7 @@ "select_calendars": "選擇您想要檢查衝突的行事曆來避免重複預約。", "check_for_conflicts": "檢查衝突", "view_recordings": "檢視錄製內容", + "check_for_recordings": "檢查錄製內容", "adding_events_to": "新增活動至", "follow_system_preferences": "遵循系統偏好設定", "custom_brand_colors": "自訂品牌顏色", @@ -1530,6 +1542,7 @@ "problem_registering_domain": "註冊子網域時發生問題,請再試一次或聯絡管理員", "team_publish": "發佈團隊", "number_text_notifications": "電話號碼 (簡訊通知)", + "number_sms_notifications": "電話號碼 (簡訊通知)", "attendee_email_variable": "與會者電子郵件", "attendee_email_info": "預約人電子郵件", "kbar_search_placeholder": "輸入指令或搜尋…", @@ -1594,6 +1607,7 @@ "options": "選項", "enter_option": "輸入選項 {{index}}", "add_an_option": "新增選項", + "location_already_exists": "此位置已存在。請選擇新位置", "radio": "無線電", "google_meet_warning": "如要使用 Google Meet,您必須將目標行事曆設為 Google 日曆", "individual": "個人", @@ -1613,6 +1627,7 @@ "date_overrides_mark_all_day_unavailable_other": "將所選日期標示不開放", "date_overrides_add_btn": "新增覆寫", "date_overrides_update_btn": "更新覆寫", + "date_successfully_added": "日期覆寫已成功新增", "event_type_duplicate_copy_text": "{{slug}}-複製", "set_as_default": "設為預設", "hide_eventtype_details": "隱藏活動類型詳細資料", From 9514a68039c225cbc420f08e5179305c27674632 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 09:59:22 +0000 Subject: [PATCH 045/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 11e8933358..bea3ec4f9a 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -1654,6 +1654,7 @@ "minimum_round_robin_hosts_count": "須參加的主辦人數", "hosts": "主辦人", "upgrade_to_enable_feature": "您需要建立團隊才能啟用此功能。按一下即可建立團隊。", + "orgs_upgrade_to_enable_feature": "須升級至 Enterprise 方案才能啟用此功能。", "new_attendee": "新參與者", "awaiting_approval": "正在等待核准", "requires_google_calendar": "此應用程式需要 Google 日曆連結", @@ -1758,6 +1759,7 @@ "show_on_booking_page": "在預約頁面上顯示", "get_started_zapier_templates": "開始使用 Zapier 範本", "team_is_unpublished": "已取消發佈 {{team}}", + "org_is_unpublished_description": "此組織連結目前無法使用。請聯絡組織擁有者,或請對方發佈。", "team_is_unpublished_description": "此 {{entity}} 連結目前無法使用。請聯絡 {{entity}} 擁有者,或請對方發佈。", "team_member": "團隊成員", "a_routing_form": "引導表單", @@ -1892,6 +1894,7 @@ "edit_invite_link": "編輯連結設定", "invite_link_copied": "邀請連結已複製", "invite_link_deleted": "邀請連結已刪除", + "api_key_deleted": "API 金鑰已刪除", "invite_link_updated": "邀請連結設定已儲存", "link_expires_after": "將連結效期設為…", "one_day": "1 天", From e55302cbc51505f5814943f299f9c17adc819edd Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 10:02:20 +0000 Subject: [PATCH 046/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index bea3ec4f9a..68e1d3364c 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2027,7 +2027,13 @@ "attendee_last_name_variable": "與會者姓氏", "attendee_first_name_info": "預約人名字", "attendee_last_name_info": "預約人姓氏", + "your_monthly_digest": "您的每月摘要", + "member_name": "成員姓名", + "most_popular_events": "最熱門活動", + "summary_of_events_for_your_team_for_the_last_30_days": "您的團隊「{{teamName}}」最近 30 天的熱門活動摘要如下", "me": "我", + "monthly_digest_email": "每月摘要電子郵件", + "monthly_digest_email_for_teams": "團隊每月摘要電子郵件", "verify_team_tooltip": "驗證您的團隊,已啟用傳送訊息給與會者的訊息", "member_removed": "成員已移除", "my_availability": "我的可預約時間", @@ -2057,10 +2063,14 @@ "team_no_event_types": "此團隊沒有任何活動類型", "seat_options_doesnt_multiple_durations": "座位選項不支援多個持續時間", "include_calendar_event": "包含行事曆活動", + "oAuth": "OAuth", "recently_added": "最近新增", "no_members_found": "找不到成員", "event_setup_length_error": "活動設定:持續時間至少必須為 1 分鐘。", "availability_schedules": "可預約時間行程表", + "unauthorized": "未授權", + "access_cal_account": "{{clientName}} 想要存取您的 {{appName}} 帳號", + "select_account_team": "選擇帳號或團隊", "view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。", "view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。", "edit_users_availability": "編輯使用者的可預約時間:{{username}}", From ecfecd832d0e605afebe8fd842b2dc1bba61fb06 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 10:05:17 +0000 Subject: [PATCH 047/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 68e1d3364c..4476864061 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2071,6 +2071,12 @@ "unauthorized": "未授權", "access_cal_account": "{{clientName}} 想要存取您的 {{appName}} 帳號", "select_account_team": "選擇帳號或團隊", + "allow_client_to": "這麼做,{{clientName}} 即可", + "associate_with_cal_account": "將您和您在 {{clientName}} 的個人資料建立關聯", + "see_personal_info": "查看您的個人資料,包括您公開的所有個人資料", + "see_primary_email_address": "查看您的主要電子郵件地址", + "connect_installed_apps": "連結至您已安裝的應用程式", + "access_event_type": "讀取、編輯、刪除您的活動類型", "view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。", "view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。", "edit_users_availability": "編輯使用者的可預約時間:{{username}}", From 67f83c7e7d4e47e4b2b7a8265d4a3d24f5b90825 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 10:08:40 +0000 Subject: [PATCH 048/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 4476864061..c66356d6cf 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2077,9 +2077,19 @@ "see_primary_email_address": "查看您的主要電子郵件地址", "connect_installed_apps": "連結至您已安裝的應用程式", "access_event_type": "讀取、編輯、刪除您的活動類型", + "access_availability": "讀取、編輯、刪除您的可預約時間", + "access_bookings": "讀取、編輯、刪除您的預約", + "allow_client_to_do": "允許 {{clientName}} 這麼做嗎?", + "oauth_access_information": "按一下「允許」後,即表示此應用程式可根據他們的服務和隱私政策使用您的資訊。您可在 {{appName}} App Store 中移除存取權限。", + "allow": "允許", "view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。", "view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。", + "you_can_override_calendar_in_advanced_tab": "您可在每個活動類型中的「進階」設定中根據活動覆寫此設定。", "edit_users_availability": "編輯使用者的可預約時間:{{username}}", + "resend_invitation": "重新傳送邀請", + "invitation_resent": "邀請已重新傳送。", + "add_client": "新增用戶端", + "copy_client_secret_info": "密碼複製後即無法再查看", "extensive_whitelabeling": "專屬的入門和工程支援", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From 693ca046260546d9144f6d282a4c628b652b6df5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 10:11:45 +0000 Subject: [PATCH 049/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index c66356d6cf..30ba758074 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2090,6 +2090,9 @@ "invitation_resent": "邀請已重新傳送。", "add_client": "新增用戶端", "copy_client_secret_info": "密碼複製後即無法再查看", + "add_new_client": "新增客戶", + "this_app_is_not_setup_already": "此應用程式尚未完成設定", + "as_csv": "採 CSV 格式", "extensive_whitelabeling": "專屬的入門和工程支援", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From 87b514b91b4eeceaf5f5960021938bf90ccf712f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 15 Nov 2023 10:14:51 +0000 Subject: [PATCH 050/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/zh-TW/common.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 30ba758074..49d485a69d 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2093,6 +2093,11 @@ "add_new_client": "新增客戶", "this_app_is_not_setup_already": "此應用程式尚未完成設定", "as_csv": "採 CSV 格式", + "overlay_my_calendar": "覆蓋我的行事曆", + "overlay_my_calendar_toc": "連接至您的行事曆後,即表示您接受我們的隱私政策和使用條款。您隨時可撤回存取權限。", + "view_overlay_calendar_events": "查看您的行事曆活動,避免預約發生衝突。", + "lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區", + "description_lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區,安排實體活動時非常實用。", "extensive_whitelabeling": "專屬的入門和工程支援", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From a804a295168e9a3263c59686ee11fd8957028c73 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 15 Nov 2023 09:29:41 -0300 Subject: [PATCH 051/119] feat: Stripe paid apps flow (#12103) * chore: Stripe paid apps flow * chore: Subscription * chore: Webhooks * chore: Abstract functions * chore: Lockfile * chore: Webhook handler * chore: Use catch-all * chore: Webhook changes, etc * chore: Cleanup * chore: Use actual price id * chore: Updates * chore: Install normally until expiry date * Disable team install for paid apps and cal.ai\ * Fix the same at another place * Fix Typescript error * redactedCause doesnt have message has enumerable prop * Fix reinstallation of an already installed app * chore: Remove unused deps * chore: Ensure index * chore: Price in usd * chore: PR suggestion * Fix missing packages in yarn.lock --------- Co-authored-by: Hariom Co-authored-by: Peer Richelsen --- apps/web/components/apps/AppPage.tsx | 57 ++++++--- .../components/apps/InstallAppButtonChild.tsx | 24 +++- .../api/integrations/subscriptions/webhook.ts | 116 ++++++++++++++++++ apps/web/pages/apps/[slug]/index.tsx | 1 + apps/web/public/static/locales/en/common.json | 2 + packages/app-store/_utils/installation.ts | 25 ++-- packages/app-store/_utils/paid-apps.ts | 92 ++++++++++++++ packages/app-store/_utils/stripe.ts | 74 +++++++++++ packages/app-store/cal-ai/api/_getAdd.ts | 79 +++++++----- packages/app-store/cal-ai/api/_getCallback.ts | 65 ++++++++++ packages/app-store/cal-ai/api/callback.ts | 5 + packages/app-store/cal-ai/api/index.ts | 1 + packages/app-store/cal-ai/config.json | 8 +- packages/app-store/package.json | 3 +- packages/app-store/utils.ts | 17 ++- packages/features/tips/Tips.tsx | 5 +- packages/lib/piiFreeData.ts | 2 +- .../lib/server/getServerErrorFromUnknown.ts | 1 + .../20231110122349_paid_apps/migration.sql | 7 ++ packages/prisma/schema.prisma | 25 ++-- packages/types/App.d.ts | 10 ++ packages/ui/components/apps/AppCard.tsx | 31 ++++- yarn.lock | 80 ++++++++---- 23 files changed, 630 insertions(+), 100 deletions(-) create mode 100644 apps/web/pages/api/integrations/subscriptions/webhook.ts create mode 100644 packages/app-store/_utils/paid-apps.ts create mode 100644 packages/app-store/_utils/stripe.ts create mode 100644 packages/app-store/cal-ai/api/_getCallback.ts create mode 100644 packages/app-store/cal-ai/api/callback.ts create mode 100644 packages/prisma/migrations/20231110122349_paid_apps/migration.sql diff --git a/apps/web/components/apps/AppPage.tsx b/apps/web/components/apps/AppPage.tsx index 05c095caa1..c6a35e9749 100644 --- a/apps/web/components/apps/AppPage.tsx +++ b/apps/web/components/apps/AppPage.tsx @@ -42,6 +42,7 @@ export type AppPageProps = { disableInstall?: boolean; dependencies?: string[]; concurrentMeetings: AppType["concurrentMeetings"]; + paid?: AppType["paid"]; }; export const AppPage = ({ @@ -67,6 +68,7 @@ export const AppPage = ({ isTemplate, dependencies, concurrentMeetings, + paid, }: AppPageProps) => { const { t, i18n } = useLocale(); const hasDescriptionItems = descriptionItems && descriptionItems.length > 0; @@ -163,6 +165,19 @@ export const AppPage = ({ className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize"> {categories[0]} {" "} + {paid && ( + <> + + {Intl.NumberFormat(i18n.language, { + style: "currency", + currency: "USD", + useGrouping: false, + maximumFractionDigits: 0, + }).format(paid.priceInUsd)} + /{t("month")} + + + )} •{" "}
{t("published_by", { author })} @@ -206,6 +221,7 @@ export const AppPage = ({ addAppMutationInput={{ type, variant, slug }} multiInstall concurrentMeetings={concurrentMeetings} + paid={paid} {...props} /> ); @@ -244,6 +260,7 @@ export const AppPage = ({ addAppMutationInput={{ type, variant, slug }} credentials={appDbQuery.data?.credentials} concurrentMeetings={concurrentMeetings} + paid={paid} {...props} /> ); @@ -263,7 +280,7 @@ export const AppPage = ({ ))} - {price !== 0 && ( + {price !== 0 && !paid && ( {feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar} {feeType === "monthly" && `/${t("month")}`} @@ -273,23 +290,27 @@ export const AppPage = ({
{body}
-

{t("pricing")}

- - {teamsPlanRequired ? ( - t("teams_plan_required") - ) : price === 0 ? ( - t("free_to_use_apps") - ) : ( - <> - {Intl.NumberFormat(i18n.language, { - style: "currency", - currency: "USD", - useGrouping: false, - }).format(price)} - {feeType === "monthly" && `/${t("month")}`} - - )} - + {!paid && ( + <> +

{t("pricing")}

+ + {teamsPlanRequired ? ( + t("teams_plan_required") + ) : price === 0 ? ( + t("free_to_use_apps") + ) : ( + <> + {Intl.NumberFormat(i18n.language, { + style: "currency", + currency: "USD", + useGrouping: false, + }).format(price)} + {feeType === "monthly" && `/${t("month")}`} + + )} + + + )}

{t("contact")}

    diff --git a/apps/web/components/apps/InstallAppButtonChild.tsx b/apps/web/components/apps/InstallAppButtonChild.tsx index b6ec80ddca..0c13e754e8 100644 --- a/apps/web/components/apps/InstallAppButtonChild.tsx +++ b/apps/web/components/apps/InstallAppButtonChild.tsx @@ -26,6 +26,7 @@ export const InstallAppButtonChild = ({ multiInstall, credentials, concurrentMeetings, + paid, ...props }: { userAdminTeams?: UserAdminTeams; @@ -34,6 +35,7 @@ export const InstallAppButtonChild = ({ multiInstall?: boolean; credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"]; concurrentMeetings?: boolean; + paid?: AppFrontendPayload["paid"]; } & ButtonProps) => { const { t } = useLocale(); @@ -46,8 +48,27 @@ export const InstallAppButtonChild = ({ if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error"); }, }); + const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false; - if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) { + // Paid apps don't support team installs at the moment + // Also, cal.ai(the only paid app at the moment) doesn't support team install either + if (paid) { + return ( + + ); + } + + if ( + !userAdminTeams?.length || + !doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid }) + ) { return ( diff --git a/apps/web/pages/api/integrations/subscriptions/webhook.ts b/apps/web/pages/api/integrations/subscriptions/webhook.ts new file mode 100644 index 0000000000..63f4cba477 --- /dev/null +++ b/apps/web/pages/api/integrations/subscriptions/webhook.ts @@ -0,0 +1,116 @@ +import { buffer } from "micro"; +import type { NextApiRequest, NextApiResponse } from "next"; +import type Stripe from "stripe"; + +import stripe from "@calcom/app-store/stripepayment/lib/server"; +import { IS_PRODUCTION } from "@calcom/lib/constants"; +import { getErrorFromUnknown } from "@calcom/lib/errors"; +import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import prisma from "@calcom/prisma"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +// This file is a catch-all for any integration related subscription/paid app. + +const handleSubscriptionUpdate = async (event: Stripe.Event) => { + const subscription = event.data.object as Stripe.Subscription; + if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" }); + + const app = await prisma.credential.findFirst({ + where: { + subscriptionId: subscription.id, + }, + }); + + if (!app) { + throw new HttpCode({ statusCode: 202, message: "Received and discarded" }); + } + + await prisma.credential.update({ + where: { + id: app.id, + }, + data: { + paymentStatus: subscription.status, + }, + }); +}; + +const handleSubscriptionDeleted = async (event: Stripe.Event) => { + const subscription = event.data.object as Stripe.Subscription; + if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" }); + + const app = await prisma.credential.findFirst({ + where: { + subscriptionId: subscription.id, + }, + }); + + if (!app) { + throw new HttpCode({ statusCode: 202, message: "Received and discarded" }); + } + + // should we delete the credential here rather than marking as inactive? + await prisma.credential.update({ + where: { + id: app.id, + }, + data: { + paymentStatus: "inactive", + billingCycleStart: null, + }, + }); +}; + +type WebhookHandler = (event: Stripe.Event) => Promise; + +const webhookHandlers: Record = { + "customer.subscription.updated": handleSubscriptionUpdate, + "customer.subscription.deleted": handleSubscriptionDeleted, +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (req.method !== "POST") { + throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); + } + const sig = req.headers["stripe-signature"]; + if (!sig) { + throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" }); + } + + if (!process.env.STRIPE_WEBHOOK_SECRET) { + throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" }); + } + const requestBuffer = await buffer(req); + const payload = requestBuffer.toString(); + + const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); + + const handler = webhookHandlers[event.type]; + if (handler) { + await handler(event); + } else { + /** Not really an error, just letting Stripe know that the webhook was received but unhandled */ + throw new HttpCode({ + statusCode: 202, + message: `Unhandled Stripe Webhook event type ${event.type}`, + }); + } + } catch (_err) { + const err = getErrorFromUnknown(_err); + console.error(`Webhook Error: ${err.message}`); + res.status(err.statusCode ?? 500).send({ + message: err.message, + stack: IS_PRODUCTION ? undefined : err.stack, + }); + return; + } + + // Return a response to acknowledge receipt of the event + res.json({ received: true }); +} diff --git a/apps/web/pages/apps/[slug]/index.tsx b/apps/web/pages/apps/[slug]/index.tsx index 0289358470..da41a33402 100644 --- a/apps/web/pages/apps/[slug]/index.tsx +++ b/apps/web/pages/apps/[slug]/index.tsx @@ -79,6 +79,7 @@ function SingleAppPage(props: inferSSRProps) { isTemplate={data.isTemplate} dependencies={data.dependencies} concurrentMeetings={data.concurrentMeetings} + paid={data.paid} // tos="https://zoom.us/terms" // privacy="https://zoom.us/privacy" body={ diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 732fb2d5dc..9c7bd29d8f 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -849,6 +849,8 @@ "next_step": "Skip step", "prev_step": "Prev step", "install": "Install", + "install_paid_app": "Subscribe", + "start_paid_trial": "Start free Trial", "installed": "Installed", "active_install_one": "{{count}} active install", "active_install_other": "{{count}} active installs", diff --git a/packages/app-store/_utils/installation.ts b/packages/app-store/_utils/installation.ts index eb635fb6d9..6ee52e8c42 100644 --- a/packages/app-store/_utils/installation.ts +++ b/packages/app-store/_utils/installation.ts @@ -15,25 +15,36 @@ export async function checkInstalled(slug: string, userId: number) { } } +type InstallationArgs = { + appType: string; + userId: number; + slug: string; + key?: Prisma.InputJsonValue; + teamId?: number; + subscriptionId?: string | null; + paymentStatus?: string | null; + billingCycleStart?: number | null; +}; + export async function createDefaultInstallation({ appType, userId, slug, key = {}, teamId, -}: { - appType: string; - userId: number; - slug: string; - key?: Prisma.InputJsonValue; - teamId?: number; -}) { + billingCycleStart, + paymentStatus, + subscriptionId, +}: InstallationArgs) { const installation = await prisma.credential.create({ data: { type: appType, key, ...(teamId ? { teamId } : { userId }), appId: slug, + subscriptionId, + paymentStatus, + billingCycleStart, }, }); if (!installation) { diff --git a/packages/app-store/_utils/paid-apps.ts b/packages/app-store/_utils/paid-apps.ts new file mode 100644 index 0000000000..6d39c98ff6 --- /dev/null +++ b/packages/app-store/_utils/paid-apps.ts @@ -0,0 +1,92 @@ +import type Stripe from "stripe"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; + +import { getStripeCustomerIdFromUserId, stripe } from "./stripe"; + +interface RedirectArgs { + userId: number; + appSlug: string; + appPaidMode: string; + priceId: string; + trialDays?: number; +} + +export const withPaidAppRedirect = async ({ + appSlug, + appPaidMode, + priceId, + userId, + trialDays, +}: RedirectArgs) => { + const redirect_uri = `${WEBAPP_URL}/api/integrations/${appSlug}/callback?checkoutId={CHECKOUT_SESSION_ID}`; + + const stripeCustomerId = await getStripeCustomerIdFromUserId(userId); + const checkoutSession = await stripe.checkout.sessions.create({ + success_url: redirect_uri, + cancel_url: redirect_uri, + mode: appPaidMode === "subscription" ? "subscription" : "payment", + payment_method_types: ["card"], + allow_promotion_codes: true, + customer: stripeCustomerId, + line_items: [ + { + quantity: 1, + price: priceId, + }, + ], + client_reference_id: userId.toString(), + ...(trialDays + ? { + subscription_data: { + trial_period_days: trialDays, + trial_settings: { end_behavior: { missing_payment_method: "cancel" } }, + }, + } + : undefined), + }); + + return checkoutSession.url; +}; + +export const withStripeCallback = async ( + checkoutId: string, + appSlug: string, + callback: (args: { checkoutSession: Stripe.Checkout.Session }) => Promise<{ url: string }> +): Promise<{ url: string }> => { + if (!checkoutId) { + return { + url: `/apps/installed?error=${encodeURIComponent( + JSON.stringify({ message: "No Stripe Checkout Session ID" }) + )}`, + }; + } + + const checkoutSession = await stripe.checkout.sessions.retrieve(checkoutId); + if (!checkoutSession) { + return { + url: `/apps/installed?error=${encodeURIComponent( + JSON.stringify({ message: "Unknown Stripe Checkout Session ID" }) + )}`, + }; + } + + if (checkoutSession.payment_status !== "paid") { + return { + url: `/apps/installed?error=${encodeURIComponent( + JSON.stringify({ message: "Stripe Payment not processed" }) + )}`, + }; + } + + if (checkoutSession.mode === "subscription" && checkoutSession.subscription) { + await stripe.subscriptions.update(checkoutSession.subscription.toString(), { + metadata: { + appSlug, + }, + }); + } + + // Execute the callback if all checks pass + return callback({ checkoutSession }); +}; diff --git a/packages/app-store/_utils/stripe.ts b/packages/app-store/_utils/stripe.ts new file mode 100644 index 0000000000..2b3b5895bc --- /dev/null +++ b/packages/app-store/_utils/stripe.ts @@ -0,0 +1,74 @@ +import { Prisma } from "@prisma/client"; +import Stripe from "stripe"; + +import { HttpError } from "@calcom/lib/http-error"; +import prisma from "@calcom/prisma"; + +export async function getStripeCustomerIdFromUserId(userId: number) { + // Get user + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + email: true, + name: true, + metadata: true, + }, + }); + + if (!user?.email) throw new HttpError({ statusCode: 404, message: "User email not found" }); + + const customerId = await getStripeCustomerId(user); + + return customerId; +} + +const userType = Prisma.validator()({ + select: { + email: true, + metadata: true, + }, +}); + +export type UserType = Prisma.UserGetPayload; +/** This will retrieve the customer ID from Stripe or create it if it doesn't exists yet. */ +export async function getStripeCustomerId(user: UserType): Promise { + let customerId: string | null = null; + + if (user?.metadata && typeof user.metadata === "object" && "stripeCustomerId" in user.metadata) { + customerId = (user?.metadata as Prisma.JsonObject).stripeCustomerId as string; + } else { + /* We fallback to finding the customer by email (which is not optimal) */ + const customersResponse = await stripe.customers.list({ + email: user.email, + limit: 1, + }); + if (customersResponse.data[0]?.id) { + customerId = customersResponse.data[0].id; + } else { + /* Creating customer on Stripe and saving it on prisma */ + const customer = await stripe.customers.create({ email: user.email }); + customerId = customer.id; + } + + await prisma.user.update({ + where: { + email: user.email, + }, + data: { + metadata: { + ...(user.metadata as Prisma.JsonObject), + stripeCustomerId: customerId, + }, + }, + }); + } + + return customerId; +} + +const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY || ""; +export const stripe = new Stripe(stripePrivateKey, { + apiVersion: "2020-08-27", +}); diff --git a/packages/app-store/cal-ai/api/_getAdd.ts b/packages/app-store/cal-ai/api/_getAdd.ts index a6dec2da5a..a607a83e7b 100644 --- a/packages/app-store/cal-ai/api/_getAdd.ts +++ b/packages/app-store/cal-ai/api/_getAdd.ts @@ -7,46 +7,63 @@ import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_route import checkSession from "../../_utils/auth"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import { checkInstalled, createDefaultInstallation } from "../../_utils/installation"; +import { withPaidAppRedirect } from "../../_utils/paid-apps"; import appConfig from "../config.json"; +const trialEndDate = new Date(Date.UTC(2023, 11, 1)); + export async function getHandler(req: NextApiRequest, res: NextApiResponse) { const session = checkSession(req); - const slug = appConfig.slug; - const appType = appConfig.type; - const ctx = await createContext({ req, res }); - const caller = apiKeysRouter.createCaller(ctx); + // if date is in the future, we install normally. + if (new Date() < trialEndDate) { + const ctx = await createContext({ req, res }); + const caller = apiKeysRouter.createCaller(ctx); - const apiKey = await caller.create({ - note: "Cal.ai", - expiresAt: null, - appId: "cal-ai", - }); + const apiKey = await caller.create({ + note: "Cal.ai", + expiresAt: null, + appId: "cal-ai", + }); - await checkInstalled(slug, session.user.id); - await createDefaultInstallation({ - appType, - userId: session.user.id, - slug, - key: { - apiKey, - }, - }); - - await fetch( - `${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`, - { - method: "POST", - headers: { - "Content-Type": "application/json", + await checkInstalled(appConfig.slug, session.user.id); + await createDefaultInstallation({ + appType: appConfig.type, + userId: session.user.id, + slug: appConfig.slug, + key: { + apiKey, }, - body: JSON.stringify({ - userId: session.user.id, - }), - } - ); + }); - return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) }; + await fetch( + `${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: session.user.id, + }), + } + ); + + return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) }; + } + + const redirectUrl = await withPaidAppRedirect({ + appPaidMode: appConfig.paid.mode, + appSlug: appConfig.slug, + userId: session.user.id, + priceId: appConfig.paid.priceId, + }); + + if (!redirectUrl) { + return res.status(500).json({ message: "Failed to create Stripe checkout session" }); + } + + return { url: redirectUrl }; } export default defaultResponder(getHandler); diff --git a/packages/app-store/cal-ai/api/_getCallback.ts b/packages/app-store/cal-ai/api/_getCallback.ts new file mode 100644 index 0000000000..b85f38e479 --- /dev/null +++ b/packages/app-store/cal-ai/api/_getCallback.ts @@ -0,0 +1,65 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { defaultResponder } from "@calcom/lib/server"; +import { createContext } from "@calcom/trpc/server/createContext"; +import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_router"; + +import checkSession from "../../_utils/auth"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import { checkInstalled, createDefaultInstallation } from "../../_utils/installation"; +import { withStripeCallback } from "../../_utils/paid-apps"; +import appConfig from "../config.json"; + +export async function getHandler(req: NextApiRequest, res: NextApiResponse) { + const session = checkSession(req); + const slug = appConfig.slug; + const appType = appConfig.type; + + const { checkoutId } = req.query as { checkoutId: string }; + if (!checkoutId) { + return { url: `/apps/installed?error=${JSON.stringify({ message: "No Stripe Checkout Session ID" })}` }; + } + + const { url } = await withStripeCallback(checkoutId, slug, async ({ checkoutSession }) => { + const ctx = await createContext({ req, res }); + const caller = apiKeysRouter.createCaller(ctx); + + const apiKey = await caller.create({ + note: "Cal.ai", + expiresAt: null, + appId: "cal-ai", + }); + + await checkInstalled(slug, session.user.id); + await createDefaultInstallation({ + appType, + userId: session.user.id, + slug, + key: { + apiKey, + }, + subscriptionId: checkoutSession.subscription?.toString(), + billingCycleStart: new Date().getDate(), + paymentStatus: "active", + }); + + await fetch( + `${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: session.user.id, + }), + } + ); + + return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) }; + }); + + return res.redirect(url); +} + +export default defaultResponder(getHandler); diff --git a/packages/app-store/cal-ai/api/callback.ts b/packages/app-store/cal-ai/api/callback.ts new file mode 100644 index 0000000000..07586cf85f --- /dev/null +++ b/packages/app-store/cal-ai/api/callback.ts @@ -0,0 +1,5 @@ +import { defaultHandler } from "@calcom/lib/server"; + +export default defaultHandler({ + GET: import("./_getCallback"), +}); diff --git a/packages/app-store/cal-ai/api/index.ts b/packages/app-store/cal-ai/api/index.ts index 4c0d2ead01..eb12c1b4ed 100644 --- a/packages/app-store/cal-ai/api/index.ts +++ b/packages/app-store/cal-ai/api/index.ts @@ -1 +1,2 @@ export { default as add } from "./add"; +export { default as callback } from "./callback"; diff --git a/packages/app-store/cal-ai/config.json b/packages/app-store/cal-ai/config.json index b371cc1e13..e6718b7b5d 100644 --- a/packages/app-store/cal-ai/config.json +++ b/packages/app-store/cal-ai/config.json @@ -3,7 +3,6 @@ "name": "Cal.ai", "slug": "cal-ai", "type": "cal-ai_automation", - "trial": 14, "logo": "icon.png", "url": "https://cal.ai", "variant": "automation", @@ -14,5 +13,10 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "basic", - "dirName": "cal-ai" + "dirName": "cal-ai", + "paid": { + "priceInUsd": 25, + "priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP", + "mode": "subscription" + } } diff --git a/packages/app-store/package.json b/packages/app-store/package.json index c4185db134..6cfd20e06a 100644 --- a/packages/app-store/package.json +++ b/packages/app-store/package.json @@ -25,7 +25,8 @@ "@calcom/zoomvideo": "*", "lodash": "^4.17.21", "qs-stringify": "^1.2.1", - "react-i18next": "^12.2.0" + "react-i18next": "^12.2.0", + "stripe": "^14.3.0" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index aaacb56292..a358f9d03a 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -142,10 +142,19 @@ export function getAppFromLocationValue(type: string): AppMeta | undefined { * @param concurrentMeetings - from app metadata * @returns - true if app supports team install */ -export function doesAppSupportTeamInstall( - appCategories: string[], - concurrentMeetings: boolean | undefined = undefined -) { +export function doesAppSupportTeamInstall({ + appCategories, + concurrentMeetings = undefined, + isPaid, +}: { + appCategories: string[]; + concurrentMeetings: boolean | undefined; + isPaid: boolean; +}) { + // Paid apps can't be installed on team level - That isn't supported + if (isPaid) { + return false; + } return !appCategories.some( (category) => category === "calendar" || diff --git a/packages/features/tips/Tips.tsx b/packages/features/tips/Tips.tsx index 9e707bb29b..e0e3a14fac 100644 --- a/packages/features/tips/Tips.tsx +++ b/packages/features/tips/Tips.tsx @@ -95,12 +95,13 @@ export const tips = [ }, { id: 12, - thumbnailUrl: "https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2", + thumbnailUrl: + "https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2", mediaLink: "https://go.cal.com/cal-ai", title: "Cal.ai", description: "Your personal AI scheduling assistant", href: "https://go.cal.com/cal-ai", - } + }, ]; const reversedTips = tips.slice(0).reverse(); diff --git a/packages/lib/piiFreeData.ts b/packages/lib/piiFreeData.ts index 6953a52477..dfa346f63f 100644 --- a/packages/lib/piiFreeData.ts +++ b/packages/lib/piiFreeData.ts @@ -105,7 +105,7 @@ export function getPiiFreeUser(user: { allowDynamicBooking?: boolean | null; defaultScheduleId?: number | null; organizationId?: number | null; - credentials?: Credential[]; + credentials?: Partial[]; destinationCalendar?: DestinationCalendar | null; }) { return { diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts index 7279f9fc97..081be54ebe 100644 --- a/packages/lib/server/getServerErrorFromUnknown.ts +++ b/packages/lib/server/getServerErrorFromUnknown.ts @@ -55,6 +55,7 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError { const redactedCause = redactError(cause); return { ...redactedCause, + message: redactedCause.message, cause: cause.cause, url: cause.url, statusCode: cause.statusCode, diff --git a/packages/prisma/migrations/20231110122349_paid_apps/migration.sql b/packages/prisma/migrations/20231110122349_paid_apps/migration.sql new file mode 100644 index 0000000000..5ba5814c67 --- /dev/null +++ b/packages/prisma/migrations/20231110122349_paid_apps/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "Credential" ADD COLUMN "billingCycleStart" INTEGER, +ADD COLUMN "paymentStatus" TEXT, +ADD COLUMN "subscriptionId" TEXT; + +-- CreateIndex +CREATE INDEX "Credential_subscriptionId_idx" ON "Credential"("subscriptionId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index ad21ba81bd..c9c2bf6596 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -126,17 +126,23 @@ model EventType { } model Credential { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) // @@type is deprecated - type String - key Json - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) + type String + key Json + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int? + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId Int? + app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) // How to make it a required column? - appId String? + appId String? + + // paid apps + subscriptionId String? + paymentStatus String? + billingCycleStart Int? + destinationCalendars DestinationCalendar[] selectedCalendars SelectedCalendar[] invalid Boolean? @default(false) @@ -144,6 +150,7 @@ model Credential { @@index([userId]) @@index([appId]) + @@index([subscriptionId]) } enum IdentityProvider { diff --git a/packages/types/App.d.ts b/packages/types/App.d.ts index 32839135e8..60f6b3308b 100644 --- a/packages/types/App.d.ts +++ b/packages/types/App.d.ts @@ -30,6 +30,13 @@ type DynamicLinkBasedEventLocation = { export type EventLocationTypeFromAppMeta = StaticLinkBasedEventLocation | DynamicLinkBasedEventLocation; +type PaidAppData = { + priceInUsd: number; + priceId: string; + trial?: number; + mode?: "subscription" | "one_time"; +}; + type AppData = { /** * TODO: We must assert that if `location` is set in App config.json, then it must have atleast Messaging or Conferencing as a category. @@ -142,6 +149,9 @@ export interface App { upgradeUrl: string; }; appData?: AppData; + /** Represents paid app data, such as price, trials, etc */ + paid?: PaidAppData; + /** * @deprecated * Used only by legacy apps which had slug different from their directory name. diff --git a/packages/ui/components/apps/AppCard.tsx b/packages/ui/components/apps/AppCard.tsx index 04ff2ad44f..f692435b90 100644 --- a/packages/ui/components/apps/AppCard.tsx +++ b/packages/ui/components/apps/AppCard.tsx @@ -39,8 +39,11 @@ export function AppCard({ app, credentials, searchText, userAdminTeams }: AppCar const { t } = useLocale(); const allowedMultipleInstalls = app.categories && app.categories.indexOf("calendar") > -1; const appAdded = (credentials && credentials.length) || 0; - - const enabledOnTeams = doesAppSupportTeamInstall(app.categories, app.concurrentMeetings); + const enabledOnTeams = doesAppSupportTeamInstall({ + appCategories: app.categories, + concurrentMeetings: app.concurrentMeetings, + isPaid: !!app.paid, + }); const appInstalled = enabledOnTeams && userAdminTeams ? userAdminTeams.length < appAdded : appAdded > 0; @@ -120,6 +123,7 @@ export function AppCard({ app, credentials, searchText, userAdminTeams }: AppCar addAppMutationInput={{ type: app.type, variant: app.variant, slug: app.slug }} appCategories={app.categories} concurrentMeetings={app.concurrentMeetings} + paid={app.paid} /> ); }} @@ -146,6 +150,7 @@ export function AppCard({ app, credentials, searchText, userAdminTeams }: AppCar appCategories={app.categories} credentials={credentials} concurrentMeetings={app.concurrentMeetings} + paid={app.paid} {...props} /> ); @@ -174,6 +179,7 @@ const InstallAppButtonChild = ({ appCategories, credentials, concurrentMeetings, + paid, ...props }: { userAdminTeams?: UserAdminTeams; @@ -181,6 +187,7 @@ const InstallAppButtonChild = ({ appCategories: string[]; credentials?: Credential[]; concurrentMeetings?: boolean; + paid: App["paid"]; } & ButtonProps) => { const { t } = useLocale(); const router = useRouter(); @@ -200,7 +207,25 @@ const InstallAppButtonChild = ({ }, }); - if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) { + // Paid apps don't support team installs at the moment + // Also, cal.ai(the only paid app at the moment) doesn't support team install either + if (paid) { + return ( + + ); + } + + if ( + !userAdminTeams?.length || + !doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid }) + ) { return (
@@ -438,16 +444,19 @@ const getError = ( bookingMutation: UseMutationResult, // eslint-disable-next-line @typescript-eslint/no-explicit-any recurringBookingMutation: UseMutationResult, - t: TFunction + t: TFunction, + responseVercelIdHeader: string | null ) => { if (globalError) return globalError.message; const error = bookingMutation.error || recurringBookingMutation.error; - return error instanceof HttpError || error instanceof Error ? ( - <>{t("can_you_try_again")} + return error.message ? ( + <> + {responseVercelIdHeader ?? ""} {t(error.message)} + ) : ( - "Unknown error" + <>{t("can_you_try_again")} ); }; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index a8d9c5dbe2..8328fd26ef 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -6,6 +6,7 @@ import { isValidPhoneNumber } from "libphonenumber-js"; import { cloneDeep } from "lodash"; import type { NextApiRequest } from "next"; import short, { uuid } from "short-uuid"; +import type { Logger } from "tslog"; import { v5 as uuidv5 } from "uuid"; import z from "zod"; @@ -52,6 +53,7 @@ import { cancelScheduledJobs, scheduleTrigger } from "@calcom/features/webhooks/ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; @@ -362,7 +364,8 @@ async function ensureAvailableUsers( eventType: Awaited> & { users: IsFixedAwareUser[]; }, - input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType } + input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }, + loggerWithEventDetails: Logger ) { const availableUsers: IsFixedAwareUser[] = []; const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute"); @@ -433,7 +436,8 @@ async function ensureAvailableUsers( } } if (!availableUsers.length) { - throw new Error("No available users found."); + loggerWithEventDetails.error(`No available users found.`); + throw new Error(ErrorCode.NoAvailableUsersFound); } return availableUsers; } @@ -556,7 +560,7 @@ async function getBookingData({ return true; }; if (!reqBodyWithEnd(reqBody)) { - throw new Error("Internal Error."); + throw new Error(ErrorCode.RequestBodyWithouEnd); } // reqBody.end is no longer an optional property. if ("customInputs" in reqBody) { @@ -691,10 +695,11 @@ async function handler( const fullName = getFullName(bookerName); + // Why are we only using "en" locale const tGuests = await getTranslation("en", "common"); const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user); - if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" }); + if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" }); const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); @@ -935,7 +940,8 @@ async function handler( dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(), timeZone: reqBody.timeZone, originalRescheduledBooking, - } + }, + loggerWithEventDetails ); const luckyUsers: typeof users = []; @@ -965,7 +971,7 @@ async function handler( if ( availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length ) { - throw new Error("Some of the hosts are unavailable for booking."); + throw new Error(ErrorCode.HostsUnavailableForBooking); } // Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer. users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers]; @@ -1283,7 +1289,7 @@ async function handler( booking.attendees.find((attendee) => attendee.email === invitee[0].email) && dayjs.utc(booking.startTime).format() === evt.startTime ) { - throw new HttpError({ statusCode: 409, message: "Already signed up for this booking." }); + throw new HttpError({ statusCode: 409, message: ErrorCode.AlreadySignedUpForBooking }); } // There are two paths here, reschedule a booking with seats and booking seats without reschedule @@ -2694,7 +2700,7 @@ const findBookingQuery = async (bookingId: number) => { // This should never happen but it's just typescript safe if (!foundBooking) { - throw new Error("Internal Error."); + throw new Error("Internal Error. Couldn't find booking"); } // Don't leak any sensitive data diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index fbc0291e3a..c893929b9a 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -13,6 +13,7 @@ import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { resetTestEmails } from "@calcom/lib/testEmails"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -1024,7 +1025,7 @@ describe("handleNewBooking", () => { }); await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "No available users found" + ErrorCode.NoAvailableUsersFound ); }, timeout @@ -1111,7 +1112,7 @@ describe("handleNewBooking", () => { }); await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "No available users found" + ErrorCode.NoAvailableUsersFound ); }, timeout @@ -1239,7 +1240,7 @@ describe("handleNewBooking", () => { * NOTE: We might want to think about making the bookings get ACCEPTED automatically if the booker is the organizer of the event-type. This is a design decision it seems for now. */ test( - `should make a fresh booking in PENDING state even when the booker is the organizer of the event-type + `should make a fresh booking in PENDING state even when the booker is the organizer of the event-type 1. Should create a booking in the database with status PENDING 2. Should send emails to the booker as well as organizer for booking request and awaiting approval 3. Should trigger BOOKING_REQUESTED webhook diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index 68c1ba52a0..04f7e72266 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid"; import { describe, expect } from "vitest"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -368,7 +369,7 @@ describe("handleNewBooking", () => { }), }); - expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow("No available users found"); + expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow(ErrorCode.NoAvailableUsersFound); // Actually the first booking goes through in this case but the status is still a failure. We should do a dry run to check if booking is possible for the 2 slots and if yes, then only go for the actual booking otherwise fail the recurring bookign }, timeout diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index 386c910e5f..eb59eed52e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -4,6 +4,7 @@ import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -353,7 +354,7 @@ describe("handleNewBooking", () => { await expect(async () => { await handleNewBooking(req); - }).rejects.toThrowError("Some of the hosts are unavailable for booking"); + }).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking); }, timeout ); @@ -666,7 +667,7 @@ describe("handleNewBooking", () => { await expect(async () => { await handleNewBooking(req); - }).rejects.toThrowError("No available users found."); + }).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); diff --git a/packages/lib/errorCodes.ts b/packages/lib/errorCodes.ts new file mode 100644 index 0000000000..07c51ae693 --- /dev/null +++ b/packages/lib/errorCodes.ts @@ -0,0 +1,9 @@ +export enum ErrorCode { + PaymentCreationFailure = "payment_not_created_error", + NoAvailableUsersFound = "no_available_users_found_error", + ChargeCardFailure = "couldnt_charge_card_error", + RequestBodyWithouEnd = "request_body_end_time_internal_error", + AlreadySignedUpForBooking = "already_signed_up_for_this_booking_error", + HostsUnavailableForBooking = "hosts_unavailable_for_booking", + EventTypeNotFound = "event_type_not_found_error", +} From eb97e1660b3288f7825c3f079ffeef19de7b78b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 15 Nov 2023 14:48:10 -0700 Subject: [PATCH 058/119] v3.5.0 --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 26755e584b..d9ad6f022d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.4.10", + "version": "3.5.0", "private": true, "scripts": { "analyze": "ANALYZE=true next build", From 6a8726f5f8f7d7467765ec939d06d827801b19ac Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 15 Nov 2023 18:50:20 -0300 Subject: [PATCH 059/119] chore: Insights readonly DB client (#12373) --- .env.example | 1 + apps/web/server/lib/ssg.ts | 3 +- packages/features/insights/server/events.ts | 2 +- .../features/insights/server/trpc-router.ts | 66 +++++++++---------- packages/prisma/index.ts | 8 +++ packages/trpc/server/createContext.ts | 3 +- turbo.json | 1 + 7 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 1cc4f37308..46237514b5 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,7 @@ CALCOM_LICENSE_KEY= DATABASE_URL="postgresql://postgres:@localhost:5450/calendso" UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= +INSIGHTS_DATABASE_URL= # Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy # Cold boots will be faster and you'll be able to scale your DB independently of your app. diff --git a/apps/web/server/lib/ssg.ts b/apps/web/server/lib/ssg.ts index 469653da97..d08313d859 100644 --- a/apps/web/server/lib/ssg.ts +++ b/apps/web/server/lib/ssg.ts @@ -3,7 +3,7 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import superjson from "superjson"; import { CALCOM_VERSION } from "@calcom/lib/constants"; -import prisma from "@calcom/prisma"; +import prisma, { readonlyPrisma } from "@calcom/prisma"; import { createProxySSGHelpers } from "@calcom/trpc/react/ssg"; import { appRouter } from "@calcom/trpc/server/routers/_app"; @@ -31,6 +31,7 @@ export async function ssgInit(opts: GetStat transformer: superjson, ctx: { prisma, + insightsDb: readonlyPrisma, session: null, locale, i18n: _i18n, diff --git a/packages/features/insights/server/events.ts b/packages/features/insights/server/events.ts index b950dce3b9..fd6f040950 100644 --- a/packages/features/insights/server/events.ts +++ b/packages/features/insights/server/events.ts @@ -1,6 +1,6 @@ import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import { prisma } from "@calcom/prisma"; +import { readonlyPrisma as prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { RawDataInput } from "./raw-data.schema"; diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index d172c4e94a..30f461e2d2 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -35,7 +35,7 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, rawIn membershipWhereConditional["teamId"] = parse.data.teamId; } - const membership = await ctx.prisma.membership.findFirst({ + const membership = await ctx.insightsDb.membership.findFirst({ where: membershipWhereConditional, }); @@ -43,7 +43,7 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, rawIn // So that would mean ctx.user.organization is present if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) { //Look for membership type in organizationId - const membershipOrg = await ctx.prisma.membership.findFirst({ + const membershipOrg = await ctx.insightsDb.membership.findFirst({ where: { userId: ctx.user.id, teamId: ctx.user.organizationId, @@ -152,7 +152,7 @@ export const insightsRouter = router({ } if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) { - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: ctx.user.organizationId, }, @@ -168,7 +168,7 @@ export const insightsRouter = router({ in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)], }, }; - const usersFromOrg = await ctx.prisma.membership.findMany({ + const usersFromOrg = await ctx.insightsDb.membership.findMany({ where: { team: teamConditional, accepted: true, @@ -197,7 +197,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId: teamId, accepted: true, @@ -348,7 +348,7 @@ export const insightsRouter = router({ let whereConditional: Prisma.BookingTimeStatusWhereInput = {}; if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) { - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: user.organizationId, }, @@ -357,7 +357,7 @@ export const insightsRouter = router({ }, }); - const usersFromOrg = await ctx.prisma.membership.findMany({ + const usersFromOrg = await ctx.insightsDb.membership.findMany({ where: { teamId: { in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)], @@ -388,7 +388,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, @@ -537,7 +537,7 @@ export const insightsRouter = router({ }; if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) { - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: user.organizationId, }, @@ -546,7 +546,7 @@ export const insightsRouter = router({ }, }); - const usersFromOrg = await ctx.prisma.membership.findMany({ + const usersFromOrg = await ctx.insightsDb.membership.findMany({ where: { teamId: { in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)], @@ -578,7 +578,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, @@ -615,7 +615,7 @@ export const insightsRouter = router({ bookingWhere.userId = memberUserId; } - const bookingsFromSelected = await ctx.prisma.bookingTimeStatus.groupBy({ + const bookingsFromSelected = await ctx.insightsDb.bookingTimeStatus.groupBy({ by: ["eventTypeId"], where: bookingWhere, _count: { @@ -639,7 +639,7 @@ export const insightsRouter = router({ }, }; - const eventTypesFrom = await ctx.prisma.eventType.findMany({ + const eventTypesFrom = await ctx.insightsDb.eventType.findMany({ select: { id: true, title: true, @@ -752,7 +752,7 @@ export const insightsRouter = router({ } if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) { - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: ctx.user?.organizationId, }, @@ -777,7 +777,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, @@ -832,7 +832,7 @@ export const insightsRouter = router({ const startDate = dayjs(date).startOf(startOfEndOf); const endDate = dayjs(date).endOf(startOfEndOf); - const bookingsInTimeRange = await ctx.prisma.bookingTimeStatus.findMany({ + const bookingsInTimeRange = await ctx.insightsDb.bookingTimeStatus.findMany({ select: { eventLength: true, }, @@ -896,7 +896,7 @@ export const insightsRouter = router({ if (isAll && user.isOwnerAdminOfParentTeam && user.organizationId) { delete bookingWhere.teamId; - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: user?.organizationId, }, @@ -904,7 +904,7 @@ export const insightsRouter = router({ id: true, }, }); - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId: { in: [user?.organizationId, ...teamsFromOrg.map((t) => t.id)], @@ -931,7 +931,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, @@ -956,7 +956,7 @@ export const insightsRouter = router({ ]; } - const bookingsFromTeam = await ctx.prisma.bookingTimeStatus.groupBy({ + const bookingsFromTeam = await ctx.insightsDb.bookingTimeStatus.groupBy({ by: ["userId"], where: bookingWhere, _count: { @@ -977,7 +977,7 @@ export const insightsRouter = router({ return []; } - const usersFromTeam = await ctx.prisma.user.findMany({ + const usersFromTeam = await ctx.insightsDb.user.findMany({ where: { id: { in: userIds as number[], @@ -1030,7 +1030,7 @@ export const insightsRouter = router({ if (isAll && user.isOwnerAdminOfParentTeam) { delete bookingWhere.teamId; - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: user?.organizationId, }, @@ -1038,7 +1038,7 @@ export const insightsRouter = router({ id: true, }, }); - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId: { in: teamsFromOrg.map((t) => t.id), @@ -1066,7 +1066,7 @@ export const insightsRouter = router({ } if (teamId && !isAll) { - const usersFromTeam = await ctx.prisma.membership.findMany({ + const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, @@ -1089,7 +1089,7 @@ export const insightsRouter = router({ ]; } - const bookingsFromTeam = await ctx.prisma.bookingTimeStatus.groupBy({ + const bookingsFromTeam = await ctx.insightsDb.bookingTimeStatus.groupBy({ by: ["userId"], where: bookingWhere, _count: { @@ -1109,7 +1109,7 @@ export const insightsRouter = router({ if (userIds.length === 0) { return []; } - const usersFromTeam = await ctx.prisma.user.findMany({ + const usersFromTeam = await ctx.insightsDb.user.findMany({ where: { id: { in: userIds as number[], @@ -1138,7 +1138,7 @@ export const insightsRouter = router({ const user = ctx.user; // Fetch user data - const userData = await ctx.prisma.user.findUnique({ + const userData = await ctx.insightsDb.user.findUnique({ where: { id: user.id, }, @@ -1167,7 +1167,7 @@ export const insightsRouter = router({ // Validate if user belongs to org as admin/owner if (user.organizationId) { - const teamsFromOrg = await ctx.prisma.team.findMany({ + const teamsFromOrg = await ctx.insightsDb.team.findMany({ where: { parentId: user.organizationId, }, @@ -1178,7 +1178,7 @@ export const insightsRouter = router({ logo: true, }, }); - const orgTeam = await ctx.prisma.team.findUnique({ + const orgTeam = await ctx.insightsDb.team.findUnique({ where: { id: user.organizationId, }, @@ -1214,7 +1214,7 @@ export const insightsRouter = router({ } // Look if user it's admin/owner in multiple teams - const belongsToTeams = await ctx.prisma.membership.findMany({ + const belongsToTeams = await ctx.insightsDb.membership.findMany({ where: membershipConditional, include: { team: { @@ -1255,7 +1255,7 @@ export const insightsRouter = router({ } if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) { - const usersInTeam = await ctx.prisma.membership.findMany({ + const usersInTeam = await ctx.insightsDb.membership.findMany({ where: { team: { parentId: user.organizationId, @@ -1271,7 +1271,7 @@ export const insightsRouter = router({ return usersInTeam.map((membership) => membership.user); } - const membership = await ctx.prisma.membership.findFirst({ + const membership = await ctx.insightsDb.membership.findFirst({ where: { userId: user.id, teamId, @@ -1292,7 +1292,7 @@ export const insightsRouter = router({ return [membership.user]; } - const usersInTeam = await ctx.prisma.membership.findMany({ + const usersInTeam = await ctx.insightsDb.membership.findMany({ where: { teamId, accepted: true, diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index b045ff8fb6..3e7223675f 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -59,6 +59,14 @@ const prismaWithClientExtensions = prismaWithoutClientExtensions export const prisma = globalForPrisma.prismaWithClientExtensions || prismaWithClientExtensions; +// This prisma instance is meant to be used only for READ operations. +// If self hosting, feel free to leave INSIGHTS_DATABASE_URL as empty and `readonlyPrisma` will default to `prisma`. +export const readonlyPrisma = process.env.INSIGHTS_DATABASE_URL + ? customPrisma({ + datasources: { db: { url: process.env.INSIGHTS_DATABASE_URL } }, + }) + : prisma; + if (process.env.NODE_ENV !== "production") { globalForPrisma.prismaWithoutClientExtensions = prismaWithoutClientExtensions; globalForPrisma.prismaWithClientExtensions = prisma; diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 3bca09f285..042fc4c738 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -4,7 +4,7 @@ import type { Session } from "next-auth"; import type { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { getLocale } from "@calcom/features/auth/lib/getLocale"; -import prisma from "@calcom/prisma"; +import prisma, { readonlyPrisma } from "@calcom/prisma"; import type { SelectedCalendar, User as PrismaUser } from "@calcom/prisma/client"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; @@ -53,6 +53,7 @@ export type GetSessionFn = export async function createContextInner(opts: CreateInnerContextOptions) { return { prisma, + insightsDb: readonlyPrisma, ...opts, }; } diff --git a/turbo.json b/turbo.json index 2b171f6723..1156aa13e5 100644 --- a/turbo.json +++ b/turbo.json @@ -247,6 +247,7 @@ "INTEGRATION_TEST_MODE", "INTERCOM_SECRET", "INTERCOM_SECRET", + "INSIGHTS_DATABASE_URL", "IP_BANLIST", "LARK_OPEN_APP_ID", "LARK_OPEN_APP_SECRET", From ea0a64624cad35bf4c72f8e5abe74f93893dfcd0 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:59:43 -0500 Subject: [PATCH 060/119] fix: saving credential id for payment apps (#12251) ## What does this PR do? This PR adds the `credentialId` to payment app data. This fixes a bug where team installed payment apps were not working with team events. Fixes # (issue) ## Requirement/Documentation - If there is a requirement document, please, share it here. - If there is ab UI/UX design document, please, share it here. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How should this be tested? - Install Stripe for the individual user - Enable it for the individual's event type - The `credentialId` should be saved to the metadata - Install Stripe to the user's team - Enable it in the team's event type - The `credentialId` should be saved to the metadata ## Mandatory Tasks - [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected. ## Checklist - I haven't checked if new and existing unit tests pass locally with my changes --- .../web/components/eventtype/EventAppsTab.tsx | 9 +- apps/web/playwright/fixtures/users.ts | 2 +- .../web/playwright/integrations-stripe.e2e.ts | 93 +++++++++++++++++++ packages/app-store/_components/AppCard.tsx | 1 + packages/app-store/alby/zod.ts | 1 + 5 files changed, 101 insertions(+), 5 deletions(-) diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 72e041744f..6c96d701cd 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { }; }; - const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => { + const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => { return function (key, value) { // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render) const allAppsDataFromForm = methods.getValues("metadata")?.apps || {}; @@ -57,6 +57,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { [appId]: { ...appData, [key]: value, + credentialId, }, }); }; @@ -76,7 +77,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { appCards.push( { appCards.push( { return ( , page: Page) { await page.goto(`/event-types/${eventType?.id}?tabName=apps`); - await page.locator("div > .ml-auto").first().click(); + await page.locator("[data-testid='app-switch']").first().click(); await page.getByPlaceholder("Price").fill("100"); await page.getByTestId("update-eventtype").click(); } diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index cd67112bc7..25a1a33fa6 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -1,6 +1,10 @@ import { expect } from "@playwright/test"; import type Prisma from "@prisma/client"; +import prisma from "@calcom/prisma"; +import { SchedulingType } from "@calcom/prisma/enums"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + import { test } from "./lib/fixtures"; import type { Fixtures } from "./lib/fixtures"; import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; @@ -34,6 +38,95 @@ test.describe("Stripe integration", () => { }); }); + test("when enabling Stripe, credentialId is included", async ({ page, users }) => { + const user = await users.create(); + await user.apiLogin(); + await page.goto("/apps/installed"); + + await user.getPaymentCredential(); + + const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; + await user.setupEventWithPrice(eventType); + + // Need to wait for the DB to be updated with the metadata + await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); + + // Check event type metadata to see if credentialId is included + const eventTypeMetadata = await prisma.eventType.findFirst({ + where: { + id: eventType.id, + }, + select: { + metadata: true, + }, + }); + + const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata); + + const stripeAppMetadata = metadata?.apps?.stripe; + + expect(stripeAppMetadata).toHaveProperty("credentialId"); + expect(typeof stripeAppMetadata?.credentialId).toBe("number"); + }); + + test("when enabling Stripe, team credentialId is included", async ({ page, users }) => { + const ownerObj = { username: "pro-user", name: "pro-user" }; + const teamMatesObj = [ + { name: "teammate-1" }, + { name: "teammate-2" }, + { name: "teammate-3" }, + { name: "teammate-4" }, + ]; + + const owner = await users.create(ownerObj, { + hasTeam: true, + teammates: teamMatesObj, + schedulingType: SchedulingType.COLLECTIVE, + }); + await owner.apiLogin(); + const { team } = await owner.getFirstTeam(); + const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id); + + const teamEvent = await owner.getFirstTeamEvent(team.id); + + await page.goto("/apps/stripe"); + + /** We start the Stripe flow */ + await Promise.all([ + page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"), + page.click('[data-testid="install-app-button"]'), + page.click('[data-testid="anything else"]'), + ]); + + await Promise.all([ + page.waitForURL("/apps/installed/payment?hl=stripe"), + /** We skip filling Stripe forms (testing mode only) */ + page.click('[id="skip-account-app"]'), + ]); + + await owner.setupEventWithPrice(teamEvent); + + // Need to wait for the DB to be updated with the metadata + await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200); + + // Check event type metadata to see if credentialId is included + const eventTypeMetadata = await prisma.eventType.findFirst({ + where: { + id: teamEvent.id, + }, + select: { + metadata: true, + }, + }); + + const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata); + + const stripeAppMetadata = metadata?.apps?.stripe; + + expect(stripeAppMetadata).toHaveProperty("credentialId"); + expect(typeof stripeAppMetadata?.credentialId).toBe("number"); + }); + test("Can book a paid booking", async ({ page, users }) => { const user = await users.create(); const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; diff --git a/packages/app-store/_components/AppCard.tsx b/packages/app-store/_components/AppCard.tsx index cf06eeba10..e946aab684 100644 --- a/packages/app-store/_components/AppCard.tsx +++ b/packages/app-store/_components/AppCard.tsx @@ -90,6 +90,7 @@ export default function AppCard({ {app?.isInstalled || app.credentialOwner ? (
{ if (switchOnClick) { diff --git a/packages/app-store/alby/zod.ts b/packages/app-store/alby/zod.ts index 0983fe0ce7..249451dc77 100644 --- a/packages/app-store/alby/zod.ts +++ b/packages/app-store/alby/zod.ts @@ -29,6 +29,7 @@ export const appDataSchema = eventTypeAppCardZod.merge( currency: z.string(), paymentOption: z.string().optional(), enabled: z.boolean().optional(), + credentialId: z.number().optional(), }) ); export const appKeysSchema = z.object({ From 0a39f53a4bde352a88176c7e1de2856933ef67bd Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:48:46 +0200 Subject: [PATCH 061/119] fix(stripePaymentCallback): better errors (#12223) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Keith Williams --- apps/web/pages/api/integrations/[...args].ts | 3 ++- .../stripepayment/api/paymentCallback.ts | 21 ++++++++++++++----- packages/lib/server/defaultResponder.ts | 7 ++++--- .../lib/server/getServerErrorFromUnknown.ts | 3 ++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/web/pages/api/integrations/[...args].ts b/apps/web/pages/api/integrations/[...args].ts index ea1b68dbf5..f4e840b021 100644 --- a/apps/web/pages/api/integrations/[...args].ts +++ b/apps/web/pages/api/integrations/[...args].ts @@ -73,7 +73,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { redirectUrl = handler.redirect?.url || getInstalledAppPath(handler); res.json({ url: redirectUrl, newTab: handler.redirect?.newTab }); } - return res.status(200); + if (!res.writableEnded) return res.status(200); + return res; } catch (error) { console.error(error); if (error instanceof HttpError) { diff --git a/packages/app-store/stripepayment/api/paymentCallback.ts b/packages/app-store/stripepayment/api/paymentCallback.ts index e10df2d0bc..a9f4d0934c 100644 --- a/packages/app-store/stripepayment/api/paymentCallback.ts +++ b/packages/app-store/stripepayment/api/paymentCallback.ts @@ -3,6 +3,7 @@ import z from "zod"; import { getCustomerAndCheckoutSession } from "@calcom/app-store/stripepayment/lib/getCustomerAndCheckoutSession"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; @@ -22,7 +23,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const { callbackUrl, checkoutSessionId } = querySchema.parse(req.query); const { stripeCustomer, checkoutSession } = await getCustomerAndCheckoutSession(checkoutSessionId); - if (!stripeCustomer) return { message: "Stripe customer not found or deleted" }; + if (!stripeCustomer) + throw new HttpError({ + statusCode: 404, + message: "Stripe customer not found or deleted", + url: req.url, + method: req.method, + }); // first let's try to find user by metadata stripeCustomerId let user = await prisma.user.findFirst({ @@ -43,10 +50,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { }); } + if (!user) + throw new HttpError({ statusCode: 404, message: "User not found", url: req.url, method: req.method }); + if (checkoutSession.payment_status === "paid" && stripeCustomer.metadata.username) { try { - if (!user) return { message: "User not found" }; - await prisma.user.update({ data: { username: stripeCustomer.metadata.username, @@ -61,10 +69,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { }); } catch (error) { console.error(error); - return { + throw new HttpError({ + statusCode: 400, + url: req.url, + method: req.method, message: "We have received your payment. Your premium username could still not be reserved. Please contact support@cal.com and mention your premium username", - }; + }); } } callbackUrl.searchParams.set("paymentStatus", checkoutSession.payment_status); diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index b70f99cd04..e044354e8d 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -14,13 +14,14 @@ export function defaultResponder(f: Handle) { const result = await f(req, res); ok = true; if (result && !res.writableEnded) { - res.json(result); + return res.json(result); } } catch (err) { console.error(err); const error = getServerErrorFromUnknown(err); - res.statusCode = error.statusCode; - res.json({ message: error.message }); + return res + .status(error.statusCode) + .json({ message: error.message, url: error.url, method: error.method }); } finally { performance.mark("End"); performance.measure(`[${ok ? "OK" : "ERROR"}][$1] ${req.method} '${req.url}'`, "Start", "End"); diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts index 081be54ebe..a1118be888 100644 --- a/packages/lib/server/getServerErrorFromUnknown.ts +++ b/packages/lib/server/getServerErrorFromUnknown.ts @@ -55,7 +55,8 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError { const redactedCause = redactError(cause); return { ...redactedCause, - message: redactedCause.message, + name: cause.name, + message: cause.message ?? "", cause: cause.cause, url: cause.url, statusCode: cause.statusCode, From f687056d60639d0456d53e9b86667966523add9a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 16 Nov 2023 08:51:33 +0000 Subject: [PATCH 062/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/ja/common.json | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 7e40f52890..09b9705e4c 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -268,6 +268,7 @@ "set_availability": "利用可否の設定", "availability_settings": "利用できる設定", "continue_without_calendar": "カレンダーなしで続行", + "continue_with": "{{appName}} で続行", "connect_your_calendar": "カレンダーに接続", "connect_your_video_app": "ビデオアプリを接続", "connect_your_video_app_instructions": "ご利用のイベントの種類で動画アプリを使用するには、ビデオアプリを接続してください。", @@ -288,6 +289,8 @@ "when": "日時", "where": "参加方法", "add_to_calendar": "カレンダーに追加", + "add_to_calendar_description": "予約時にイベントを追加する場所を選びます。", + "add_events_to": "イベントの追加先", "add_another_calendar": "別のカレンダーを追加", "other": "その他", "email_sign_in_subject": "{{appName}} のサインインリンク", @@ -422,6 +425,7 @@ "booking_created": "予約を作成しました", "booking_rejected": "予約が拒否されました", "booking_requested": "予約がリクエストされました", + "booking_payment_initiated": "予約に関するお支払いを開始しました", "meeting_ended": "ミーティングが終了しました", "form_submitted": "フォームが送信されました", "booking_paid": "予約の支払いが済みました", @@ -456,6 +460,7 @@ "no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。", "edit_logo": "ロゴを編集", "upload_a_logo": "ロゴをアップロード", + "upload_logo": "ロゴをアップロード", "remove_logo": "ロゴを削除する", "enable": "有効にする", "code": "コード", @@ -568,6 +573,7 @@ "your_team_name": "チーム名", "team_updated_successfully": "チームが正常に更新されました", "your_team_updated_successfully": "チームが正常に更新されました。", + "your_org_updated_successfully": "組織は正常に更新されました。", "about": "このアプリについて", "team_description": "あなたのチームについての簡単な説明文です。あなたのチームの URL ページに表示されます。", "org_description": "あなたの組織についての簡単な説明文です。あなたの組織の URL ページに表示されます。", @@ -599,6 +605,7 @@ "hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする", "hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。", "danger_zone": "危険ゾーン", + "account_deletion_cannot_be_undone": "ご注意ください。アカウントの削除は元に戻せません。", "back": "戻る", "cancel": "キャンセル", "cancel_all_remaining": "残りをすべてキャンセル", @@ -688,6 +695,7 @@ "people": "ユーザー", "your_email": "あなたのメールアドレス", "change_avatar": "アバターを変更", + "upload_avatar": "アバターをアップロード", "language": "言語", "timezone": "タイムゾーン", "first_day_of_week": "週の最初の日", @@ -778,6 +786,7 @@ "disable_guests": "ゲストを無効化", "disable_guests_description": "予約中のゲストの追加を無効にします。", "private_link": "プライベートリンクを生成", + "enable_private_url": "プライベート URL を有効にする", "private_link_label": "プライベートリンク", "private_link_hint": "プライベートリンクは使用するたびに再生成されます", "copy_private_link": "プライベートリンクをコピー", @@ -1214,6 +1223,7 @@ "organizer_name_variable": "主催者名", "app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。", "invalid_number": "電話番号が無効です", + "invalid_url_error_message": "{{label}} の無効な URL です。サンプル URL:{{sampleUrl}}", "navigate": "ナビゲート", "open": "開く", "close": "閉じる", @@ -1277,6 +1287,7 @@ "personal_cal_url": "私の個人 {{appName}} URL", "bio_hint": "あなたに関する簡潔な説明。これはあなたの個人 URL ページに表示されます。", "user_has_no_bio": "このユーザーはまだ経歴を追加していません。", + "bio": "経歴", "delete_account_modal_title": "アカウントを削除する", "confirm_delete_account_modal": "{{appName}} アカウントを削除してもよろしいですか?", "delete_my_account": "アカウントを削除する", @@ -1287,6 +1298,7 @@ "select_calendars": "ダブルブッキングを防ぐために、スケジュールの重なりをチェックするカレンダーを選択してください。", "check_for_conflicts": "スケジュールの重なりをチェック", "view_recordings": "録音を表示", + "check_for_recordings": "レコーディングを確認", "adding_events_to": "イベントの追加先", "follow_system_preferences": "システム環境設定に従う", "custom_brand_colors": "カスタムブランドカラー", @@ -1531,6 +1543,7 @@ "problem_registering_domain": "サブドメインの登録時に問題が発生しました。もう一度お試しいただくか、管理者までお問い合せください", "team_publish": "チームを公開", "number_text_notifications": "電話番号 (テキスト通知)", + "number_sms_notifications": "電話番号(SMS 通知)", "attendee_email_variable": "出席者のメールアドレス", "attendee_email_info": "予約者のメールアドレス", "kbar_search_placeholder": "コマンドを入力するか、検索してください...", @@ -1595,6 +1608,7 @@ "options": "オプション", "enter_option": "オプション {{index}} を入力してください", "add_an_option": "オプションを追加", + "location_already_exists": "この場所はすでに存在します。新しい場所を選んでください", "radio": "ラジオボタン", "google_meet_warning": "Google Meet を使用するには、目的のカレンダーを Google カレンダーに設定する必要があります", "individual": "個人", @@ -1614,6 +1628,7 @@ "date_overrides_mark_all_day_unavailable_other": "選択した日付を参加不可としてマーク", "date_overrides_add_btn": "上書きを追加", "date_overrides_update_btn": "上書きを更新", + "date_successfully_added": "日付の上書きが正常に追加されました", "event_type_duplicate_copy_text": "{{slug}}-copy", "set_as_default": "デフォルトとして設定", "hide_eventtype_details": "イベントの種類の詳細を非表示にする", @@ -1640,6 +1655,7 @@ "minimum_round_robin_hosts_count": "出席が必要なホストの数", "hosts": "ホスト", "upgrade_to_enable_feature": "この機能を有効にするには、チームを作成する必要があります。クリックしてチームを作成してください。", + "orgs_upgrade_to_enable_feature": "この機能を有効にするには Enterprise プランにアップグレードする必要があります。", "new_attendee": "新規参加者", "awaiting_approval": "承認を待っています", "requires_google_calendar": "このアプリは Google カレンダーとの接続が必要です", @@ -1744,6 +1760,7 @@ "show_on_booking_page": "予約ページに表示", "get_started_zapier_templates": "Zapier テンプレートの使用を開始する", "team_is_unpublished": "{{team}} は公開されていません", + "org_is_unpublished_description": "この組織のリンクは現在利用できません。組織の所有者に連絡するか、リンクを公開するよう依頼してください。", "team_is_unpublished_description": "この {{entity}} のリンクは現在利用できません。{{entity}} の所有者に問い合わせるか、リンクを公開するように依頼してください。", "team_member": "チームメンバー", "a_routing_form": "ルーティングフォーム", @@ -1878,6 +1895,7 @@ "edit_invite_link": "リンクの設定を編集する", "invite_link_copied": "招待リンクをコピーしました", "invite_link_deleted": "招待リンクを削除しました", + "api_key_deleted": "API キーを削除しました", "invite_link_updated": "招待リンクの設定を保存しました", "link_expires_after": "リンクの期限切れまで...", "one_day": "1 日", @@ -2010,7 +2028,13 @@ "attendee_last_name_variable": "出席者の姓", "attendee_first_name_info": "予約者の名", "attendee_last_name_info": "予約者の姓", + "your_monthly_digest": "月 1 回のダイジェスト", + "member_name": "メンバーの名前", + "most_popular_events": "最も人気のイベント", + "summary_of_events_for_your_team_for_the_last_30_days": "こちらはこの 30 日間、チーム {{teamName}} で最も人気があったイベントのサマリーです", "me": "私", + "monthly_digest_email": "月 1 回のダイジェストのメール", + "monthly_digest_email_for_teams": "チームのための月 1 回のダイジェストのメール", "verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください", "member_removed": "メンバーが削除されました", "my_availability": "私の空き状況", @@ -2040,13 +2064,41 @@ "team_no_event_types": "このチームにはイベントタイプはありません", "seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません", "include_calendar_event": "カレンダーのイベントを含める", + "oAuth": "OAuth", "recently_added": "最近追加されました", "no_members_found": "メンバーが見つかりません", "event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。", "availability_schedules": "空き状況一覧", + "unauthorized": "権限がありません", + "access_cal_account": "{{clientName}}があなたの {{appName}} アカウントへのアクセスを求めています", + "select_account_team": "アカウントまたはチームを選択", + "allow_client_to": "これにより {{clientName}} は", + "associate_with_cal_account": "あなたと {{clientName}} からのあなたの個人情報を関連づけられます", + "see_personal_info": "あなたの個人情報(あなたがこれまでに公開したあなたの個人情報など)を表示", + "see_primary_email_address": "プライマリメールアドレスを表示", + "connect_installed_apps": "インストールしたアプリに接続", + "access_event_type": "イベントタイプを読み取り、編集し、削除する", + "access_availability": "空き状況を読み取り、編集し、削除する", + "access_bookings": "予約を読み取り、編集し、削除する", + "allow_client_to_do": "{{clientName}} にこれの実行を許可しますか?", + "oauth_access_information": "「許可」をクリックすると、このアプリのサービス利用規約とプライバシーポリシーに従って、アプリにあなたの個人情報の使用を許可することになります。{{appName}} の App Store でアクセスを削除できます。", + "allow": "許可", "view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。", "view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。", + "you_can_override_calendar_in_advanced_tab": "各イベントタイプの詳細設定で、イベントごとにこれを上書きすることができます。", "edit_users_availability": "ユーザーの空き状況を編集:{{username}}", + "resend_invitation": "招待を再送", + "invitation_resent": "招待は再送されました。", + "add_client": "顧客を追加", + "copy_client_secret_info": "このシークレットをコピーすると、もう表示できなくなります", + "add_new_client": "新しい顧客を追加", + "this_app_is_not_setup_already": "このアプリはまだ設定されていません", + "as_csv": "CSV として", + "overlay_my_calendar": "カレンダーを重ね合わせる", + "overlay_my_calendar_toc": "カレンダーに接続することで、弊社のプライバシーポリシーと利用規約に同意することになります。アクセスはいつでも取り消せます。", + "view_overlay_calendar_events": "カレンダーのイベントを表示して予約が重ならないようにします。", + "lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定する", + "description_lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定するためのもので、対面のイベントに役立ちます。", "extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From b7e2a62e2673deebeb18fc5cd53e80c320056649 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:05:39 +0000 Subject: [PATCH 063/119] chore: Use apiLogin instead of user.login in e2e tests (#12382) --- apps/web/playwright/login.2fa.e2e.ts | 4 ++-- apps/web/playwright/workflow.e2e.ts | 2 +- packages/app-store/typeform/playwright/tests/basic.e2e.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/playwright/login.2fa.e2e.ts b/apps/web/playwright/login.2fa.e2e.ts index 4a57ba26bb..af02b992a8 100644 --- a/apps/web/playwright/login.2fa.e2e.ts +++ b/apps/web/playwright/login.2fa.e2e.ts @@ -24,7 +24,7 @@ test.describe("2FA Tests", async () => { const user = await users.create(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const userPassword = user.username!; - await user.login(); + await user.apiLogin(); // expects the home page for an authorized user await page.goto("/settings/security/two-factor-auth"); @@ -94,7 +94,7 @@ test.describe("2FA Tests", async () => { const user = await users.create(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const userPassword = user.username!; - await user.login(); + await user.apiLogin(); // expects the home page for an authorized user await page.goto("/settings/security/two-factor-auth"); diff --git a/apps/web/playwright/workflow.e2e.ts b/apps/web/playwright/workflow.e2e.ts index e84f10a25f..5fc176bb65 100644 --- a/apps/web/playwright/workflow.e2e.ts +++ b/apps/web/playwright/workflow.e2e.ts @@ -17,7 +17,7 @@ test.describe("Workflow tests", () => { async ({ page, users }) => { const user = await users.create(); const [eventType] = user.eventTypes; - await user.login(); + await user.apiLogin(); await page.goto(`/workflows`); await page.click('[data-testid="create-button"]'); diff --git a/packages/app-store/typeform/playwright/tests/basic.e2e.ts b/packages/app-store/typeform/playwright/tests/basic.e2e.ts index 3d04e0df12..052385cdf5 100644 --- a/packages/app-store/typeform/playwright/tests/basic.e2e.ts +++ b/packages/app-store/typeform/playwright/tests/basic.e2e.ts @@ -16,7 +16,7 @@ const installApps = async (page: Page, users: Fixtures["users"]) => { hasTeam: true, } ); - await user.login(); + await user.apiLogin(); await page.goto(`/apps/typeform`); await page.click('[data-testid="install-app-button"]'); (await page.waitForSelector('[data-testid="install-app-button-personal"]')).click(); From 833a326c5b1ca8f08b102635d21a10871ec25aff Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 16 Nov 2023 13:09:03 +0000 Subject: [PATCH 064/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/ro/common.json | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index cb74ee4afe..f294bc25dd 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -268,6 +268,7 @@ "set_availability": "Setează-ți disponibilitatea", "availability_settings": "Setări de disponibilitate", "continue_without_calendar": "Continuă fără calendar", + "continue_with": "Continuați cu {{appName}}", "connect_your_calendar": "Conectează-ți calendarul", "connect_your_video_app": "Conectați-vă aplicațiile video", "connect_your_video_app_instructions": "Conectați aplicațiile video pentru a le utiliza la tipurile dvs. de evenimente.", @@ -288,6 +289,8 @@ "when": "Când", "where": "Unde", "add_to_calendar": "Adaugă în calendar", + "add_to_calendar_description": "Selectați unde vor adăuga evenimentele atunci când aveți rezervări.", + "add_events_to": "Adăugare evenimente în", "add_another_calendar": "Adăugați alt calendar", "other": "Altele", "email_sign_in_subject": "Linkul tău de conectare pentru {{appName}}", @@ -422,6 +425,7 @@ "booking_created": "Rezervare creată", "booking_rejected": "Rezervare respinsă", "booking_requested": "Rezervare solicitată", + "booking_payment_initiated": "Plată de rezervare inițiată", "meeting_ended": "Ședință încheiată", "form_submitted": "Formular trimis", "booking_paid": "Rezervare plătită", @@ -456,6 +460,7 @@ "no_event_types_have_been_setup": "Acest utilizator nu a configurat încă niciun tip de eveniment.", "edit_logo": "Editare logo", "upload_a_logo": "Încărcați o siglă", + "upload_logo": "Încărcare siglă", "remove_logo": "Eliminați sigla", "enable": "Activare", "code": "Cod", @@ -568,6 +573,7 @@ "your_team_name": "Numele echipei tale", "team_updated_successfully": "Echipă actualizată cu succes", "your_team_updated_successfully": "Echipa ta a fost actualizată cu succes.", + "your_org_updated_successfully": "Organizația dvs. a fost actualizată cu succes.", "about": "Despre", "team_description": "Câteva propoziții despre echipa dvs. Aceste informații vor apărea pe pagina de URL a echipei dvs.", "org_description": "Câteva propoziții despre organizația dvs. Aceste informații vor apărea pe pagina publică a organizației dvs.", @@ -599,6 +605,7 @@ "hide_book_a_team_member": "Ascunde butonul Rezervare membru echipă", "hide_book_a_team_member_description": "Ascunde butonul de rezervare pentru un membru al echipei din paginile tale publice.", "danger_zone": "Zonă periculoasă", + "account_deletion_cannot_be_undone": "Aveți grijă. Ștergerea contului nu poate fi anulată.", "back": "Înapoi", "cancel": "Anulează", "cancel_all_remaining": "Anulați toate pozițiile rămase", @@ -688,6 +695,7 @@ "people": "Persoane", "your_email": "E-mailul tău", "change_avatar": "Schimbă avatarul", + "upload_avatar": "Încărcare avatar", "language": "Limba", "timezone": "Timezone", "first_day_of_week": "Prima Zi a Săptămânii", @@ -778,6 +786,7 @@ "disable_guests": "Dezactivează vizitatorii", "disable_guests_description": "Dezactivează adăugarea de vizitatori suplimentari în timpul rezervării.", "private_link": "Generare URL privat", + "enable_private_url": "Activare URL privat", "private_link_label": "Link privat", "private_link_hint": "Linkul dvs. privat se va regenera după fiecare utilizare", "copy_private_link": "Copiere link privat", @@ -1214,6 +1223,7 @@ "organizer_name_variable": "Nume organizator", "app_upgrade_description": "Pentru a utiliza această caracteristică, trebuie să faceți upgrade la un cont Pro.", "invalid_number": "Număr de telefon nevalid", + "invalid_url_error_message": "Adresă URL nevalidă pentru {{label}}. Model URL: {{sampleUrl}}", "navigate": "Navigare", "open": "Deschidere", "close": "Închidere", @@ -1277,6 +1287,7 @@ "personal_cal_url": "URL-ul meu personal {{appName}}", "bio_hint": "Câteva propoziții despre dvs. Acestea vor apărea pe pagina URL-ului dvs. personal.", "user_has_no_bio": "Acest utilizator nu a adăugat încă o biografie.", + "bio": "Biografie", "delete_account_modal_title": "Ștergeți contul", "confirm_delete_account_modal": "Sigur doriți să vă ștergeți contul {{appName}}?", "delete_my_account": "Ștergeți contul meu", @@ -1287,6 +1298,7 @@ "select_calendars": "Selectați pe ce calendare vreți să verificați dacă există conflicte pentru a preveni rezervările suprapuse.", "check_for_conflicts": "Verifică dacă există conflicte", "view_recordings": "Vezi înregistrările", + "check_for_recordings": "Verificați înregistrările", "adding_events_to": "Adăugarea de evenimente la", "follow_system_preferences": "Urmăriți preferințele sistemului", "custom_brand_colors": "Culori personalizate ale mărcii", @@ -1531,6 +1543,7 @@ "problem_registering_domain": "A survenit o problemă la înregistrarea subdomeniului. Încercați din nou sau contactați un administrator", "team_publish": "Publicați echipa", "number_text_notifications": "Număr de telefon (notificări text)", + "number_sms_notifications": "Număr de telefon (notificări SMS)", "attendee_email_variable": "E-mail participant", "attendee_email_info": "E-mailul persoanei care efectuează rezervarea", "kbar_search_placeholder": "Scrie o comandă sau caută...", @@ -1595,6 +1608,7 @@ "options": "Opțiuni", "enter_option": "Introdu opțiunea {{index}}", "add_an_option": "Adaugă o opțiune", + "location_already_exists": "Această locație există deja. Selectați o locație nouă", "radio": "Radio", "google_meet_warning": "Pentru a utiliza Google Meet, trebuie să setezi calendarul tău de destinație la un cont Google Calendar", "individual": "Persoană", @@ -1614,6 +1628,7 @@ "date_overrides_mark_all_day_unavailable_other": "Marchează datele selectate ca indisponibile", "date_overrides_add_btn": "Adaugă suprascriere", "date_overrides_update_btn": "Actualizează suprascrierea", + "date_successfully_added": "Suprascriere dată adăugată cu succes", "event_type_duplicate_copy_text": "{{slug}}-copie", "set_as_default": "Setează ca implicit", "hide_eventtype_details": "Ascundere detalii despre tipul de eveniment", @@ -1640,6 +1655,7 @@ "minimum_round_robin_hosts_count": "Numărul de gazde necesar pentru participare", "hosts": "Gazde", "upgrade_to_enable_feature": "Trebuie să creați o echipă pentru a activa această caracteristică. Faceți clic pentru a crea o echipă.", + "orgs_upgrade_to_enable_feature": "Trebuie să faceți upgrade la planul nostru Enterprise pentru a activa această caracteristică.", "new_attendee": "Participant nou", "awaiting_approval": "În așteptarea aprobării", "requires_google_calendar": "Această aplicație necesită o conexiune cu Google Calendar", @@ -1744,6 +1760,7 @@ "show_on_booking_page": "Afișare pe pagina de rezervare", "get_started_zapier_templates": "Faceți primii pași cu șabloanele Zapier", "team_is_unpublished": "Echipa {{team}} nu este publicată", + "org_is_unpublished_description": "Acest link de organizație nu este disponibil momentan. Contactați proprietarul organizației sau rugați-l să îl publice.", "team_is_unpublished_description": "Acest link de {{entity}} nu este disponibil momentan. Contactați proprietarul {{entity}} sau rugați-l să îl publice.", "team_member": "Membru al echipei", "a_routing_form": "Un formular de parcurs", @@ -1878,6 +1895,7 @@ "edit_invite_link": "Modificare setări link", "invite_link_copied": "Link de invitație copiat", "invite_link_deleted": "Link de invitație șters", + "api_key_deleted": "Cheie API ștearsă", "invite_link_updated": "Setări link de invitație salvate", "link_expires_after": "Linkurile vor expira după...", "one_day": "1 zi", @@ -2010,7 +2028,13 @@ "attendee_last_name_variable": "Numele de familie al participantului", "attendee_first_name_info": "Prenumele persoanei care efectuează rezervarea", "attendee_last_name_info": "Numele de familie al persoanei care efectuează rezervarea", + "your_monthly_digest": "Rezumatul dvs. lunar", + "member_name": "Nume membru", + "most_popular_events": "Cele mai populare evenimente", + "summary_of_events_for_your_team_for_the_last_30_days": "Iată rezumatul evenimentelor populare pentru echipa dvs. {{teamName}} din ultimele 30 de zile", "me": "Eu", + "monthly_digest_email": "E-mail cu rezumatul lunar", + "monthly_digest_email_for_teams": "E-mail cu rezumatul lunar pentru echipe", "verify_team_tooltip": "Verificați-vă echipa pentru a permite trimiterea de mesaje către participanți", "member_removed": "Membru eliminat", "my_availability": "Disponibilitatea mea", @@ -2040,13 +2064,41 @@ "team_no_event_types": "Această echipă nu are niciun tip de eveniment", "seat_options_doesnt_multiple_durations": "Opțiunea de loc nu acceptă mai multe durate", "include_calendar_event": "Includeți eveniment din calendar", + "oAuth": "OAuth", "recently_added": "Adăugate recent", "no_members_found": "Nu s-a găsit niciun membru", "event_setup_length_error": "Configurare eveniment: Durata trebuie să fie de cel puțin 1 minut.", "availability_schedules": "Programe de disponibilitate", + "unauthorized": "Neautorizat", + "access_cal_account": "{{clientName}} ar dori acces la contul dvs. {{appName}}", + "select_account_team": "Selectați contul sau echipa", + "allow_client_to": "Acest lucru va permite ca {{clientName}}", + "associate_with_cal_account": "să vă asocieze cu informațiile dvs. personale deținute de {{clientName}}", + "see_personal_info": "să vadă informațiile dvs. personale, inclusiv orice informații personale pe care le-ați făcut publice", + "see_primary_email_address": "să vadă adresa dvs. de e-mail principală", + "connect_installed_apps": "să se conecteze la aplicațiile dvs. instalate", + "access_event_type": "să citească, să editeze, să șteargă tipurile dvs. de evenimente", + "access_availability": "să citească, să editeze, să șteargă disponibilitatea dvs.", + "access_bookings": "să citească, să editeze, să șteargă rezervările dvs.", + "allow_client_to_do": "Permiteți ca {{clientName}} să facă acest lucru?", + "oauth_access_information": "Dacă faceți clic pe „Permiteți”, înseamnă că permiteți acestei aplicații să utilizeze informațiile dvs. în conformitate cu condițiile de utilizare și politica de confidențialitate ale acesteia. Puteți elimina accesul din Magazinul de aplicații {{appName}}.", + "allow": "Permiteți", "view_only_edit_availability_not_onboarded": "Acest utilizator nu a finalizat integrarea. Nu-i veți putea seta disponibilitatea până când nu finalizează integrarea.", "view_only_edit_availability": "Vizualizați disponibilitatea acestui utilizator. Nu puteți edita decât disponibilitatea dvs. personală.", + "you_can_override_calendar_in_advanced_tab": "Puteți suprascrie această setare pentru fiecare eveniment din secțiunea Setări avansate aferentă fiecărui tip de eveniment.", "edit_users_availability": "Editați disponibilitatea utilizatorului: {{username}}", + "resend_invitation": "Retrimitere invitație", + "invitation_resent": "Invitația a fost trimisă din nou.", + "add_client": "Adăugare client", + "copy_client_secret_info": "După ce copiați secretul, nu îl veți mai putea vedea", + "add_new_client": "Adăugare client nou", + "this_app_is_not_setup_already": "Această aplicație nu a fost configurată încă", + "as_csv": "ca CSV", + "overlay_my_calendar": "Suprapunere calendar propriu", + "overlay_my_calendar_toc": "Prin conectarea la calendarul dvs., acceptați politica noastră de confidențialitate și condițiile de utilizare. Puteți revoca accesul în orice moment.", + "view_overlay_calendar_events": "Vizualizați evenimentele din calendar pentru a preveni conflictele între rezervări.", + "lock_timezone_toggle_on_booking_page": "Blocare fus orar pe pagina de rezervare", + "description_lock_timezone_toggle_on_booking_page": "Pentru a bloca fusul orar pe pagina de rezervare, util pentru evenimentele în persoană.", "extensive_whitelabeling": "Asistență dedicată pentru integrare și inginerie", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From a9418b06d6b6d450c4fa0771d6b6b9a42e160dcb Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:15:38 +0000 Subject: [PATCH 065/119] fix: Wrong spelling of the word "pode" (fix-wrongStringPT-BR) (#12344) Co-authored-by: gitstart-calcom Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- apps/web/public/static/locales/pt-BR/common.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index 61d4870943..f4721862d2 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -944,7 +944,7 @@ "create_events_on": "Criar eventos em", "enterprise_license": "Este não é um recurso corporativo", "enterprise_license_description": "Para ativar este recurso, obtenha uma chave de desenvolvimento no console {{consoleUrl}} e adicione ao seu .env como CALCOM_LICENSE_KEY. Caso sua equipe já tenha uma licença, entre em contato com {{supportMail}} para obter ajuda.", - "enterprise_license_development": "Você pdoe testar este recurso no modo de desenvolvimento. Para uso em produção, solicite que um administrador acesse <2>/auth/setup e insira uma chave de licença.", + "enterprise_license_development": "Você pode testar este recurso no modo de desenvolvimento. Para uso em produção, solicite que um administrador acesse <2>/auth/setup e insira uma chave de licença.", "missing_license": "Falta a licença", "signup_requires": "Licença comercial obrigatória", "signup_requires_description": "Atualmente a {{companyName}} não oferece nenhuma versão gratuita de código aberto da página de inscrição. Para obter acesso completo aos componentes da inscrição, você precisa adquirir uma licença comercial. Para uso pessoal, recomendamos o Prisma Data Platform ou outras interfaces em Postgres para criação de contas.", From ca9e0f5b725cdf120107defd3d5b22ff360c68a4 Mon Sep 17 00:00:00 2001 From: shaharyarshamshi Date: Thu, 16 Nov 2023 19:29:01 +0530 Subject: [PATCH 066/119] refactor: Removed the dirName config for app store cli (#12321) Co-authored-by: Peer Richelsen --- packages/app-store-cli/src/core.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-store-cli/src/core.ts b/packages/app-store-cli/src/core.ts index ed36d375aa..09ff54d1ef 100644 --- a/packages/app-store-cli/src/core.ts +++ b/packages/app-store-cli/src/core.ts @@ -110,7 +110,6 @@ export const BaseAppFork = { isTemplate, // Store the template used to create an app __template: template, - dirName: slug, }; const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString()); config = { From 0b96ef54768408d223d8703a1f1e9d6ce78ce7c3 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 16 Nov 2023 11:57:37 -0300 Subject: [PATCH 067/119] perf: Increased memory/vCPU for slots perf (#12387) --- apps/web/vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/vercel.json b/apps/web/vercel.json index 6f4decb2ee..2183cd596c 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -7,10 +7,10 @@ ], "functions": { "pages/api/trpc/public/[trpc].ts": { - "memory": 768 + "memory": 3008 }, "pages/api/trpc/slots/[trpc].ts": { - "memory": 768 + "memory": 3008 } } } From a4c1df365819d859e136b2267ffbfd93a4d2d17a Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:48:24 +0530 Subject: [PATCH 068/119] refactor: team settings redesign (#12230) Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen --- .../pages/settings/my-account/appearance.tsx | 10 +- apps/web/public/static/locales/en/common.json | 4 + .../ee/components/BrandColorsForm.tsx | 130 +++++ .../ee/components/CommonSkeletonLoaders.tsx | 31 + .../pages/settings/appearance.tsx | 190 +----- .../organizations/pages/settings/profile.tsx | 19 - .../components/DisableTeamImpersonation.tsx | 43 +- .../components/MakeTeamPrivateSwitch.tsx | 46 +- .../ee/teams/pages/team-appearance-view.tsx | 373 ++++++------ .../ee/teams/pages/team-members-view.tsx | 6 +- .../ee/teams/pages/team-profile-view.tsx | 550 ++++++++++-------- .../routers/viewer/teams/update.handler.ts | 10 + .../routers/viewer/teams/update.schema.ts | 2 +- .../image-uploader/ImageUploader.tsx | 12 +- 14 files changed, 698 insertions(+), 728 deletions(-) create mode 100644 packages/features/ee/components/BrandColorsForm.tsx create mode 100644 packages/features/ee/components/CommonSkeletonLoaders.tsx diff --git a/apps/web/pages/settings/my-account/appearance.tsx b/apps/web/pages/settings/my-account/appearance.tsx index f2475a02a9..a8b479974a 100644 --- a/apps/web/pages/settings/my-account/appearance.tsx +++ b/apps/web/pages/settings/my-account/appearance.tsx @@ -250,10 +250,7 @@ const AppearanceView = ({ /> {lightModeError ? (
- +
) : null}
@@ -282,10 +279,7 @@ const AppearanceView = ({ /> {darkModeError ? (
- +
) : null}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 94506cb885..69906ee67e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -56,6 +56,8 @@ "a_refund_failed": "A refund failed", "awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}", "meeting_awaiting_payment": "Your meeting is awaiting payment", + "dark_theme_contrast_error":"Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.", + "light_theme_contrast_error":"Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.", "payment_not_created_error": "Payment could not be created", "couldnt_charge_card_error": "Could not charge card for Payment", "no_available_users_found_error": "No available users found. Could you try another time slot?", @@ -615,6 +617,7 @@ "hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.", "danger_zone": "Danger zone", "account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.", + "team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone", "back": "Back", "cancel": "Cancel", "cancel_all_remaining": "Cancel all remaining", @@ -1413,6 +1416,7 @@ "slot_length": "Slot length", "booking_appearance": "Booking Appearance", "appearance_team_description": "Manage settings for your team's booking appearance", + "appearance_org_description": "Manage settings for your organization's booking appearance", "only_owner_change": "Only the owner of this team can make changes to the team's booking ", "team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'", "invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}", diff --git a/packages/features/ee/components/BrandColorsForm.tsx b/packages/features/ee/components/BrandColorsForm.tsx new file mode 100644 index 0000000000..a3181ac24d --- /dev/null +++ b/packages/features/ee/components/BrandColorsForm.tsx @@ -0,0 +1,130 @@ +import { useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import { classNames } from "@calcom/lib"; +import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; +import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, ColorPicker, SettingsToggle, Alert } from "@calcom/ui"; + +type BrandColorsFormValues = { + brandColor: string; + darkBrandColor: string; +}; + +const BrandColorsForm = ({ + onSubmit, + brandColor, + darkBrandColor, +}: { + onSubmit: (values: BrandColorsFormValues) => void; + brandColor: string | undefined; + darkBrandColor: string | undefined; +}) => { + const { t } = useLocale(); + const brandColorsFormMethods = useFormContext(); + const { + formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty }, + handleSubmit, + } = brandColorsFormMethods; + + const [isCustomBrandColorChecked, setIsCustomBrandColorChecked] = useState( + brandColor !== DEFAULT_LIGHT_BRAND_COLOR || darkBrandColor !== DEFAULT_DARK_BRAND_COLOR + ); + const [darkModeError, setDarkModeError] = useState(false); + const [lightModeError, setLightModeError] = useState(false); + return ( +
+ { + setIsCustomBrandColorChecked(checked); + if (!checked) { + onSubmit({ + brandColor: DEFAULT_LIGHT_BRAND_COLOR, + darkBrandColor: DEFAULT_DARK_BRAND_COLOR, + }); + } + }} + childrenClassName="lg:ml-0" + switchContainerClassName={classNames( + "py-6 px-4 sm:px-6 border-subtle rounded-xl border", + isCustomBrandColorChecked && "rounded-b-none" + )}> +
+ ( +
+

{t("light_brand_color")}

+ { + try { + checkWCAGContrastColor("#ffffff", value); + setLightModeError(false); + brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); + } catch (err) { + setLightModeError(false); + } + }} + /> + {lightModeError ? ( +
+ +
+ ) : null} +
+ )} + /> + + ( +
+

{t("dark_brand_color")}

+ { + try { + checkWCAGContrastColor("#101010", value); + setDarkModeError(false); + brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); + } catch (err) { + setDarkModeError(true); + } + }} + /> + {darkModeError ? ( +
+ +
+ ) : null} +
+ )} + /> +
+ + + +
+
+ ); +}; + +export default BrandColorsForm; diff --git a/packages/features/ee/components/CommonSkeletonLoaders.tsx b/packages/features/ee/components/CommonSkeletonLoaders.tsx new file mode 100644 index 0000000000..38bdaf8785 --- /dev/null +++ b/packages/features/ee/components/CommonSkeletonLoaders.tsx @@ -0,0 +1,31 @@ +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import { Meta, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui"; + +export const AppearanceSkeletonLoader = ({ title, description }: { title: string; description: string }) => { + return ( + + +
+ +
+
+
+
+
+
+
+
+ + +
+ + +
+
+ + + +
+ + ); +}; diff --git a/packages/features/ee/organizations/pages/settings/appearance.tsx b/packages/features/ee/organizations/pages/settings/appearance.tsx index 04a004e9e3..507b38dab1 100644 --- a/packages/features/ee/organizations/pages/settings/appearance.tsx +++ b/packages/features/ee/organizations/pages/settings/appearance.tsx @@ -1,60 +1,20 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; -import { Controller, useForm, useFormContext } from "react-hook-form"; +import { useForm } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import BrandColorsForm from "@calcom/features/ee/components/BrandColorsForm"; +import { AppearanceSkeletonLoader } from "@calcom/features/ee/components/CommonSkeletonLoaders"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import ThemeLabel from "@calcom/features/settings/ThemeLabel"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { classNames } from "@calcom/lib"; import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants"; -import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { MembershipRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react"; -import { - Button, - ColorPicker, - Form, - Meta, - showToast, - SkeletonButton, - SkeletonContainer, - SkeletonText, - SettingsToggle, - Alert, -} from "@calcom/ui"; - -const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { - return ( - - -
- -
-
-
- - - -
-
- - -
- - -
-
- - - -
-
- ); -}; +import { Button, Form, Meta, showToast, SettingsToggle } from "@calcom/ui"; type BrandColorsFormValues = { brandColor: string; @@ -100,11 +60,13 @@ const OrgAppearanceView = ({ await utils.viewer.organizations.listCurrent.invalidate(); showToast(t("your_team_updated_successfully"), "success"); - brandColorsFormMethods.reset({ - brandColor: res.data.brandColor as string, - darkBrandColor: res.data.darkBrandColor as string, - }); - resetOrgThemeReset({ theme: res.data.theme as string | undefined }); + if (res) { + brandColorsFormMethods.reset({ + brandColor: res.data.brandColor as string, + darkBrandColor: res.data.darkBrandColor as string, + }); + resetOrgThemeReset({ theme: res.data.theme as string | undefined }); + } }, }); @@ -116,7 +78,7 @@ const OrgAppearanceView = ({ {isAdminOrOwner ? ( @@ -175,8 +137,8 @@ const OrgAppearanceView = ({ }}> @@ -190,7 +152,7 @@ const OrgAppearanceView = ({ setHideBrandingValue(checked); mutation.mutate({ hideBranding: checked }); }} - switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6" + switchContainerClassName="mt-6" />
) : ( @@ -202,126 +164,6 @@ const OrgAppearanceView = ({ ); }; -const BrandColorsForm = ({ - onSubmit, - orgBrandColor, - orgDarkBrandColor, -}: { - onSubmit: (values: BrandColorsFormValues) => void; - orgBrandColor: string | undefined; - orgDarkBrandColor: string | undefined; -}) => { - const { t } = useLocale(); - const brandColorsFormMethods = useFormContext(); - const { - formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty }, - handleSubmit, - } = brandColorsFormMethods; - - const [isCustomBrandColorChecked, setIsCustomBrandColorChecked] = useState( - orgBrandColor !== DEFAULT_LIGHT_BRAND_COLOR || orgDarkBrandColor !== DEFAULT_DARK_BRAND_COLOR - ); - const [darkModeError, setDarkModeError] = useState(false); - const [lightModeError, setLightModeError] = useState(false); - return ( -
- { - setIsCustomBrandColorChecked(checked); - if (!checked) { - onSubmit({ - brandColor: DEFAULT_LIGHT_BRAND_COLOR, - darkBrandColor: DEFAULT_DARK_BRAND_COLOR, - }); - } - }} - childrenClassName="lg:ml-0" - switchContainerClassName={classNames( - "py-6 px-4 sm:px-6 border-subtle rounded-xl border", - isCustomBrandColorChecked && "rounded-b-none" - )}> -
- ( -
-

{t("light_brand_color")}

- { - try { - checkWCAGContrastColor("#ffffff", value); - setLightModeError(false); - brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); - } catch (err) { - setLightModeError(false); - } - }} - /> - {lightModeError ? ( -
- -
- ) : null} -
- )} - /> - - ( -
-

{t("dark_brand_color")}

- { - try { - checkWCAGContrastColor("#101010", value); - setDarkModeError(false); - brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); - } catch (err) { - setDarkModeError(true); - } - }} - /> - {darkModeError ? ( -
- -
- ) : null} -
- )} - /> -
- - - -
-
- ); -}; - const OrgAppearanceViewWrapper = () => { const router = useRouter(); const { t } = useLocale(); @@ -332,7 +174,7 @@ const OrgAppearanceViewWrapper = () => { }); if (isLoading) { - return ; + return ; } if (!currentOrg) return null; diff --git a/packages/features/ee/organizations/pages/settings/profile.tsx b/packages/features/ee/organizations/pages/settings/profile.tsx index e6d652dfd1..ba40ed4c69 100644 --- a/packages/features/ee/organizations/pages/settings/profile.tsx +++ b/packages/features/ee/organizations/pages/settings/profile.tsx @@ -141,25 +141,6 @@ const OrgProfileView = () => {
)} - {/* Disable Org disbanding */} - {/*
-
{t("danger_zone")}
- {currentOrganisation?.user.role === "OWNER" ? ( - - - - - - {t("disband_org_confirmation_message")} - - - ) : null} */} {/* LEAVE ORG should go above here ^ */} diff --git a/packages/features/ee/teams/components/DisableTeamImpersonation.tsx b/packages/features/ee/teams/components/DisableTeamImpersonation.tsx index c514210f27..7e27a7c355 100644 --- a/packages/features/ee/teams/components/DisableTeamImpersonation.tsx +++ b/packages/features/ee/teams/components/DisableTeamImpersonation.tsx @@ -1,7 +1,8 @@ -import { classNames } from "@calcom/lib"; +import { useState } from "react"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { showToast, Switch } from "@calcom/ui"; +import { showToast, SettingsToggle } from "@calcom/ui"; const DisableTeamImpersonation = ({ teamId, @@ -24,35 +25,23 @@ const DisableTeamImpersonation = ({ await utils.viewer.teams.getMembershipbyUser.invalidate(); }, }); + const [allowImpersonation, setAllowImpersonation] = useState(!query.data?.disableImpersonation ?? true); if (query.isLoading) return <>; return ( <> -
-
-
-

- {t("user_impersonation_heading")} -

-
-

- {t("team_impersonation_description")} -

-
-
- { - mutation.mutate({ teamId, memberId, disableImpersonation: !isChecked }); - }} - /> -
-
+ { + setAllowImpersonation(_allowImpersonation); + mutation.mutate({ teamId, memberId, disableImpersonation: !_allowImpersonation }); + }} + switchContainerClassName="mt-6" + /> ); }; diff --git a/packages/features/ee/teams/components/MakeTeamPrivateSwitch.tsx b/packages/features/ee/teams/components/MakeTeamPrivateSwitch.tsx index 73041200e6..3a1bd4a368 100644 --- a/packages/features/ee/teams/components/MakeTeamPrivateSwitch.tsx +++ b/packages/features/ee/teams/components/MakeTeamPrivateSwitch.tsx @@ -1,7 +1,8 @@ -import { classNames } from "@calcom/lib"; +import { useState } from "react"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { showToast, Switch } from "@calcom/ui"; +import { showToast, SettingsToggle } from "@calcom/ui"; const MakeTeamPrivateSwitch = ({ teamId, @@ -26,34 +27,23 @@ const MakeTeamPrivateSwitch = ({ }, }); + const [isTeamPrivate, setTeamPrivate] = useState(isPrivate); + return ( <> -
-
-
-

- {t("make_team_private")} -

-
-

- {t("make_team_private_description")} -

-
-
- { - mutation.mutate({ id: teamId, isPrivate: isChecked }); - }} - /> -
-
+ { + setTeamPrivate(checked); + mutation.mutate({ id: teamId, isPrivate: checked }); + }} + switchContainerClassName="mt-6" + data-testid="make-team-private-check" + /> ); }; diff --git a/packages/features/ee/teams/pages/team-appearance-view.tsx b/packages/features/ee/teams/pages/team-appearance-view.tsx index 3ce599ab4c..a8cc2ac269 100644 --- a/packages/features/ee/teams/pages/team-appearance-view.tsx +++ b/packages/features/ee/teams/pages/team-appearance-view.tsx @@ -1,73 +1,188 @@ import { useRouter } from "next/navigation"; -import { Controller, useForm } from "react-hook-form"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import BrandColorsForm from "@calcom/features/ee/components/BrandColorsForm"; +import { AppearanceSkeletonLoader } from "@calcom/features/ee/components/CommonSkeletonLoaders"; +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import { APP_NAME } from "@calcom/lib/constants"; +import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { MembershipRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; -import { - Button, - ColorPicker, - Form, - Meta, - showToast, - SkeletonButton, - SkeletonContainer, - SkeletonText, - Switch, -} from "@calcom/ui"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { Button, Form, Meta, showToast, SettingsToggle } from "@calcom/ui"; import ThemeLabel from "../../../settings/ThemeLabel"; import { getLayout } from "../../../settings/layouts/SettingsLayout"; -const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { - return ( - - -
-
- - - -
-
- - -
- - - - -
-
- ); -}; - -interface TeamAppearanceValues { - hideBranding: boolean; - hideBookATeamMember: boolean; +type BrandColorsFormValues = { brandColor: string; darkBrandColor: string; - theme: string | null | undefined; -} +}; -const ProfileView = () => { - const params = useParamsWithFallback(); +type ProfileViewProps = { team: RouterOutputs["viewer"]["teams"]["get"] }; + +const ProfileView = ({ team }: ProfileViewProps) => { const { t } = useLocale(); - const router = useRouter(); const utils = trpc.useContext(); + const [hideBrandingValue, setHideBrandingValue] = useState(team?.hideBranding ?? false); + const [hideBookATeamMember, setHideBookATeamMember] = useState(team?.hideBookATeamMember ?? false); + + const themeForm = useForm<{ theme: string | null | undefined }>({ + defaultValues: { + theme: team?.theme, + }, + }); + + const { + formState: { isSubmitting: isThemeSubmitting, isDirty: isThemeDirty }, + reset: resetTheme, + } = themeForm; + + const brandColorsFormMethods = useForm({ + defaultValues: { + brandColor: team?.brandColor || DEFAULT_LIGHT_BRAND_COLOR, + darkBrandColor: team?.darkBrandColor || DEFAULT_DARK_BRAND_COLOR, + }, + }); + + const { reset: resetBrandColors } = brandColorsFormMethods; + const mutation = trpc.viewer.teams.update.useMutation({ onError: (err) => { showToast(err.message, "error"); }, - async onSuccess() { + async onSuccess(res) { await utils.viewer.teams.get.invalidate(); + if (res) { + resetTheme({ theme: res.theme }); + resetBrandColors({ brandColor: res.brandColor, darkBrandColor: res.darkBrandColor }); + } + showToast(t("your_team_updated_successfully"), "success"); }, }); + const onBrandColorsFormSubmit = (values: BrandColorsFormValues) => { + mutation.mutate({ ...values, id: team.id }); + }; + + const isAdmin = + team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN); + + return ( + <> + + {isAdmin ? ( + <> +
{ + mutation.mutate({ + id: team.id, + theme: values.theme || null, + }); + }}> +
+
+

{t("theme")}

+

{t("theme_applies_note")}

+
+
+
+ + + +
+ + + +
+ +
{ + onBrandColorsFormSubmit(values); + }}> + + + +
+ { + setHideBrandingValue(checked); + mutation.mutate({ id: team.id, hideBranding: checked }); + }} + /> + + { + setHideBookATeamMember(checked); + mutation.mutate({ id: team.id, hideBookATeamMember: checked }); + }} + /> +
+ + ) : ( +
+ {t("only_owner_change")} +
+ )} + + ); +}; + +const ProfileViewWrapper = () => { + const router = useRouter(); + const params = useParamsWithFallback(); + + const { t } = useLocale(); + const { data: team, isLoading } = trpc.viewer.teams.get.useQuery( { teamId: Number(params.id) }, { @@ -77,170 +192,16 @@ const ProfileView = () => { } ); - const form = useForm({ - defaultValues: { - theme: team?.theme, - brandColor: team?.brandColor, - darkBrandColor: team?.darkBrandColor, - hideBranding: team?.hideBranding, - }, - }); + if (isLoading) + return ( + + ); - const isAdmin = - team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN); + if (!team) return null; - if (isLoading) { - return ; - } - return ( - <> - - {isAdmin ? ( -
{ - mutation.mutate({ - id: team.id, - ...values, - theme: values.theme || null, - }); - }}> -
-
-

{t("theme")}

-

{t("theme_applies_note")}

-
-
-
- - - -
- -
-
-
-

{t("custom_brand_colors")}

-

{t("customize_your_brand_colors")}

-
-
- -
- ( -
-

{t("light_brand_color")}

- form.setValue("brandColor", value, { shouldDirty: true })} - /> -
- )} - /> - ( -
-

{t("dark_brand_color")}

- form.setValue("darkBrandColor", value, { shouldDirty: true })} - /> -
- )} - /> -
-
- -
-
-
- -

- {t("team_disable_cal_branding_description", { appName: APP_NAME })} -

-
- -
- ( - { - form.setValue("hideBranding", isChecked); - }} - /> - )} - /> -
-
-
-
- -

{t("hide_book_a_team_member_description")}

-
-
- ( - { - form.setValue("hideBookATeamMember", isChecked); - }} - /> - )} - /> -
-
-
- -
- ) : ( -
- {t("only_owner_change")} -
- )} - - ); + return ; }; -ProfileView.getLayout = getLayout; +ProfileViewWrapper.getLayout = getLayout; -export default ProfileView; +export default ProfileViewWrapper; diff --git a/packages/features/ee/teams/pages/team-members-view.tsx b/packages/features/ee/teams/pages/team-members-view.tsx index da8128d734..6b7ff9b14a 100644 --- a/packages/features/ee/teams/pages/team-members-view.tsx +++ b/packages/features/ee/teams/pages/team-members-view.tsx @@ -170,7 +170,6 @@ const MembersView = () => { {((team?.isPrivate && isAdmin) || !team?.isPrivate || isOrgAdminOrOwner) && ( <> -
)} @@ -183,10 +182,7 @@ const MembersView = () => { )} {team && (isAdmin || isOrgAdminOrOwner) && ( - <> -
- - + )}
{showMemberInvitationModal && team && ( diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index 69179974d0..294ce36fbd 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -9,6 +9,7 @@ import { z } from "zod"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -19,6 +20,7 @@ import objectKeys from "@calcom/lib/objectKeys"; import slugify from "@calcom/lib/slugify"; import turndown from "@calcom/lib/turndownService"; import { MembershipRole } from "@calcom/prisma/enums"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { Avatar, @@ -36,6 +38,8 @@ import { SkeletonContainer, SkeletonText, TextField, + SkeletonAvatar, + SkeletonButton, } from "@calcom/ui"; import { ExternalLink, Link as LinkIcon, LogOut, Trash2 } from "@calcom/ui/components/icon"; @@ -51,10 +55,31 @@ const teamProfileFormSchema = z.object({ message: "Url can only have alphanumeric characters(a-z, 0-9) and hyphen(-) symbol.", }) .min(1, { message: "Url cannot be left empty" }), - logo: z.string(), + logo: z.string().nullable(), bio: z.string(), }); +type FormValues = z.infer; + +const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { + return ( + + +
+
+ + +
+ + + + + +
+
+ ); +}; + const ProfileView = () => { const params = useParamsWithFallback(); const teamId = Number(params.id); @@ -62,27 +87,11 @@ const ProfileView = () => { const router = useRouter(); const utils = trpc.useContext(); const session = useSession(); - const [firstRender, setFirstRender] = useState(true); - const orgBranding = useOrgBranding(); useLayoutEffect(() => { document.body.focus(); }, []); - const mutation = trpc.viewer.teams.update.useMutation({ - onError: (err) => { - showToast(err.message, "error"); - }, - async onSuccess() { - await utils.viewer.teams.get.invalidate(); - showToast(t("your_team_updated_successfully"), "success"); - }, - }); - - const form = useForm({ - resolver: zodResolver(teamProfileFormSchema), - }); - const { data: team, isLoading } = trpc.viewer.teams.get.useQuery( { teamId, includeTeamLogo: true }, { @@ -90,17 +99,6 @@ const ProfileView = () => { onError: () => { router.push("/settings"); }, - onSuccess: (team) => { - if (team) { - form.setValue("name", team.name || ""); - form.setValue("slug", team.slug || ""); - form.setValue("bio", team.bio || ""); - form.setValue("logo", team.logo || ""); - if (team.slug === null && (team?.metadata as Prisma.JsonObject)?.requestedSlug) { - form.setValue("slug", ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || ""); - } - } - }, } ); @@ -131,17 +129,6 @@ const ProfileView = () => { }, }); - const publishMutation = trpc.viewer.teams.publish.useMutation({ - async onSuccess(data: { url?: string }) { - if (data.url) { - router.push(data.url); - } - }, - async onError(err) { - showToast(err.message, "error"); - }, - }); - function deleteTeam() { if (team?.id) deleteTeamMutation.mutate({ teamId: team.id }); } @@ -154,233 +141,284 @@ const ProfileView = () => { }); } + if (isLoading) { + return ; + } + return ( <> - - {!isLoading ? ( - <> - {isAdmin ? ( -
{ - if (team) { - const variables = { - name: values.name, - slug: values.slug, - bio: values.bio, - logo: values.logo, - }; - objectKeys(variables).forEach((key) => { - if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key]; - }); - mutation.mutate({ id: team.id, ...variables }); - } - }}> - {!team.parent && ( - <> -
- ( - <> - -
- { - form.setValue("logo", newLogo); - }} - imageSrc={value} - /> -
- - )} - /> -
-
- - )} + - ( -
- { - form.setValue("name", e?.target.value); - }} - /> -
- )} - /> - ( -
- { - form.clearErrors("slug"); - form.setValue("slug", slugify(e?.target.value, true)); - }} - /> -
- )} - /> -
- - md.render(form.getValues("bio") || "")} - setText={(value: string) => form.setValue("bio", turndown(value))} - excludedToolbarItems={["blockType"]} - disableLists - firstRender={firstRender} - setFirstRender={setFirstRender} - /> -
-

{t("team_description")}

- - {IS_TEAM_BILLING_ENABLED && - team.slug === null && - (team.metadata as Prisma.JsonObject)?.requestedSlug && ( - - )} - - ) : ( -
-
-
- -

{team?.name}

-
- {team && !isBioEmpty && ( - <> - -
- - )} -
-
- - {t("preview")} - - { - navigator.clipboard.writeText(permalink); - showToast("Copied to clipboard", "success"); - }}> - {t("copy_link_team")} - -
-
- )} -
- -
{t("danger_zone")}
- {team?.membership.role === "OWNER" ? ( - - - - - - {t("disband_team_confirmation_message")} - - - ) : ( - - - - - - {t("leave_team_confirmation_message")} - - - )} - + {isAdmin ? ( + ) : ( - <> - -
-
- -
- -
+
+
+
+ +

{team?.name}

-
- -
- -
-
- -
- -
-
-
- -
- - -
- -
- -
-
-
- + {team && !isBioEmpty && ( + <> + +
+ + )} +
+
+ + {t("preview")} + + { + navigator.clipboard.writeText(permalink); + showToast("Copied to clipboard", "success"); + }}> + {t("copy_link_team")} + +
+
+ )} + +
+ + {team?.membership.role === "OWNER" && ( +

{t("team_deletion_cannot_be_undone")}

+ )} +
+ {team?.membership.role === "OWNER" ? ( + + + + + + + + {t("disband_team_confirmation_message")} + + + ) : ( + + + + + + + + {t("leave_team_confirmation_message")} + + )} ); }; +export type TeamProfileFormProps = { team: RouterOutputs["viewer"]["teams"]["get"] }; + +const TeamProfileForm = ({ team }: TeamProfileFormProps) => { + const utils = trpc.useContext(); + const { t } = useLocale(); + const router = useRouter(); + + const mutation = trpc.viewer.teams.update.useMutation({ + onError: (err) => { + showToast(err.message, "error"); + }, + async onSuccess(res) { + reset({ + logo: (res?.logo || "") as string, + name: (res?.name || "") as string, + bio: (res?.bio || "") as string, + slug: res?.slug as string, + }); + await utils.viewer.teams.get.invalidate(); + showToast(t("your_team_updated_successfully"), "success"); + }, + }); + + const defaultValues: FormValues = { + name: team?.name || "", + logo: team?.logo || "", + bio: team?.bio || "", + slug: team?.slug || ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || "", + }; + + const form = useForm({ + defaultValues, + resolver: zodResolver(teamProfileFormSchema), + }); + + const [firstRender, setFirstRender] = useState(true); + const orgBranding = useOrgBranding(); + + const { + formState: { isSubmitting, isDirty }, + reset, + } = form; + + const isDisabled = isSubmitting || !isDirty; + + const publishMutation = trpc.viewer.teams.publish.useMutation({ + async onSuccess(data: { url?: string }) { + if (data.url) { + router.push(data.url); + } + }, + async onError(err) { + showToast(err.message, "error"); + }, + }); + + return ( +
{ + if (team) { + const variables = { + name: values.name, + slug: values.slug, + bio: values.bio, + logo: values.logo, + }; + objectKeys(variables).forEach((key) => { + if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key]; + }); + mutation.mutate({ id: team.id, ...variables }); + } + }}> +
+ {!team.parent && ( +
+ { + const showRemoveLogoButton = !!value; + + return ( + <> + +
+ { + form.setValue("logo", newLogo, { shouldDirty: true }); + }} + triggerButtonColor={showRemoveLogoButton ? "secondary" : "primary"} + imageSrc={value ?? undefined} + /> + {showRemoveLogoButton && ( + + )} +
+ + ); + }} + /> +
+ )} + + ( +
+ { + form.setValue("name", e?.target.value, { shouldDirty: true }); + }} + /> +
+ )} + /> + ( +
+ { + form.clearErrors("slug"); + form.setValue("slug", slugify(e?.target.value, true), { shouldDirty: true }); + }} + /> +
+ )} + /> +
+ + md.render(form.getValues("bio") || "")} + setText={(value: string) => form.setValue("bio", turndown(value), { shouldDirty: true })} + excludedToolbarItems={["blockType"]} + disableLists + firstRender={firstRender} + setFirstRender={setFirstRender} + /> +
+

{t("team_description")}

+
+ + + {IS_TEAM_BILLING_ENABLED && + team.slug === null && + (team.metadata as Prisma.JsonObject)?.requestedSlug && ( + + )} + +
+ ); +}; + ProfileView.getLayout = getLayout; export default ProfileView; diff --git a/packages/trpc/server/routers/viewer/teams/update.handler.ts b/packages/trpc/server/routers/viewer/teams/update.handler.ts index e578eefc2c..ff5692f0ac 100644 --- a/packages/trpc/server/routers/viewer/teams/update.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/update.handler.ts @@ -86,4 +86,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { // Sync Services: Close.com if (prevTeam) closeComUpdateTeam(prevTeam, updatedTeam); + + return { + logo: updatedTeam.logo, + name: updatedTeam.name, + bio: updatedTeam.bio, + slug: updatedTeam.slug, + theme: updatedTeam.theme, + brandColor: updatedTeam.brandColor, + darkBrandColor: updatedTeam.darkBrandColor, + }; }; diff --git a/packages/trpc/server/routers/viewer/teams/update.schema.ts b/packages/trpc/server/routers/viewer/teams/update.schema.ts index 5c88cd84f5..3eb58f5024 100644 --- a/packages/trpc/server/routers/viewer/teams/update.schema.ts +++ b/packages/trpc/server/routers/viewer/teams/update.schema.ts @@ -6,7 +6,7 @@ export const ZUpdateInputSchema = z.object({ id: z.number(), bio: z.string().optional(), name: z.string().optional(), - logo: z.string().optional(), + logo: z.string().nullable().optional(), slug: z .string() .transform((val) => slugify(val.trim())) diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index c8d13b716e..1f2c5089e5 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -163,9 +163,12 @@ export default function ImageUploader({ return ( !opened && setFile(null) // unset file on close - }> + onOpenChange={(opened) => { + // unset file on close + if (!opened) { + setFile(null); + } + }}>
+ {t("cancel")} + showCroppedImage(croppedAreaPixels)}> {t("save")} - {t("cancel")} From 9029c5b4c4b6ec6ec6855e1b5586ca94b7cfc2a9 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:50:43 +0200 Subject: [PATCH 069/119] fix: change api root statusCode to 200 (#12362) --- apps/api/pages/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/pages/api/index.ts b/apps/api/pages/api/index.ts index 85f1d6be3b..9f0dfa4829 100644 --- a/apps/api/pages/api/index.ts +++ b/apps/api/pages/api/index.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) { - res.status(201).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" }); + res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" }); } From 7d09ccb0d743becbf08e7d0264713e4941625ac1 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:51:30 +0100 Subject: [PATCH 070/119] fix: add sendgrid-specific header only for SendGrid smtp server (#11932) Co-authored-by: CarinaWolli --- packages/emails/templates/_base-email.ts | 13 ++----------- packages/lib/getAdditionalEmailHeaders.ts | 21 +++++++++++++++++++++ packages/lib/serverConfig.ts | 3 +++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 packages/lib/getAdditionalEmailHeaders.ts diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index 6aefdeffa3..51159a198d 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -48,17 +48,7 @@ export default class BaseEmail { const payload = this.getNodeMailerPayload(); const parseSubject = z.string().safeParse(payload?.subject); const payloadWithUnEscapedSubject = { - headers: { - "X-SMTPAPI": JSON.stringify({ - filters: { - bypass_list_management: { - settings: { - enable: 1, - }, - }, - }, - }), - }, + headers: this.getMailerOptions().headers, ...payload, ...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }), }; @@ -84,6 +74,7 @@ export default class BaseEmail { return { transport: serverConfig.transport, from: serverConfig.from, + headers: serverConfig.headers, }; } diff --git a/packages/lib/getAdditionalEmailHeaders.ts b/packages/lib/getAdditionalEmailHeaders.ts new file mode 100644 index 0000000000..4ef02f9292 --- /dev/null +++ b/packages/lib/getAdditionalEmailHeaders.ts @@ -0,0 +1,21 @@ +type EmailHostHeaders = { + [key: string]: { + [subKey: string]: string; + }; +}; + +export function getAdditionalEmailHeaders(): EmailHostHeaders { + return { + "smtp.sendgrid.net": { + "X-SMTPAPI": JSON.stringify({ + filters: { + bypass_list_management: { + settings: { + enable: 1, + }, + }, + }, + }), + }, + }; +} diff --git a/packages/lib/serverConfig.ts b/packages/lib/serverConfig.ts index 3e6d10993e..09feb75661 100644 --- a/packages/lib/serverConfig.ts +++ b/packages/lib/serverConfig.ts @@ -3,6 +3,8 @@ import type SMTPConnection from "nodemailer/lib/smtp-connection"; import { isENVDev } from "@calcom/lib/env"; +import { getAdditionalEmailHeaders } from "./getAdditionalEmailHeaders"; + function detectTransport(): SendmailTransport.Options | SMTPConnection.Options | string { if (process.env.EMAIL_SERVER) { return process.env.EMAIL_SERVER; @@ -41,4 +43,5 @@ function detectTransport(): SendmailTransport.Options | SMTPConnection.Options | export const serverConfig = { transport: detectTransport(), from: process.env.EMAIL_FROM, + headers: getAdditionalEmailHeaders()[process.env.EMAIL_SERVER_HOST || ""] || undefined, }; From e48a6c3dcfd0bca8ce051d41539be3701fe11a85 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 16 Nov 2023 13:56:24 -0300 Subject: [PATCH 071/119] chore: Disable i18n cache in development (#12374) --- packages/trpc/server/createNextApiHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trpc/server/createNextApiHandler.ts b/packages/trpc/server/createNextApiHandler.ts index 31fccbf06b..9df0e23bbd 100644 --- a/packages/trpc/server/createNextApiHandler.ts +++ b/packages/trpc/server/createNextApiHandler.ts @@ -64,7 +64,7 @@ export function createNextApiHandler(router: AnyRouter, isPublic = false, namesp const cacheRules = { session: "no-cache", - i18n: `max-age=${ONE_YEAR_IN_SECONDS}`, + i18n: process.env.NODE_ENV === "development" ? "no-cache" : `max-age=${ONE_YEAR_IN_SECONDS}`, // FIXME: Using `max-age=1, stale-while-revalidate=60` fails some booking tests. "slots.getSchedule": `no-cache`, From 3534e3c224fe98a13d456d36131cd46814306d41 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 16 Nov 2023 15:40:42 -0300 Subject: [PATCH 072/119] Revert "perf: Increased memory/vCPU for slots perf (#12387)" (#12389) This reverts commit 0b96ef54768408d223d8703a1f1e9d6ce78ce7c3. --- apps/web/vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/vercel.json b/apps/web/vercel.json index 2183cd596c..6f4decb2ee 100644 --- a/apps/web/vercel.json +++ b/apps/web/vercel.json @@ -7,10 +7,10 @@ ], "functions": { "pages/api/trpc/public/[trpc].ts": { - "memory": 3008 + "memory": 768 }, "pages/api/trpc/slots/[trpc].ts": { - "memory": 3008 + "memory": 768 } } } From 28acbe549ae6eff2a471276f6e7787a19488a7f4 Mon Sep 17 00:00:00 2001 From: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:38:27 +0200 Subject: [PATCH 073/119] chore: [app dir bootstrapping 9]: replace useSearchParams with useCompatSearchParams hook (#12056) Co-authored-by: zomars --- apps/web/components/AppListCard.tsx | 5 +- apps/web/lib/hooks/useIsBookingPage.ts | 6 +- apps/web/lib/hooks/useRouterQuery.ts | 6 +- apps/web/lib/hooks/useToggleQuery.tsx | 4 +- apps/web/pages/500.tsx | 4 +- apps/web/pages/apps/[slug]/setup.tsx | 5 +- apps/web/pages/apps/categories/[category].tsx | 4 +- apps/web/pages/apps/installed/[category].tsx | 4 +- apps/web/pages/auth/error.tsx | 4 +- apps/web/pages/auth/login.tsx | 5 +- apps/web/pages/auth/oauth2/authorize.tsx | 4 +- apps/web/pages/auth/saml-idp.tsx | 5 +- apps/web/pages/auth/setup/index.tsx | 5 +- apps/web/pages/auth/sso/[provider].tsx | 5 +- apps/web/pages/auth/verify.tsx | 5 +- apps/web/pages/availability/[schedule].tsx | 5 +- apps/web/pages/availability/index.tsx | 5 +- apps/web/pages/booking/[uid].tsx | 5 +- apps/web/pages/event-types/index.tsx | 9 +-- apps/web/pages/signup.tsx | 4 +- .../alby/components/AlbyPaymentComponent.tsx | 4 +- packages/app-store/alby/pages/setup/index.tsx | 6 +- .../routing-forms/components/FormActions.tsx | 5 +- .../pages/routing-link/[...appPages].tsx | 5 +- .../embeds/embed-core/src/embed-iframe.ts | 4 +- .../embed-core/src/useCompatSearchParams.tsx | 21 ++++++ packages/embeds/embed-core/tsconfig.json | 1 + packages/embeds/embed-snippet/tsconfig.json | 1 + packages/features/apps/AdminAppsList.tsx | 4 +- .../features/bookings/Booker/utils/event.ts | 5 +- .../ee/payments/components/Payment.tsx | 5 +- .../ee/teams/components/AddNewTeamMembers.tsx | 7 +- .../ee/teams/components/TeamListItem.tsx | 4 +- .../ee/teams/components/TeamsListing.tsx | 5 +- .../ee/teams/pages/team-members-view.tsx | 5 +- packages/features/embed/Embed.tsx | 9 +-- .../eventtypes/components/DuplicateDialog.tsx | 4 +- .../insights/context/FiltersProvider.tsx | 5 +- .../settings/layouts/SettingsLayout.tsx | 5 +- .../webhooks/pages/webhook-edit-view.tsx | 5 +- .../webhooks/pages/webhook-new-view.tsx | 5 +- packages/lib/bookingSuccessRedirect.ts | 5 +- .../lib/hooks/useCompatSearchParams.test.ts | 67 +++++++++++++++++++ packages/lib/hooks/useCompatSearchParams.tsx | 20 ++++++ packages/lib/hooks/useRouterQuery.ts | 4 +- packages/lib/hooks/useUrlMatchesCurrentUrl.ts | 6 +- packages/ui/components/apps/AllApps.tsx | 7 +- .../components/createButton/CreateButton.tsx | 5 +- packages/ui/components/dialog/Dialog.tsx | 5 +- packages/ui/components/dialog/dialog.test.tsx | 6 ++ .../ui/components/form/wizard/WizardForm.tsx | 5 +- .../form/wizard/wizardForm.test.tsx | 6 ++ vitest.workspace.ts | 1 + 53 files changed, 254 insertions(+), 97 deletions(-) create mode 100644 packages/embeds/embed-core/src/useCompatSearchParams.tsx create mode 100644 packages/lib/hooks/useCompatSearchParams.test.ts create mode 100644 packages/lib/hooks/useCompatSearchParams.tsx diff --git a/apps/web/components/AppListCard.tsx b/apps/web/components/AppListCard.tsx index 2f9547fbb7..e9deaae260 100644 --- a/apps/web/components/AppListCard.tsx +++ b/apps/web/components/AppListCard.tsx @@ -1,4 +1,4 @@ -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { ReactNode } from "react"; import { useEffect, useRef, useState } from "react"; import { z } from "zod"; @@ -6,6 +6,7 @@ import { z } from "zod"; import type { CredentialOwner } from "@calcom/app-store/types"; import classNames from "@calcom/lib/classNames"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { Badge, ListItemText, Avatar } from "@calcom/ui"; @@ -56,7 +57,7 @@ export default function AppListCard(props: AppListCardProps) { const router = useRouter(); const [highlight, setHighlight] = useState(shouldHighlight && hl === slug); const timeoutRef = useRef(null); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); useEffect(() => { diff --git a/apps/web/lib/hooks/useIsBookingPage.ts b/apps/web/lib/hooks/useIsBookingPage.ts index 3f890bcedc..ffa6804e02 100644 --- a/apps/web/lib/hooks/useIsBookingPage.ts +++ b/apps/web/lib/hooks/useIsBookingPage.ts @@ -1,10 +1,12 @@ -import { usePathname, useSearchParams } from "next/navigation"; +import { usePathname } from "next/navigation"; + +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; export default function useIsBookingPage(): boolean { const pathname = usePathname(); const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route)); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const userParam = Boolean(searchParams?.get("user")); const teamParam = Boolean(searchParams?.get("team")); diff --git a/apps/web/lib/hooks/useRouterQuery.ts b/apps/web/lib/hooks/useRouterQuery.ts index 3bd40e57b3..1ec5a909b2 100644 --- a/apps/web/lib/hooks/useRouterQuery.ts +++ b/apps/web/lib/hooks/useRouterQuery.ts @@ -1,8 +1,10 @@ -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useCallback } from "react"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; + export default function useRouterQuery(name: T) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const router = useRouter(); diff --git a/apps/web/lib/hooks/useToggleQuery.tsx b/apps/web/lib/hooks/useToggleQuery.tsx index 29e3f4fabf..8e705851a9 100644 --- a/apps/web/lib/hooks/useToggleQuery.tsx +++ b/apps/web/lib/hooks/useToggleQuery.tsx @@ -1,7 +1,7 @@ -import { useSearchParams } from "next/navigation"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; export function useToggleQuery(name: string) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); return { isOn: searchParams?.get(name) === "1", diff --git a/apps/web/pages/500.tsx b/apps/web/pages/500.tsx index a7ba940d2f..d91524a60d 100644 --- a/apps/web/pages/500.tsx +++ b/apps/web/pages/500.tsx @@ -1,7 +1,7 @@ import Head from "next/head"; -import { useSearchParams } from "next/navigation"; import { APP_NAME } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, showToast } from "@calcom/ui"; import { Copy } from "@calcom/ui/components/icon"; @@ -9,7 +9,7 @@ import { Copy } from "@calcom/ui/components/icon"; import PageWrapper from "@components/PageWrapper"; export default function Error500() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); return ( diff --git a/apps/web/pages/apps/[slug]/setup.tsx b/apps/web/pages/apps/[slug]/setup.tsx index 4942585a4c..31d8c78a11 100644 --- a/apps/web/pages/apps/[slug]/setup.tsx +++ b/apps/web/pages/apps/[slug]/setup.tsx @@ -1,15 +1,16 @@ import type { InferGetServerSidePropsType } from "next"; import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { AppSetupPage } from "@calcom/app-store/_pages/setup"; import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { HeadSeo } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; export default function SetupInformation(props: InferGetServerSidePropsType) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const router = useRouter(); const slug = searchParams?.get("slug") as string; const { status } = useSession(); diff --git a/apps/web/pages/apps/categories/[category].tsx b/apps/web/pages/apps/categories/[category].tsx index da82248c82..81de5e70c8 100644 --- a/apps/web/pages/apps/categories/[category].tsx +++ b/apps/web/pages/apps/categories/[category].tsx @@ -1,10 +1,10 @@ import { Prisma } from "@prisma/client"; import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import Shell from "@calcom/features/shell/Shell"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma from "@calcom/prisma"; import { AppCategories } from "@calcom/prisma/enums"; @@ -13,7 +13,7 @@ import { AppCard, SkeletonText } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; export default function Apps({ apps }: InferGetStaticPropsType) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t, isLocaleReady } = useLocale(); const category = searchParams?.get("category"); diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index 83ad068cdb..8e6d8c2404 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -1,8 +1,8 @@ -import { useSearchParams } from "next/navigation"; import { useReducer } from "react"; import { z } from "zod"; import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AppCategories } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; @@ -122,7 +122,7 @@ type ModalState = { }; export default function InstalledApps() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const category = searchParams?.get("category") as querySchemaType["category"]; const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => { diff --git a/apps/web/pages/auth/error.tsx b/apps/web/pages/auth/error.tsx index 20e5f77516..502fead3ff 100644 --- a/apps/web/pages/auth/error.tsx +++ b/apps/web/pages/auth/error.tsx @@ -1,8 +1,8 @@ import type { GetStaticPropsContext } from "next"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import z from "zod"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button } from "@calcom/ui"; import { X } from "@calcom/ui/components/icon"; @@ -18,7 +18,7 @@ const querySchema = z.object({ export default function Error() { const { t } = useLocale(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { error } = querySchema.parse(searchParams); const isTokenVerificationError = error?.toLowerCase() === "verification"; const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login"); diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 1fa1eab0cb..327486bc8c 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -4,7 +4,7 @@ import { jwtVerify } from "jose"; import type { GetServerSidePropsContext } from "next"; import { getCsrfToken, signIn } from "next-auth/react"; import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import type { CSSProperties } from "react"; import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; @@ -17,6 +17,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import prisma from "@calcom/prisma"; @@ -53,7 +54,7 @@ export default function Login({ totpEmail, }: // eslint-disable-next-line @typescript-eslint/ban-types inferSSRProps & WithNonceProps<{}>) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const router = useRouter(); const formSchema = z diff --git a/apps/web/pages/auth/oauth2/authorize.tsx b/apps/web/pages/auth/oauth2/authorize.tsx index e34635540c..18b9fa1404 100644 --- a/apps/web/pages/auth/oauth2/authorize.tsx +++ b/apps/web/pages/auth/oauth2/authorize.tsx @@ -1,9 +1,9 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import { APP_NAME } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Avatar, Button, Select } from "@calcom/ui"; @@ -16,7 +16,7 @@ export default function Authorize() { const { status } = useSession(); const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const client_id = searchParams?.get("client_id") as string; const state = searchParams?.get("state") as string; diff --git a/apps/web/pages/auth/saml-idp.tsx b/apps/web/pages/auth/saml-idp.tsx index 77b0ac0650..6942574a60 100644 --- a/apps/web/pages/auth/saml-idp.tsx +++ b/apps/web/pages/auth/saml-idp.tsx @@ -1,12 +1,13 @@ import { signIn } from "next-auth/react"; -import { useSearchParams } from "next/navigation"; import { useEffect } from "react"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; + import PageWrapper from "@components/PageWrapper"; // To handle the IdP initiated login flow callback export default function Page() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); useEffect(() => { const code = searchParams?.get("code"); diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx index 4badbd1804..bc128f4cff 100644 --- a/apps/web/pages/auth/setup/index.tsx +++ b/apps/web/pages/auth/setup/index.tsx @@ -1,11 +1,12 @@ import type { GetServerSidePropsContext } from "next"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useState } from "react"; import AdminAppsList from "@calcom/features/apps/AdminAppsList"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey"; import { APP_NAME } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import prisma from "@calcom/prisma"; import { UserPermissionRole } from "@calcom/prisma/enums"; @@ -21,7 +22,7 @@ import { ssrInit } from "@server/lib/ssr"; function useSetStep() { const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const setStep = (newStep = 1) => { const _searchParams = new URLSearchParams(searchParams ?? undefined); diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index ee9aa08d27..b2d8b418da 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -1,6 +1,6 @@ import type { GetServerSidePropsContext } from "next"; import { signIn } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; @@ -9,6 +9,7 @@ import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomain import stripe from "@calcom/features/ee/payments/server/stripe"; import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { checkUsername } from "@calcom/lib/server/checkUsername"; import prisma from "@calcom/prisma"; @@ -22,7 +23,7 @@ import { ssrInit } from "@server/lib/ssr"; export type SSOProviderPageProps = inferSSRProps; export default function Provider(props: SSOProviderPageProps) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const router = useRouter(); useEffect(() => { diff --git a/apps/web/pages/auth/verify.tsx b/apps/web/pages/auth/verify.tsx index d0ce633d2f..85b17a4be0 100644 --- a/apps/web/pages/auth/verify.tsx +++ b/apps/web/pages/auth/verify.tsx @@ -1,10 +1,11 @@ import { signIn } from "next-auth/react"; import Head from "next/head"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import z from "zod"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { trpc } from "@calcom/trpc/react"; import { Button, showToast } from "@calcom/ui"; @@ -54,7 +55,7 @@ const querySchema = z.object({ }); export default function Verify() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const router = useRouter(); const routerQuery = useRouterQuery(); diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index da393efe05..f3502b6f9a 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -1,4 +1,4 @@ -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; @@ -8,6 +8,7 @@ import Schedule from "@calcom/features/schedules/components/Schedule"; import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { availabilityAsString } from "@calcom/lib/availability"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; @@ -83,7 +84,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => { }; export default function Availability() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const router = useRouter(); const utils = trpc.useContext(); diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 4e4738f632..d368c2a233 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -1,11 +1,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { useCallback } from "react"; import { getLayout } from "@calcom/features/MainLayout"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { ShellMain } from "@calcom/features/shell/Shell"; import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -131,7 +132,7 @@ const WithQuery = withQuery(trpc.viewer.availability.list as any); export default function AvailabilityPage() { const { t } = useLocale(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const router = useRouter(); const pathname = usePathname(); diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 44094705b9..029652211a 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -4,7 +4,7 @@ import { createEvent } from "ics"; import type { GetServerSidePropsContext } from "next"; import { useSession } from "next-auth/react"; import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { RRule } from "rrule"; import { z } from "zod"; @@ -36,6 +36,7 @@ import { } from "@calcom/lib/date-fns"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import useGetBrandingColours from "@calcom/lib/getBrandColours"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import useTheme from "@calcom/lib/hooks/useTheme"; @@ -98,7 +99,7 @@ export default function Success(props: SuccessProps) { const router = useRouter(); const routerQuery = useRouterQuery(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { allRemainingBookings, isSuccessBookingPage, diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 468e1fde5d..7bde895793 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { User } from "@prisma/client"; import { Trans } from "next-i18next"; import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { FC } from "react"; import { memo, useEffect, useState } from "react"; import { z } from "zod"; @@ -19,6 +19,7 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF import { ShellMain } from "@calcom/features/shell/Shell"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; @@ -208,7 +209,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL const { t } = useLocale(); const router = useRouter(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const orgBranding = useOrgBranding(); const [parent] = useAutoAnimate(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -840,7 +841,7 @@ const Main = ({ filters: ReturnType; }) => { const isMobile = useMediaQuery("(max-width: 768px)"); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const orgBranding = useOrgBranding(); if (!data || status === "loading") { @@ -904,7 +905,7 @@ const Main = ({ const EventTypesPage = () => { const { t } = useLocale(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { open } = useIntercom(); const { data: user } = useMeQuery(); const [showProfileBanner, setShowProfileBanner] = useState(false); diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 21f3459f1f..0b0cb0e5a8 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import type { GetServerSidePropsContext } from "next"; import { signIn } from "next-auth/react"; -import { useSearchParams } from "next/navigation"; import type { CSSProperties } from "react"; import type { SubmitHandler } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form"; @@ -13,6 +12,7 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; @@ -53,7 +53,7 @@ function addOrUpdateQueryParam(url: string, key: string, value: string) { } export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoAcceptEmail }: SignupProps) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const telemetry = useTelemetry(); const { t, i18n } = useLocale(); const flags = useFlagMap(); diff --git a/packages/app-store/alby/components/AlbyPaymentComponent.tsx b/packages/app-store/alby/components/AlbyPaymentComponent.tsx index 34599a9800..30e7063535 100644 --- a/packages/app-store/alby/components/AlbyPaymentComponent.tsx +++ b/packages/app-store/alby/components/AlbyPaymentComponent.tsx @@ -1,11 +1,11 @@ import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import QRCode from "react-qr-code"; import z from "zod"; import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment"; import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; @@ -130,7 +130,7 @@ type PaymentCheckerProps = PaymentPageProps; function PaymentChecker(props: PaymentCheckerProps) { // TODO: move booking success code to a common lib function // TODO: subscribe rather than polling - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const bookingSuccessRedirect = useBookingSuccessRedirect(); const utils = trpc.useContext(); const { t } = useLocale(); diff --git a/packages/app-store/alby/pages/setup/index.tsx b/packages/app-store/alby/pages/setup/index.tsx index fdd8403b03..ae447708c3 100644 --- a/packages/app-store/alby/pages/setup/index.tsx +++ b/packages/app-store/alby/pages/setup/index.tsx @@ -1,10 +1,10 @@ import { auth, Client, webln } from "@getalby/sdk"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useSearchParams } from "next/navigation"; import { useState, useCallback, useEffect } from "react"; import { Toaster } from "react-hot-toast"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { Badge, Button, showToast } from "@calcom/ui"; @@ -20,7 +20,7 @@ export interface IAlbySetupProps { } export default function AlbySetup(props: IAlbySetupProps) { - const params = useSearchParams(); + const params = useCompatSearchParams(); if (params?.get("callback") === "true") { return ; } @@ -30,7 +30,7 @@ export default function AlbySetup(props: IAlbySetupProps) { function AlbySetupCallback() { const [error, setError] = useState(null); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); useEffect(() => { if (!searchParams) { diff --git a/packages/app-store/routing-forms/components/FormActions.tsx b/packages/app-store/routing-forms/components/FormActions.tsx index 6c96a221eb..088d374231 100644 --- a/packages/app-store/routing-forms/components/FormActions.tsx +++ b/packages/app-store/routing-forms/components/FormActions.tsx @@ -1,5 +1,5 @@ import type { App_RoutingForms_Form } from "@prisma/client"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { createContext, forwardRef, useContext, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; @@ -9,6 +9,7 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/features/embed/RoutingFormEmbed"; import { classNames } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { trpc } from "@calcom/trpc/react"; @@ -46,7 +47,7 @@ const newFormModalQuerySchema = z.object({ export const useOpenModal = () => { const router = useRouter(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const openModal = (option: z.infer) => { const newQuery = new URLSearchParams(searchParams ?? undefined); newQuery.set("dialog", "new-form"); diff --git a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx index e740da2f83..ae2aab94ec 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx @@ -1,5 +1,5 @@ import Head from "next/head"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import type { FormEvent } from "react"; import { useEffect, useRef, useState } from "react"; import { Toaster } from "react-hot-toast"; @@ -9,6 +9,7 @@ import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import classNames from "@calcom/lib/classNames"; import useGetBrandingColours from "@calcom/lib/getBrandColours"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { trpc } from "@calcom/trpc/react"; @@ -296,7 +297,7 @@ export const getServerSideProps = async function getServerSideProps( }; const usePrefilledResponse = (form: Props["form"]) => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const prefillResponse: Response = {}; // Prefill the form from query params diff --git a/packages/embeds/embed-core/src/embed-iframe.ts b/packages/embeds/embed-core/src/embed-iframe.ts index bb5cb8c655..a4d72bf32c 100644 --- a/packages/embeds/embed-core/src/embed-iframe.ts +++ b/packages/embeds/embed-core/src/embed-iframe.ts @@ -1,10 +1,10 @@ import { useRouter } from "next/navigation"; -import { useSearchParams } from "next/navigation"; import { useEffect, useRef, useState, useCallback } from "react"; import type { Message } from "./embed"; import { sdkActionManager } from "./sdk-event"; import type { EmbedThemeConfig, UiConfig, EmbedNonStylesConfig, BookerLayouts, EmbedStyles } from "./types"; +import { useCompatSearchParams } from "./useCompatSearchParams"; type SetStyles = React.Dispatch>; type setNonStylesConfig = React.Dispatch>; @@ -208,7 +208,7 @@ const useUrlChange = (callback: (newUrl: string) => void) => { }; export const useEmbedTheme = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const [theme, setTheme] = useState( embedStore.theme || (searchParams?.get("theme") as typeof embedStore.theme) ); diff --git a/packages/embeds/embed-core/src/useCompatSearchParams.tsx b/packages/embeds/embed-core/src/useCompatSearchParams.tsx new file mode 100644 index 0000000000..68bb1e6d6d --- /dev/null +++ b/packages/embeds/embed-core/src/useCompatSearchParams.tsx @@ -0,0 +1,21 @@ +// this file is copied from '@calcom/lib/hooks/useCompatSearchParams.tsx' +import { ReadonlyURLSearchParams, useParams, useSearchParams } from "next/navigation"; + +export const useCompatSearchParams = () => { + const _searchParams = useSearchParams() ?? new URLSearchParams(); + const params = useParams() ?? {}; + + const searchParams = new URLSearchParams(_searchParams.toString()); + Object.getOwnPropertyNames(params).forEach((key) => { + searchParams.delete(key); + + const param = params[key]; + const paramArr = typeof param === "string" ? param.split("/") : param; + + paramArr.forEach((p) => { + searchParams.append(key, p); + }); + }); + + return new ReadonlyURLSearchParams(searchParams); +}; diff --git a/packages/embeds/embed-core/tsconfig.json b/packages/embeds/embed-core/tsconfig.json index 154a1cb01f..05fc61c244 100644 --- a/packages/embeds/embed-core/tsconfig.json +++ b/packages/embeds/embed-core/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@calcom/tsconfig/base.json", "compilerOptions": { + "jsx": "react", "target": "ES2015", "module": "esnext", "moduleResolution": "Node", diff --git a/packages/embeds/embed-snippet/tsconfig.json b/packages/embeds/embed-snippet/tsconfig.json index a1dfc8123e..b2907d9204 100644 --- a/packages/embeds/embed-snippet/tsconfig.json +++ b/packages/embeds/embed-snippet/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@calcom/tsconfig/base.json", "compilerOptions": { + "jsx": "react", "target": "ES2015", "baseUrl": ".", "module": "ESNext", diff --git a/packages/features/apps/AdminAppsList.tsx b/packages/features/apps/AdminAppsList.tsx index 5c96da95bd..46277b242e 100644 --- a/packages/features/apps/AdminAppsList.tsx +++ b/packages/features/apps/AdminAppsList.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; -import { useSearchParams } from "next/navigation"; import type { FC } from "react"; import { useReducer, useState } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -10,6 +9,7 @@ import { z } from "zod"; import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation"; import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated"; import { classNames as cs } from "@calcom/lib"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AppCategories } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -266,7 +266,7 @@ interface EditModalState extends Pick { } const AdminAppsListContainer = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const category = searchParams?.get("category") || AppCategories.calendar; diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index b32440197c..dc6eb24be1 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -1,7 +1,8 @@ -import { useSearchParams, usePathname } from "next/navigation"; +import { usePathname } from "next/navigation"; import { shallow } from "zustand/shallow"; import { useSchedule } from "@calcom/features/schedules"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { trpc } from "@calcom/trpc/react"; import { useTimePreferences } from "../../lib/timePreferences"; @@ -61,7 +62,7 @@ export const useScheduleForEvent = ({ (state) => [state.username, state.eventSlug, state.month, state.selectedDuration], shallow ); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const rescheduleUid = searchParams?.get("rescheduleUid"); const pathname = usePathname(); diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index 874e795063..62d4da5518 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -2,13 +2,14 @@ import type { Payment } from "@prisma/client"; import type { EventType } from "@prisma/client"; import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"; import type { StripeElementLocale } from "@stripe/stripe-js"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import type { SyntheticEvent } from "react"; import { useEffect, useState } from "react"; import getStripe from "@calcom/app-store/stripepayment/lib/client"; import { getBookingRedirectExtraParams, useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; import { CAL_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, CheckboxField } from "@calcom/ui"; @@ -69,7 +70,7 @@ const PaymentForm = (props: Props) => { } = props; const { t, i18n } = useLocale(); const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const [state, setState] = useState({ status: "idle" }); const [isCanceling, setIsCanceling] = useState(false); const stripe = useStripe(); diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index 1b892aa1be..750f8ecbae 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -1,5 +1,5 @@ import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useState } from "react"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; @@ -8,6 +8,7 @@ import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberIn import { classNames } from "@calcom/lib"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -30,7 +31,7 @@ type FormValues = { }; const AddNewTeamMembers = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const session = useSession(); const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1; const teamQuery = trpc.viewer.teams.get.useQuery( @@ -49,7 +50,7 @@ export const AddNewTeamMembersForm = ({ defaultValues: FormValues; teamId: number; }) => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const router = useRouter(); diff --git a/packages/features/ee/teams/components/TeamListItem.tsx b/packages/features/ee/teams/components/TeamListItem.tsx index fcb88ba118..6be6a2344d 100644 --- a/packages/features/ee/teams/components/TeamListItem.tsx +++ b/packages/features/ee/teams/components/TeamListItem.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -7,6 +6,7 @@ import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSetti import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal"; import classNames from "@calcom/lib/classNames"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { MembershipRole } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -53,7 +53,7 @@ interface Props { } export default function TeamListItem(props: Props) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const utils = trpc.useContext(); const team = props.team; diff --git a/packages/features/ee/teams/components/TeamsListing.tsx b/packages/features/ee/teams/components/TeamsListing.tsx index f121ea9e8f..5b909484c7 100644 --- a/packages/features/ee/teams/components/TeamsListing.tsx +++ b/packages/features/ee/teams/components/TeamsListing.tsx @@ -1,7 +1,8 @@ -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Alert, Button, ButtonGroup, EmptyScreen, Label, showToast } from "@calcom/ui"; @@ -12,7 +13,7 @@ import SkeletonLoaderTeamList from "./SkeletonloaderTeamList"; import TeamList from "./TeamList"; export function TeamsListing() { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const token = searchParams?.get("token"); const { t } = useLocale(); const trpcContext = trpc.useContext(); diff --git a/packages/features/ee/teams/pages/team-members-view.tsx b/packages/features/ee/teams/pages/team-members-view.tsx index 6b7ff9b14a..553a8a449c 100644 --- a/packages/features/ee/teams/pages/team-members-view.tsx +++ b/packages/features/ee/teams/pages/team-members-view.tsx @@ -1,7 +1,8 @@ import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -73,7 +74,7 @@ function MembersList(props: MembersListProps) { } const MembersView = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const router = useRouter(); diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 2a39590dc9..50e023ba76 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -1,7 +1,7 @@ import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import { useSession } from "next-auth/react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { RefObject } from "react"; import { createRef, useRef, useState } from "react"; import type { ControlProps } from "react-select"; @@ -21,6 +21,7 @@ import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; import { APP_NAME, CAL_URL } from "@calcom/lib/constants"; import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -57,7 +58,7 @@ const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl", "eventId" function useRouterHelpers() { const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const goto = (newSearchParams: Record) => { @@ -507,7 +508,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({ types: EmbedTypes; }) => { const { t } = useLocale(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const { goto, removeQueryParams } = useRouterHelpers(); const iframeRef = useRef(null); @@ -1095,7 +1096,7 @@ export const EmbedDialog = ({ tabs: EmbedTabs; eventTypeHideOptionDisabled: boolean; }) => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const embedUrl = searchParams?.get("embedUrl") as string; return ( diff --git a/packages/features/eventtypes/components/DuplicateDialog.tsx b/packages/features/eventtypes/components/DuplicateDialog.tsx index 86f1e7da9e..0095b8b340 100644 --- a/packages/features/eventtypes/components/DuplicateDialog.tsx +++ b/packages/features/eventtypes/components/DuplicateDialog.tsx @@ -1,9 +1,9 @@ -import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; @@ -33,7 +33,7 @@ const querySchema = z.object({ }); const DuplicateDialog = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const router = useRouter(); const [firstRender, setFirstRender] = useState(true); diff --git a/packages/features/insights/context/FiltersProvider.tsx b/packages/features/insights/context/FiltersProvider.tsx index 2eec6e91b3..98ed7542a7 100644 --- a/packages/features/insights/context/FiltersProvider.tsx +++ b/packages/features/insights/context/FiltersProvider.tsx @@ -1,8 +1,9 @@ -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useState } from "react"; import { z } from "zod"; import dayjs from "@calcom/dayjs"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { trpc } from "@calcom/trpc"; import type { FilterContextType } from "./provider"; @@ -21,7 +22,7 @@ const querySchema = z.object({ export function FiltersProvider({ children }: { children: React.ReactNode }) { // searchParams to get initial values from query params const utils = trpc.useContext(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const router = useRouter(); const pathname = usePathname(); diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 25f14bc57f..95f217cb15 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -1,7 +1,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { useSession } from "next-auth/react"; import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { ComponentProps } from "react"; import React, { Suspense, useEffect, useState } from "react"; @@ -10,6 +10,7 @@ import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; @@ -194,7 +195,7 @@ const SettingsSidebarContainer = ({ navigationIsOpenedOnMobile, bannersHeight, }: SettingsSidebarContainerProps) => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const tabsWithPermissions = useTabs(); const [teamMenuState, setTeamMenuState] = diff --git a/packages/features/webhooks/pages/webhook-edit-view.tsx b/packages/features/webhooks/pages/webhook-edit-view.tsx index e44ffeb5e6..8da185a933 100644 --- a/packages/features/webhooks/pages/webhook-edit-view.tsx +++ b/packages/features/webhooks/pages/webhook-edit-view.tsx @@ -1,6 +1,7 @@ -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { APP_NAME } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Meta, showToast, SkeletonContainer } from "@calcom/ui"; @@ -11,7 +12,7 @@ import WebhookForm from "../components/WebhookForm"; import { subscriberUrlReserved } from "../lib/subscriberUrlReserved"; const EditWebhook = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const id = searchParams?.get("id"); if (!id) return ; diff --git a/packages/features/webhooks/pages/webhook-new-view.tsx b/packages/features/webhooks/pages/webhook-new-view.tsx index 17574ab183..e6c35c80a4 100644 --- a/packages/features/webhooks/pages/webhook-new-view.tsx +++ b/packages/features/webhooks/pages/webhook-new-view.tsx @@ -1,7 +1,8 @@ import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { APP_NAME } from "@calcom/lib/constants"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Meta, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui"; @@ -23,7 +24,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st ); }; const NewWebhookView = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const utils = trpc.useContext(); const router = useRouter(); diff --git a/packages/lib/bookingSuccessRedirect.ts b/packages/lib/bookingSuccessRedirect.ts index b1e3c6a8a0..9d1095ee2b 100644 --- a/packages/lib/bookingSuccessRedirect.ts +++ b/packages/lib/bookingSuccessRedirect.ts @@ -1,8 +1,9 @@ import type { EventType } from "@prisma/client"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import type { PaymentPageProps } from "@calcom/ee/payments/pages/payment"; import type { BookingResponse } from "@calcom/features/bookings/types"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; function getNewSeachParams(args: { query: Record; @@ -41,7 +42,7 @@ export const getBookingRedirectExtraParams = (booking: SuccessRedirectBookingTyp export const useBookingSuccessRedirect = () => { const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const bookingSuccessRedirect = ({ successRedirectUrl, query, diff --git a/packages/lib/hooks/useCompatSearchParams.test.ts b/packages/lib/hooks/useCompatSearchParams.test.ts new file mode 100644 index 0000000000..72307663d0 --- /dev/null +++ b/packages/lib/hooks/useCompatSearchParams.test.ts @@ -0,0 +1,67 @@ +import { renderHook } from "@testing-library/react-hooks"; +import { vi } from "vitest"; +import { describe, expect, it } from "vitest"; + +import { useCompatSearchParams } from "./useCompatSearchParams"; + +vi.mock("next/navigation", () => ({ + ReadonlyURLSearchParams: vi.fn((a) => a), +})); + +describe("useCompatSearchParams hook", () => { + it("should return the searchParams in next@13.4.6 Pages Router, SSR", async () => { + const navigation = await import("next/navigation"); + + navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a&b=b")); + navigation.useParams = vi.fn().mockReturnValue(null); + + const { result } = renderHook(() => useCompatSearchParams()); + + expect(result.current.toString()).toEqual("a=a&b=b"); + }); + + it("should return both searchParams and params in next@13.4.6 App Router, SSR", async () => { + const navigation = await import("next/navigation"); + + navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a")); + navigation.useParams = vi.fn().mockReturnValue({ b: "b" }); + + const { result } = renderHook(() => useCompatSearchParams()); + + expect(result.current.toString()).toEqual("a=a&b=b"); + }); + + it("params should always override searchParams in case of conflicting keys", async () => { + const navigation = await import("next/navigation"); + + navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a")); + navigation.useParams = vi.fn().mockReturnValue({ a: "b" }); + + const { result } = renderHook(() => useCompatSearchParams()); + + expect(result.current.toString()).toEqual("a=b"); + }); + + it("should split paramsseparated with '/' (catch-all segments) in next@13.4.6 App Router, SSR", async () => { + const navigation = await import("next/navigation"); + + navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams()); + // in next@13.4.6 useParams will return params separated by `/` + navigation.useParams = vi.fn().mockReturnValue({ a: "a/b/c" }); + + const { result } = renderHook(() => useCompatSearchParams()); + + expect(result.current.getAll("a")).toEqual(["a", "b", "c"]); + }); + + it("should include params and searchParams in next@13.5.4, Pages/App Router, SSR", async () => { + const navigation = await import("next/navigation"); + + navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a")); + navigation.useParams = vi.fn().mockReturnValue({ b: "b" }); + + const { result } = renderHook(() => useCompatSearchParams()); + + expect(result.current.toString()).toEqual("a=a&b=b"); + }); +}); diff --git a/packages/lib/hooks/useCompatSearchParams.tsx b/packages/lib/hooks/useCompatSearchParams.tsx new file mode 100644 index 0000000000..032ba115a1 --- /dev/null +++ b/packages/lib/hooks/useCompatSearchParams.tsx @@ -0,0 +1,20 @@ +import { ReadonlyURLSearchParams, useParams, useSearchParams } from "next/navigation"; + +export const useCompatSearchParams = () => { + const _searchParams = useSearchParams() ?? new URLSearchParams(); + const params = useParams() ?? {}; + + const searchParams = new URLSearchParams(_searchParams.toString()); + Object.getOwnPropertyNames(params).forEach((key) => { + searchParams.delete(key); + + const param = params[key]; + const paramArr = typeof param === "string" ? param.split("/") : param; + + paramArr.forEach((p) => { + searchParams.append(key, p); + }); + }); + + return new ReadonlyURLSearchParams(searchParams); +}; diff --git a/packages/lib/hooks/useRouterQuery.ts b/packages/lib/hooks/useRouterQuery.ts index 5a923f284d..d1c1ebfd77 100644 --- a/packages/lib/hooks/useRouterQuery.ts +++ b/packages/lib/hooks/useRouterQuery.ts @@ -1,4 +1,4 @@ -import { useSearchParams } from "next/navigation"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; /** * An alternative to Object.fromEntries that allows duplicate keys. @@ -35,7 +35,7 @@ function fromEntriesWithDuplicateKeys(entries: IterableIterator<[string, string] * @returns {Object} routerQuery */ export const useRouterQuery = () => { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const routerQuery = fromEntriesWithDuplicateKeys(searchParams?.entries() ?? null); return routerQuery; }; diff --git a/packages/lib/hooks/useUrlMatchesCurrentUrl.ts b/packages/lib/hooks/useUrlMatchesCurrentUrl.ts index 7fc0c5fe2c..f022561350 100644 --- a/packages/lib/hooks/useUrlMatchesCurrentUrl.ts +++ b/packages/lib/hooks/useUrlMatchesCurrentUrl.ts @@ -1,12 +1,14 @@ "use client"; -import { usePathname, useSearchParams } from "next/navigation"; +import { usePathname } from "next/navigation"; + +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; export const useUrlMatchesCurrentUrl = (url: string) => { // I don't know why usePathname ReturnType doesn't include null. // It can certainly have null value https://nextjs.org/docs/app/api-reference/functions/use-pathname#:~:text=usePathname%20can%20return%20null%20when%20a%20fallback%20route%20is%20being%20rendered%20or%20when%20a%20pages%20directory%20page%20has%20been%20automatically%20statically%20optimized%20by%20Next.js%20and%20the%20router%20is%20not%20ready. const pathname = usePathname() as null | string; - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const query = searchParams?.toString(); let pathnameWithQuery; if (query) { diff --git a/packages/ui/components/apps/AllApps.tsx b/packages/ui/components/apps/AllApps.tsx index 4a906b1e87..665d87bd43 100644 --- a/packages/ui/components/apps/AllApps.tsx +++ b/packages/ui/components/apps/AllApps.tsx @@ -1,11 +1,12 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { AppCategories } from "@prisma/client"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { UIEvent } from "react"; import { useEffect, useRef, useState } from "react"; import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams"; import { classNames } from "@calcom/lib"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AppFrontendPayload as App } from "@calcom/types/App"; import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential"; @@ -55,7 +56,7 @@ interface CategoryTabProps { function CategoryTab({ selectedCategory, categories, searchText }: CategoryTabProps) { const pathname = usePathname(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const router = useRouter(); const { ref, calculateScroll, leftVisible, rightVisible } = useShouldShowArrows(); @@ -140,7 +141,7 @@ function CategoryTab({ selectedCategory, categories, searchText }: CategoryTabPr } export function AllApps({ apps, searchText, categories, userAdminTeams }: AllAppsPropsType) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { t } = useLocale(); const [selectedCategory, setSelectedCategory] = useState(null); const [appsContainerRef, enableAnimation] = useAutoAnimate(); diff --git a/packages/ui/components/createButton/CreateButton.tsx b/packages/ui/components/createButton/CreateButton.tsx index f42b4225e9..51b4a3c973 100644 --- a/packages/ui/components/createButton/CreateButton.tsx +++ b/packages/ui/components/createButton/CreateButton.tsx @@ -1,6 +1,7 @@ -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { ButtonColor } from "@calcom/ui"; import { @@ -40,7 +41,7 @@ export type CreateBtnProps = { export function CreateButton(props: CreateBtnProps) { const { t } = useLocale(); const router = useRouter(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const pathname = usePathname(); const bookerUrl = useBookerUrl(); diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 7565c0524c..06971f9745 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -1,9 +1,10 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import type { ReactNode } from "react"; import React, { useState } from "react"; import classNames from "@calcom/lib/classNames"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { SVGComponent } from "@calcom/types/SVGComponent"; @@ -27,7 +28,7 @@ const enum DIALOG_STATE { export function Dialog(props: DialogProps) { const router = useRouter(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const newSearchParams = new URLSearchParams(searchParams ?? undefined); const { children, name, ...dialogProps } = props; diff --git a/packages/ui/components/dialog/dialog.test.tsx b/packages/ui/components/dialog/dialog.test.tsx index 994b821e04..061bc386a5 100644 --- a/packages/ui/components/dialog/dialog.test.tsx +++ b/packages/ui/components/dialog/dialog.test.tsx @@ -3,6 +3,12 @@ import { vi } from "vitest"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "./Dialog"; +vi.mock("@calcom/lib/hooks/useCompatSearchParams", () => ({ + useCompatSearchParams() { + return new URLSearchParams(); + }, +})); + vi.mock("next/navigation", () => ({ usePathname() { return ""; diff --git a/packages/ui/components/form/wizard/WizardForm.tsx b/packages/ui/components/form/wizard/WizardForm.tsx index c532a25303..72fcf9aeb1 100644 --- a/packages/ui/components/form/wizard/WizardForm.tsx +++ b/packages/ui/components/form/wizard/WizardForm.tsx @@ -1,10 +1,11 @@ // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import type { Dispatch, SetStateAction } from "react"; import { useEffect, useState } from "react"; import classNames from "@calcom/lib/classNames"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { Button, Steps } from "../../.."; @@ -28,7 +29,7 @@ function WizardForm(props: { finishLabel?: string; stepLabel?: React.ComponentProps["stepLabel"]; }) { - const searchParams = useSearchParams(); + const searchParams = useCompatSearchParams(); const { href, steps, nextLabel = "Next", finishLabel = "Finish", prevLabel = "Back", stepLabel } = props; const router = useRouter(); const step = parseInt((searchParams?.get("step") as string) || "1"); diff --git a/packages/ui/components/form/wizard/wizardForm.test.tsx b/packages/ui/components/form/wizard/wizardForm.test.tsx index 8cf7004d17..e52a5b5f8f 100644 --- a/packages/ui/components/form/wizard/wizardForm.test.tsx +++ b/packages/ui/components/form/wizard/wizardForm.test.tsx @@ -4,6 +4,12 @@ import { vi } from "vitest"; import WizardForm from "./WizardForm"; +vi.mock("@calcom/lib/hooks/useCompatSearchParams", () => ({ + useCompatSearchParams() { + return { get: vi.fn().mockReturnValue(currentStepNavigation) }; + }, +})); + vi.mock("next/navigation", () => ({ useRouter() { return { replace: vi.fn() }; diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 1b59d9ba80..70fe2276d9 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -72,6 +72,7 @@ const workspaces = packagedEmbedTestsOnly name: "@calcom/packages/lib/hooks", include: ["packages/lib/hooks/**/*.{test,spec}.{ts,js}"], environment: "jsdom", + setupFiles: [], }, }, ]; From 57a65401e7a8355aeb13953823e1bf8dcc99f409 Mon Sep 17 00:00:00 2001 From: Somay Chauhan Date: Fri, 17 Nov 2023 02:39:48 +0530 Subject: [PATCH 074/119] fix: weird date override behaviour (#12292) Co-authored-by: Peer Richelsen --- .../components/DateOverrideInputDialog.tsx | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/features/schedules/components/DateOverrideInputDialog.tsx b/packages/features/schedules/components/DateOverrideInputDialog.tsx index 84ec723052..48612ff4ce 100644 --- a/packages/features/schedules/components/DateOverrideInputDialog.tsx +++ b/packages/features/schedules/components/DateOverrideInputDialog.tsx @@ -82,21 +82,22 @@ const DateOverrideForm = ({ const form = useForm({ values: { - range: value - ? value.map((range) => ({ - start: new Date( - dayjs - .utc() - .hour(range.start.getUTCHours()) - .minute(range.start.getUTCMinutes()) - .second(0) - .format() - ), - end: new Date( - dayjs.utc().hour(range.end.getUTCHours()).minute(range.end.getUTCMinutes()).second(0).format() - ), - })) - : defaultRanges, + range: + value && value[0].start.valueOf() !== value[0].end.valueOf() + ? value.map((range) => ({ + start: new Date( + dayjs + .utc() + .hour(range.start.getUTCHours()) + .minute(range.start.getUTCMinutes()) + .second(0) + .format() + ), + end: new Date( + dayjs.utc().hour(range.end.getUTCHours()).minute(range.end.getUTCMinutes()).second(0).format() + ), + })) + : defaultRanges, }, }); @@ -128,7 +129,7 @@ const DateOverrideForm = ({ ? selectedDates.map((date) => { return { start: date.utc(true).startOf("day").toDate(), - end: date.utc(true).startOf("day").add(1, "day").toDate(), + end: date.utc(true).startOf("day").toDate(), }; }) : datesInRanges From 27d59b6413144fcac224d644f814cc27bdfb3806 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:01:13 +0200 Subject: [PATCH 075/119] fix: add idx scheduleId on eventType (#12400) --- .../20231117081852_idx_eventtype_scheduleid/migration.sql | 2 ++ packages/prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 packages/prisma/migrations/20231117081852_idx_eventtype_scheduleid/migration.sql diff --git a/packages/prisma/migrations/20231117081852_idx_eventtype_scheduleid/migration.sql b/packages/prisma/migrations/20231117081852_idx_eventtype_scheduleid/migration.sql new file mode 100644 index 0000000000..48eb119e72 --- /dev/null +++ b/packages/prisma/migrations/20231117081852_idx_eventtype_scheduleid/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "EventType_scheduleId_idx" ON "EventType"("scheduleId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c9c2bf6596..004fcc3147 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -123,6 +123,7 @@ model EventType { @@unique([userId, parentId]) @@index([userId]) @@index([teamId]) + @@index([scheduleId]) } model Credential { From 176564f4ed97dc48c99b60658555547ea854187f Mon Sep 17 00:00:00 2001 From: Mehul Date: Fri, 17 Nov 2023 19:40:58 +0530 Subject: [PATCH 076/119] fix: logo lack of contrast (#12401) Co-authored-by: Peer Richelsen --- .../event-meta/AvailableEventLocations.tsx | 2 +- packages/lib/invertLogoOnDark.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx index cc8b6e253e..0eca36957c 100644 --- a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx +++ b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx @@ -22,7 +22,7 @@ function RenderIcon({ return ( {`${eventLocationType.label} ); diff --git a/packages/lib/invertLogoOnDark.ts b/packages/lib/invertLogoOnDark.ts index 13aa216a95..657751b7a2 100644 --- a/packages/lib/invertLogoOnDark.ts +++ b/packages/lib/invertLogoOnDark.ts @@ -1,6 +1,15 @@ // we want to invert all logos that contain -dark in their name // we don't want to invert logos that are not coming from the app-store -export default function invertLogoOnDark(url?: string) { - return (url?.includes("-dark") || !url?.startsWith("/app-store")) && "dark:invert"; +export default function invertLogoOnDark( + url?: string, + // The background color of the logo's display location is opposite of the general theme of the application. + // Ex. General App theme is black but the logo's background color is white. + opposite?: boolean +) { + return url?.includes("-dark") || !url?.startsWith("/app-store") + ? opposite + ? "invert dark:invert-0" + : "dark:invert" + : ""; } From c818ef318830ab61bac51f49e16cb9d0aa450351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Fri, 17 Nov 2023 09:06:29 -0700 Subject: [PATCH 077/119] feat: implements basic user locking for admins (#12393) * feat: implements basic user locking for admins * Update sendPasswordReset.handler.ts * check fixes * Update packages/features/ee/users/components/UsersTable.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --- packages/features/auth/lib/ErrorCode.ts | 1 + .../features/auth/lib/next-auth-options.ts | 6 ++ .../ee/users/components/UsersTable.tsx | 36 +++++++++++ packages/lib/test/builder.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 3 + .../server/middlewares/sessionMiddleware.ts | 2 + .../server/routers/viewer/admin/_router.ts | 62 +++++++------------ .../viewer/admin/listPaginated.handler.ts | 5 +- .../viewer/admin/lockUserAccount.handler.ts | 36 +++++++++++ .../viewer/admin/lockUserAccount.schema.ts | 8 +++ .../viewer/admin/sendPasswordReset.handler.ts | 4 +- 12 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 packages/prisma/migrations/20231117002911_add_users_locked/migration.sql create mode 100644 packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts create mode 100644 packages/trpc/server/routers/viewer/admin/lockUserAccount.schema.ts diff --git a/packages/features/auth/lib/ErrorCode.ts b/packages/features/auth/lib/ErrorCode.ts index c4f86af26b..4f5bdd435b 100644 --- a/packages/features/auth/lib/ErrorCode.ts +++ b/packages/features/auth/lib/ErrorCode.ts @@ -16,4 +16,5 @@ export enum ErrorCode { ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled", RateLimitExceeded = "rate-limit-exceeded", SocialIdentityProviderRequired = "social-identity-provider-required", + UserAccountLocked = "user-account-locked", } diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 97cc306065..ebe299571f 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -105,6 +105,7 @@ const providers: Provider[] = [ email: credentials.email.toLowerCase(), }, select: { + locked: true, role: true, id: true, username: true, @@ -136,6 +137,11 @@ const providers: Provider[] = [ throw new Error(ErrorCode.IncorrectEmailPassword); } + // Locked users cannot login + if (user.locked) { + throw new Error(ErrorCode.UserAccountLocked); + } + await checkRateLimitAndThrowError({ identifier: user.email, }); diff --git a/packages/features/ee/users/components/UsersTable.tsx b/packages/features/ee/users/components/UsersTable.tsx index 96340689a0..e4af526cbd 100644 --- a/packages/features/ee/users/components/UsersTable.tsx +++ b/packages/features/ee/users/components/UsersTable.tsx @@ -76,6 +76,31 @@ function UsersTableBare() { }, }); + const lockUserAccount = trpc.viewer.admin.lockUserAccount.useMutation({ + onSuccess: ({ userId, locked }) => { + showToast(locked ? "User was locked" : "User was unlocked", "success"); + utils.viewer.admin.listPaginated.setInfiniteData({ limit: FETCH_LIMIT }, (cachedData) => { + if (!cachedData) { + return { + pages: [], + pageParams: [], + }; + } + return { + ...cachedData, + pages: cachedData.pages.map((page) => ({ + ...page, + rows: page.rows.map((row) => { + const newUser = row; + if (row.id === userId) newUser.locked = locked; + return newUser; + }), + })), + }; + }); + }, + }); + //we must flatten the array of arrays from the useInfiniteQuery hook const flatData = useMemo(() => data?.pages?.flatMap((page) => page.rows) ?? [], [data]); const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0; @@ -140,6 +165,11 @@ function UsersTableBare() {
{user.name} /{user.username} + {user.locked && ( + + + + )}
{user.email}
@@ -167,6 +197,12 @@ function UsersTableBare() { onClick: () => sendPasswordResetEmail.mutate({ userId: user.id }), icon: Lock, }, + { + id: "lock-user", + label: user.locked ? "Unlock User Account" : "Lock User Account", + onClick: () => lockUserAccount.mutate({ userId: user.id, locked: !user.locked }), + icon: Lock, + }, { id: "delete", label: "Delete", diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index bd9e9fc516..e9eb598659 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -182,6 +182,7 @@ type UserPayload = Prisma.UserGetPayload<{ }>; export const buildUser = >(user?: T): UserPayload => { return { + locked: false, name: faker.name.firstName(), email: faker.internet.email(), timeZone: faker.address.timeZone(), diff --git a/packages/prisma/migrations/20231117002911_add_users_locked/migration.sql b/packages/prisma/migrations/20231117002911_add_users_locked/migration.sql new file mode 100644 index 0000000000..94f7ebc2c8 --- /dev/null +++ b/packages/prisma/migrations/20231117002911_add_users_locked/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "locked" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 004fcc3147..3130a9d423 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -259,6 +259,9 @@ model User { //linkedBy User? @relation("linked_account", fields: [linkedByUserId], references: [id], onDelete: Cascade) //linkedUsers User[] @relation("linked_account")*/ + // Used to lock the user account + locked Boolean @default(false) + @@unique([email]) @@unique([email, username]) @@unique([username, organizationId]) diff --git a/packages/trpc/server/middlewares/sessionMiddleware.ts b/packages/trpc/server/middlewares/sessionMiddleware.ts index 2a9e072bd8..46b55b6450 100644 --- a/packages/trpc/server/middlewares/sessionMiddleware.ts +++ b/packages/trpc/server/middlewares/sessionMiddleware.ts @@ -20,6 +20,8 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe `${NAMESPACE}.${s}`; export const adminRouter = router({ - listPaginated: authedAdminProcedure.input(ZListMembersSchema).query(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.listPaginated) { - UNSTABLE_HANDLER_CACHE.listPaginated = await import("./listPaginated.handler").then( - (mod) => mod.listPaginatedHandler - ); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.listPaginated) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.listPaginated({ - ctx, - input, - }); + listPaginated: authedAdminProcedure.input(ZListMembersSchema).query(async (opts) => { + const handler = await importHandler(namespaced("listPaginated"), () => import("./listPaginated.handler")); + return handler(opts); + }), + sendPasswordReset: authedAdminProcedure.input(ZAdminPasswordResetSchema).mutation(async (opts) => { + const handler = await importHandler( + namespaced("sendPasswordReset"), + () => import("./sendPasswordReset.handler") + ); + return handler(opts); + }), + lockUserAccount: authedAdminProcedure.input(ZAdminLockUserAccountSchema).mutation(async (opts) => { + const handler = await importHandler( + namespaced("lockUserAccount"), + () => import("./lockUserAccount.handler") + ); + return handler(opts); }), - sendPasswordReset: authedAdminProcedure - .input(ZAdminPasswordResetSchema) - .mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.sendPasswordReset) { - UNSTABLE_HANDLER_CACHE.sendPasswordReset = await import("./sendPasswordReset.handler").then( - (mod) => mod.sendPasswordResetHandler - ); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.sendPasswordReset) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.sendPasswordReset({ - ctx, - input, - }); - }), toggleFeatureFlag: authedAdminProcedure .input(z.object({ slug: z.string(), enabled: z.boolean() })) .mutation(({ ctx, input }) => { diff --git a/packages/trpc/server/routers/viewer/admin/listPaginated.handler.ts b/packages/trpc/server/routers/viewer/admin/listPaginated.handler.ts index 59924347e0..d0b43d918a 100644 --- a/packages/trpc/server/routers/viewer/admin/listPaginated.handler.ts +++ b/packages/trpc/server/routers/viewer/admin/listPaginated.handler.ts @@ -11,7 +11,7 @@ type GetOptions = { input: TListMembersSchema; }; -export const listPaginatedHandler = async ({ input }: GetOptions) => { +const listPaginatedHandler = async ({ input }: GetOptions) => { const { cursor, limit, searchTerm } = input; const getTotalUsers = await prisma.user.count(); @@ -44,6 +44,7 @@ export const listPaginatedHandler = async ({ input }: GetOptions) => { }, select: { id: true, + locked: true, email: true, username: true, name: true, @@ -67,3 +68,5 @@ export const listPaginatedHandler = async ({ input }: GetOptions) => { }, }; }; + +export default listPaginatedHandler; diff --git a/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts b/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts new file mode 100644 index 0000000000..64c3ad6eb5 --- /dev/null +++ b/packages/trpc/server/routers/viewer/admin/lockUserAccount.handler.ts @@ -0,0 +1,36 @@ +import { prisma } from "@calcom/prisma"; + +import type { TrpcSessionUser } from "../../../trpc"; +import type { TAdminLockUserAccountSchema } from "./lockUserAccount.schema"; + +type GetOptions = { + ctx: { + user: NonNullable; + }; + input: TAdminLockUserAccountSchema; +}; + +const lockUserAccountHandler = async ({ input }: GetOptions) => { + const { userId, locked } = input; + + const user = await prisma.user.update({ + where: { + id: userId, + }, + data: { + locked, + }, + }); + + if (!user) { + throw new Error("User not found"); + } + + return { + success: true, + userId, + locked, + }; +}; + +export default lockUserAccountHandler; diff --git a/packages/trpc/server/routers/viewer/admin/lockUserAccount.schema.ts b/packages/trpc/server/routers/viewer/admin/lockUserAccount.schema.ts new file mode 100644 index 0000000000..e5a9434f19 --- /dev/null +++ b/packages/trpc/server/routers/viewer/admin/lockUserAccount.schema.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const ZAdminLockUserAccountSchema = z.object({ + userId: z.number(), + locked: z.boolean(), +}); + +export type TAdminLockUserAccountSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/admin/sendPasswordReset.handler.ts b/packages/trpc/server/routers/viewer/admin/sendPasswordReset.handler.ts index b15f4e758c..0c222a824c 100644 --- a/packages/trpc/server/routers/viewer/admin/sendPasswordReset.handler.ts +++ b/packages/trpc/server/routers/viewer/admin/sendPasswordReset.handler.ts @@ -14,7 +14,7 @@ type GetOptions = { input: TAdminPasswordResetSchema; }; -export const sendPasswordResetHandler = async ({ input }: GetOptions) => { +const sendPasswordResetHandler = async ({ input }: GetOptions) => { const { userId } = input; const user = await prisma.user.findUnique({ @@ -57,3 +57,5 @@ export const sendPasswordResetHandler = async ({ input }: GetOptions) => { success: true, }; }; + +export default sendPasswordResetHandler; From e83e0a770cab5040fd87b1bc6b11b26ec410f32a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 17 Nov 2023 16:09:31 +0000 Subject: [PATCH 078/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 9f80e2ca70..f145476199 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -2079,6 +2079,7 @@ "add_new_client": "Ajouter un nouveau client", "as_csv": "au format CSV", "overlay_my_calendar": "Superposer mon calendrier", + "overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.", "view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.", "lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation", "description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.", From f2c39fc7861b1f4f32ac07f572d7c9b135b6737e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 17 Nov 2023 16:12:37 +0000 Subject: [PATCH 079/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index f145476199..e0bab7ee71 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -409,7 +409,7 @@ "automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité", "user_dynamic_booking_disabled": "Certains utilisateurs du groupe ont actuellement désactivé les réservations de groupe dynamiques", "allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».", - "allow_dynamic_booking": "Autoriser les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques", + "allow_dynamic_booking": "Autorisez les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques.", "dynamic_booking": "Liens de groupe dynamiques", "allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.", "seo_indexing": "Autoriser l'indexation SEO", @@ -2030,7 +2030,7 @@ "summary_of_events_for_your_team_for_the_last_30_days": "Voici votre résumé des événements populaires pour votre équipe {{teamName}} au cours des 30 derniers jours", "me": "Moi", "monthly_digest_email": "E-mail de résumé mensuel", - "monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes", + "monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes.", "verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants", "member_removed": "Membre supprimé", "my_availability": "Mes disponibilités", From 2144fcb23e6abba42f7d1b81952f15692421eeec Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 20 Nov 2023 16:34:29 +0530 Subject: [PATCH 080/119] fix: add node-mocks-http to web (#12435) --- apps/web/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index d9ad6f022d..e565cb0d4e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -168,6 +168,7 @@ "module-alias": "^2.2.2", "msw": "^0.42.3", "node-html-parser": "^6.1.10", + "node-mocks-http": "^1.11.0", "postcss": "^8.4.18", "tailwindcss": "^3.3.1", "tailwindcss-animate": "^1.0.6", diff --git a/yarn.lock b/yarn.lock index f03d7d2160..4e231a4e0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4604,6 +4604,7 @@ __metadata: next-seo: ^6.0.0 next-themes: ^0.2.0 node-html-parser: ^6.1.10 + node-mocks-http: ^1.11.0 nodemailer: ^6.7.8 otplib: ^12.0.1 postcss: ^8.4.18 From 9a4c20cca457efa666b5a74fd15ea541ecee0db5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 20 Nov 2023 11:08:01 +0000 Subject: [PATCH 081/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/he/common.json | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index c98de025cd..fdb11fce61 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -268,6 +268,7 @@ "set_availability": "ציין את הזמינות שלך", "availability_settings": "הגדרות זמינוּת", "continue_without_calendar": "להמשיך בלי לוח שנה", + "continue_with": "להמשיך עם {{appName}}", "connect_your_calendar": "קשר את לוח השנה שלך", "connect_your_video_app": "חיבור אפליקציות הווידאו שלך", "connect_your_video_app_instructions": "חבר/י את אפליקציות הווידאו שלך כדי להשתמש בהן עבור סוגי האירועים שלך.", @@ -288,6 +289,8 @@ "when": "מתי", "where": "היכן", "add_to_calendar": "הוספה ללוח השנה", + "add_to_calendar_description": "בחר/י להיכן יש להוסיף אירועים כשאת/ה עסוק/ה.", + "add_events_to": "הוספת אירועים ל", "add_another_calendar": "להוסיף לוח שנה אחר", "other": "אחר", "email_sign_in_subject": "קישור הכניסה שלך אל {{appName}}", @@ -422,6 +425,7 @@ "booking_created": "ההזמנה נוצרה", "booking_rejected": "ההזמנה נדחתה", "booking_requested": "התקבלה בקשת הזמנה", + "booking_payment_initiated": "הופעל תשלום על ההזמנה", "meeting_ended": "הפגישה הסתיימה", "form_submitted": "הטופס נשלח", "booking_paid": "בוצע תשלום עבור ההזמנה", @@ -456,6 +460,7 @@ "no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.", "edit_logo": "עריכת לוגו", "upload_a_logo": "העלאת לוגו", + "upload_logo": "העלאת לוגו", "remove_logo": "הסרת לוגו", "enable": "הפעלה", "code": "קוד", @@ -568,6 +573,7 @@ "your_team_name": "שם הצוות שלך", "team_updated_successfully": "עדכון הצוות בוצע בהצלחה", "your_team_updated_successfully": "הצוות שלך עודכן בהצלחה.", + "your_org_updated_successfully": "הארגון שלך עודכן בהצלחה.", "about": "אודות", "team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.", "org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.", @@ -599,6 +605,7 @@ "hide_book_a_team_member": "הסתרת הלחצן לשריון זמן של חבר/ת צוות", "hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.", "danger_zone": "אזור מסוכן", + "account_deletion_cannot_be_undone": "יש לנקוט זהירות. מחיקת חשבון היא פעולה בלתי הפיכה.", "back": "הקודם", "cancel": "ביטול", "cancel_all_remaining": "לבטל את כל הנותרים", @@ -688,6 +695,7 @@ "people": "אנשים", "your_email": "הדוא\"ל שלך", "change_avatar": "שינוי אווטאר", + "upload_avatar": "העלאת אווטאר", "language": "שפה", "timezone": "אזור זמן", "first_day_of_week": "היום הראשון בשבוע", @@ -778,6 +786,7 @@ "disable_guests": "השבתת אורחים", "disable_guests_description": "השבת את האפשרות להוסיף אורחים נוספים בעת ביצוע הזמנה.", "private_link": "יצירת קישור פרטי", + "enable_private_url": "לאפשר כתובת URL פרטית", "private_link_label": "קישור פרטי", "private_link_hint": "הקישור הפרטי שלך ייווצר מחדש לאחר כל שימוש", "copy_private_link": "העתקת קישור פרטי", @@ -1214,6 +1223,7 @@ "organizer_name_variable": "שם המארגן/ת", "app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.", "invalid_number": "מספר טלפון לא תקין", + "invalid_url_error_message": "כתובת URL לא חוקית עבור {{label}}. כתובת URL לדוגמה: {{sampleUrl}}", "navigate": "ניווט", "open": "פתח", "close": "סגירה", @@ -1277,6 +1287,7 @@ "personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}", "bio_hint": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.", "user_has_no_bio": "משתמש זה עדיין לא הוסיף ביוגרפיה.", + "bio": "ביוגרפיה", "delete_account_modal_title": "מחיקת החשבון", "confirm_delete_account_modal": "בטוח שברצונך למחוק את חשבון {{appName}} שלך?", "delete_my_account": "מחיקת החשבון שלי", @@ -1287,6 +1298,7 @@ "select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.", "check_for_conflicts": "בדיקת התנגשויות", "view_recordings": "צפייה בהקלטות", + "check_for_recordings": "חיפוש הקלטות", "adding_events_to": "הוספת אירועים ל", "follow_system_preferences": "פעל לפי העדפות המערכת", "custom_brand_colors": "צבעי מותג בהתאמה אישית", @@ -1531,6 +1543,7 @@ "problem_registering_domain": "הייתה בעיה ברישום תת-הדומיין; אפשר לנסות שוב או לפנות למנהל/ת מערכת", "team_publish": "פרסום צוות", "number_text_notifications": "מספר טלפון (להודעות טקסט)", + "number_sms_notifications": "מספר טלפון (להודעות SMS)", "attendee_email_variable": "כתובת הדוא\"ל של המשתתף", "attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה", "kbar_search_placeholder": "הקלד/י פקודה או חפש/י...", @@ -1595,6 +1608,7 @@ "options": "אפשרויות", "enter_option": "הזנת ה-{{index}} של האפשרות", "add_an_option": "הוספת אפשרות", + "location_already_exists": "המיקום הזה כבר קיים, יש לבחור מיקום חדש", "radio": "רדיו", "google_meet_warning": "כדי להשתמש ב-Google Meet, יש להגדיר את Google Calendar כלוח השנה של המארח", "individual": "משתמש בודד", @@ -1614,6 +1628,7 @@ "date_overrides_mark_all_day_unavailable_other": "סימון אי-זמינות בתאריכים מסוימים", "date_overrides_add_btn": "הוספת מעקף", "date_overrides_update_btn": "עדכון מעקף", + "date_successfully_added": "עקיפת תאריך נוספה בהצלחה", "event_type_duplicate_copy_text": "{{slug}}-עותק", "set_as_default": "להגדיר כברירת מחדל", "hide_eventtype_details": "הסתרת פרטי סוג האירוע", @@ -1640,6 +1655,7 @@ "minimum_round_robin_hosts_count": "מספר המארחים שחייבים להשתתף", "hosts": "מארחים", "upgrade_to_enable_feature": "אתה צריך לייצר צוות כדי להפעיל את היכולת. לחץ ליצירת צוות.", + "orgs_upgrade_to_enable_feature": "כדי לאפשר שימוש בתכונה הזו, יש לשדרג לתוכנית שלנו לארגונים.", "new_attendee": "משתתף/ת חדש/ה", "awaiting_approval": "בהמתנה לאישור", "requires_google_calendar": "האפליקציה הזו מחייבת חיבור ל-Google Calendar", @@ -1744,6 +1760,7 @@ "show_on_booking_page": "להציג בדף ההזמנות", "get_started_zapier_templates": "התחל עם תבניות Zapier", "team_is_unpublished": "צוות {{team}} אינו מפורסם", + "org_is_unpublished_description": "הקישור לארגון הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של הארגון או לבקש מהם לפרסם אותו.", "team_is_unpublished_description": "קישור ה-{{entity}} הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של ה-{{entity}} או לבקש מהם לפרסם אותו.", "team_member": "חבר צוות", "a_routing_form": "טופס ניתוב", @@ -1878,6 +1895,7 @@ "edit_invite_link": "עריכת הגדרות הקישור", "invite_link_copied": "קישור ההזמנה הועתק", "invite_link_deleted": "קישור ההזמנה נמחק", + "api_key_deleted": "מפתח API נמחק", "invite_link_updated": "הגדרות קישור ההזמנה נשמרו", "link_expires_after": "הקישורים מוגדרים לפוג לאחר...", "one_day": "יום אחד", @@ -2010,7 +2028,13 @@ "attendee_last_name_variable": "שם המשפחה של המשתתף", "attendee_first_name_info": "השם הפרטי של האדם שביצע את ההזמנה", "attendee_last_name_info": "שם המשפחה של האדם שביצע את ההזמנה", + "your_monthly_digest": "הסיכום החודשי שלך", + "member_name": "שם החבר/ה", + "most_popular_events": "האירועים הפופולריים ביותר", + "summary_of_events_for_your_team_for_the_last_30_days": "הנה הסיכום של האירועים הפופולריים של הצוות שלך, {{teamName}}, ל-30 הימים האחרונים", "me": "אני", + "monthly_digest_email": "אימייל עם סיכום חודשי", + "monthly_digest_email_for_teams": "אימייל עם סיכום חודשי עבור צוותים", "verify_team_tooltip": "אמת/י את הצוות שלך כדי לאפשר שליחת הודעות למשתתפים", "member_removed": "החבר הוסר", "my_availability": "הזמינות שלי", @@ -2040,13 +2064,41 @@ "team_no_event_types": "אין לצוות זה אף סוג של אירוע", "seat_options_doesnt_multiple_durations": "האפשרויות של הושבה במקומות לא תומכות במשכי זמן שונים", "include_calendar_event": "כלילת אירוע מלוח השנה", + "oAuth": "OAuth", "recently_added": "נוספו לאחרונה", "no_members_found": "לא נמצא אף חבר", "event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.", "availability_schedules": "לוחות זמנים לזמינוּת", + "unauthorized": "אין הרשאה", + "access_cal_account": "{{clientName}} רוצה לקבל גישה לחשבון {{appName}} שלך", + "select_account_team": "בחירת חשבון או צוות", + "allow_client_to": "הדבר יאפשר ל-{{clientName}}:", + "associate_with_cal_account": "לשייך בינך לבין הפרטים האישיים שלך מ-{{clientName}}", + "see_personal_info": "לראות את הפרטים האישיים שלך, כולל פרטים אישיים שהגדרת כגלויים לכולם", + "see_primary_email_address": "לראות את כתובת הדוא\"ל הראשית שלך", + "connect_installed_apps": "להתחבר לאפליקציות המותקנות שלך", + "access_event_type": "לקרוא, לערוך ולמחוק את סוגי האירועים שלך", + "access_availability": "לקרוא, לערוך ולמחוק את הזמינות שלך", + "access_bookings": "לקרוא, לערוך ולמחוק את ההזמנות שלך", + "allow_client_to_do": "האם לאפשר ל-{{clientName}} לעשות זאת?", + "oauth_access_information": "לחיצה על 'אפשר' מהווה מתן הרשאה מצידך ליישום זה להשתמש במידע שלך בהתאם לתנאי השירות ולמדיניות הפרטיות שלו. ניתן לשלול את הגישה בחנות האפליקציות של {{appName}}.", + "allow": "אפשר", "view_only_edit_availability_not_onboarded": "משתמש זה לא השלים תהליך הטמעה. לא תהיה לך אפשרות להגדיר את הזמינות שלו עד שהוא יעשה זאת.", "view_only_edit_availability": "את/ה צופה בזמינות של משתמש זה. יש לך אפשרות לערוך רק את פרטי הזמינות שלך.", + "you_can_override_calendar_in_advanced_tab": "ניתן לעקוף זאת על בסיס כל אירוע לגופו בהגדרות המתקדמות בכל סוג אירוע.", "edit_users_availability": "עריכת הזמינות של משתמש: {{username}}", + "resend_invitation": "שליחת ההזמנה מחדש", + "invitation_resent": "ההזמנה נשלחה מחדש.", + "add_client": "הוספת לקוח", + "copy_client_secret_info": "לאחר העתקת הסוד, כבר לא תהיה לך אפשרות לראות אותו", + "add_new_client": "הוספת לקוח חדש", + "this_app_is_not_setup_already": "האפליקציה הזו עדיין לא הוגדרה", + "as_csv": "כ-CSV", + "overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על", + "overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.", + "view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.", + "lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות", + "description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות – שימושי לאירועים אישיים.", "extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From bdd3b132d441a226cb0a0c477cc86fe34e1c3272 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:19:33 +0000 Subject: [PATCH 082/119] feat: troubleshooter with weekly view (V2) (#12280) * Inital UI + layout setup * use booker approach of grid * event-select - sidebar + store work * adds get schedule by event-type-slug * Calendar toggle * Load schedule from event slug * Add busy events to calendar * useschedule * Store more event info than just slug * Add date override to calendar * Changes sizes on smaller screens * add event title as a tooltip * Ensure header navigation works * Stop navigator throwing errors on inital render * Correct br * Event duration fixes * Add getMoreInfo if user is authed with current request.username * Add calendar color map wip * Add WIP comments for coloured outlines * Revert more info changes * Calculate date override correctly * Add description option * Fix inital schedule data not being populated * Nudge overlap over to make it clearer * Fix disabled state * WIP on math logic * Event list overlapping events logic * NIT about width * i18n + manage calendars link * Delete old troubleshooter * Update packages/features/calendars/weeklyview/components/event/EventList.tsx * Remove t-slots * Fix i18n & install calendar action * sm:imrovments * NITS * Fix types * fix: back button * Month prop null as we control from query param * Add head SEO * Fix headseo import * Fix date override tests --- apps/web/pages/availability/troubleshoot.tsx | 141 ++--------------- apps/web/playwright/availability.e2e.ts | 1 + apps/web/public/static/locales/en/common.json | 6 + packages/core/getCalendarsEvents.ts | 6 +- .../weeklyview/components/event/Event.tsx | 58 ++++--- .../weeklyview/components/event/EventList.tsx | 86 +++++++---- .../components/verticalLines/index.tsx | 8 +- .../calendars/weeklyview/state/store.ts | 4 +- .../calendars/weeklyview/types/events.ts | 4 + .../troubleshooter/Troubleshooter.tsx | 70 +++++++++ .../AvailabilitySchedulesContainer.tsx | 38 +++++ .../components/CalendarToggleContainer.tsx | 121 +++++++++++++++ .../components/ConnectedAppsContainer.tsx | 38 +++++ .../components/EventScheduleItem.tsx | 42 ++++++ .../components/EventTypeSelect.tsx | 53 +++++++ .../components/LargeCalendar.tsx | 142 ++++++++++++++++++ .../components/TroubleshooterHeader.tsx | 80 ++++++++++ .../TroubleshooterListItemContainer.tsx | 42 ++++++ .../components/TroubleshooterSidebar.tsx | 39 +++++ packages/features/troubleshooter/layout.tsx | 23 +++ packages/features/troubleshooter/store.ts | 110 ++++++++++++++ packages/features/troubleshooter/types.ts | 13 ++ .../viewer/availability/schedule/_router.tsx | 19 +++ .../getScheduleByEventTypeSlug.handler.ts | 69 +++++++++ .../getScheduleByEventTypeSlug.schema.ts | 7 + .../viewer/availability/user.handler.ts | 7 +- 26 files changed, 1038 insertions(+), 189 deletions(-) create mode 100644 packages/features/troubleshooter/Troubleshooter.tsx create mode 100644 packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx create mode 100644 packages/features/troubleshooter/components/CalendarToggleContainer.tsx create mode 100644 packages/features/troubleshooter/components/ConnectedAppsContainer.tsx create mode 100644 packages/features/troubleshooter/components/EventScheduleItem.tsx create mode 100644 packages/features/troubleshooter/components/EventTypeSelect.tsx create mode 100644 packages/features/troubleshooter/components/LargeCalendar.tsx create mode 100644 packages/features/troubleshooter/components/TroubleshooterHeader.tsx create mode 100644 packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx create mode 100644 packages/features/troubleshooter/components/TroubleshooterSidebar.tsx create mode 100644 packages/features/troubleshooter/layout.tsx create mode 100644 packages/features/troubleshooter/store.ts create mode 100644 packages/features/troubleshooter/types.ts create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts diff --git a/apps/web/pages/availability/troubleshoot.tsx b/apps/web/pages/availability/troubleshoot.tsx index e3bfd24f0e..e37b598d68 100644 --- a/apps/web/pages/availability/troubleshoot.tsx +++ b/apps/web/pages/availability/troubleshoot.tsx @@ -1,139 +1,20 @@ -import dayjs from "@calcom/dayjs"; -import Shell from "@calcom/features/shell/Shell"; +import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter"; +import { getLayout } from "@calcom/features/troubleshooter/layout"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { RouterOutputs } from "@calcom/trpc/react"; -import { trpc } from "@calcom/trpc/react"; -import { SkeletonText } from "@calcom/ui"; - -import useRouterQuery from "@lib/hooks/useRouterQuery"; +import { HeadSeo } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; -type User = RouterOutputs["viewer"]["me"]; - -export interface IBusySlot { - start: string | Date; - end: string | Date; - title?: string; - source?: string | null; -} - -const AvailabilityView = ({ user }: { user: User }) => { - const { t } = useLocale(); - const { date, setQuery: setSelectedDate } = useRouterQuery("date"); - const selectedDate = dayjs(date); - const formattedSelectedDate = selectedDate.format("YYYY-MM-DD"); - - const { data, isLoading } = trpc.viewer.availability.user.useQuery( - { - username: user.username || "", - dateFrom: selectedDate.startOf("day").utc().format(), - dateTo: selectedDate.endOf("day").utc().format(), - withSource: true, - }, - { - enabled: !!user.username, - } - ); - - const overrides = - data?.dateOverrides.reduce((acc, override) => { - if ( - formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") && - formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD") - ) - return acc; - acc.push({ ...override, source: "Date override" }); - return acc; - }, [] as IBusySlot[]) || []; - - return ( -
-
- {t("overview_of_day")}{" "} - { - if (e.target.value) setSelectedDate(e.target.value); - }} - /> - {t("hover_over_bold_times_tip")} -
-
-
- {t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)} -
-
- {(() => { - if (isLoading) - return ( - <> - - - - ); - - if (data && (data.busy.length > 0 || overrides.length > 0)) - return [...data.busy, ...overrides] - .sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1)) - .map((slot: IBusySlot) => ( -
-
- {t("calendar_shows_busy_between")}{" "} - - {dayjs(slot.start).format("HH:mm")} - {" "} - {t("and")}{" "} - - {dayjs(slot.end).format("HH:mm")} - {" "} - {t("on")} {dayjs(slot.start).format("D")}{" "} - {t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")} - {slot.title && ` - (${slot.title})`} - {slot.source && {` - (source: ${slot.source})`}} -
-
- )); - return ( -
-
{t("calendar_no_busy_slots")}
-
- ); - })()} - -
-
- {t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)} -
-
-
-
-
- ); -}; - -export default function Troubleshoot() { - const { data, isLoading } = trpc.viewer.me.useQuery(); +function TroubleshooterPage() { const { t } = useLocale(); return ( -
- - {!isLoading && data && } - -
+ <> + + + ); } -Troubleshoot.PageWrapper = PageWrapper; -function convertMinsToHrsMins(mins: number) { - const h = Math.floor(mins / 60); - const m = mins % 60; - const hs = h < 10 ? `0${h}` : h; - const ms = m < 10 ? `0${m}` : m; - return `${hs}:${ms}`; -} +TroubleshooterPage.getLayout = getLayout; +TroubleshooterPage.PageWrapper = PageWrapper; +export default TroubleshooterPage; diff --git a/apps/web/playwright/availability.e2e.ts b/apps/web/playwright/availability.e2e.ts index d065e3dbb5..bd07557603 100644 --- a/apps/web/playwright/availability.e2e.ts +++ b/apps/web/playwright/availability.e2e.ts @@ -40,6 +40,7 @@ test.describe("Availablity tests", () => { const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date); const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`; await page.goto(troubleshooterURL); + await page.waitForLoadState("networkidle"); await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1); }); }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 69906ee67e..c3a505de55 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2112,8 +2112,14 @@ "overlay_my_calendar":"Overlay my calendar", "overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.", "view_overlay_calendar_events":"View your calendar events to prevent clashed booking.", + "troubleshooting":"Troubleshooting", + "calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts", + "availabilty_schedules":"Availability schedules", + "manage_calendars":"Manage calendars", + "manage_availability_schedules":"Manage availability schedules", "lock_timezone_toggle_on_booking_page": "Lock timezone on booking page", "description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.", + "install_calendar":"Install Calendar", "branded_subdomain": "Branded Subdomain", "branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com", "org_insights": "Organization-wide Insights", diff --git a/packages/core/getCalendarsEvents.ts b/packages/core/getCalendarsEvents.ts index 8ee9d44fb0..f8700ddcf9 100644 --- a/packages/core/getCalendarsEvents.ts +++ b/packages/core/getCalendarsEvents.ts @@ -33,6 +33,7 @@ const getCalendarsEvents = async ( const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type); if (!passedSelectedCalendars.length) return []; /** We extract external Ids so we don't cache too much */ + const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId); /** If we don't then we actually fetch external calendars (which can be very slow) */ performance.mark("eventBusyDatesStart"); @@ -51,7 +52,10 @@ const getCalendarsEvents = async ( "eventBusyDatesEnd" ); - return eventBusyDates.map((a) => ({ ...a, source: `${appId}` })); + return eventBusyDates.map((a) => ({ + ...a, + source: `${appId}`, + })); }); const awaitedResults = await Promise.all(results); performance.mark("getBusyCalendarTimesEnd"); diff --git a/packages/features/calendars/weeklyview/components/event/Event.tsx b/packages/features/calendars/weeklyview/components/event/Event.tsx index c997e9a16d..8cb854ae09 100644 --- a/packages/features/calendars/weeklyview/components/event/Event.tsx +++ b/packages/features/calendars/weeklyview/components/event/Event.tsx @@ -1,6 +1,8 @@ import { cva } from "class-variance-authority"; import dayjs from "@calcom/dayjs"; +import classNames from "@calcom/lib/classNames"; +import { Tooltip } from "@calcom/ui"; import type { CalendarEvent } from "../../types/events"; @@ -13,7 +15,7 @@ type EventProps = { }; const eventClasses = cva( - "group flex h-full w-full flex-col overflow-y-auto rounded-[4px] px-[6px] py-1 text-xs font-semibold leading-5 ", + "group flex h-full w-full overflow-y-auto rounded-[6px] px-[6px] text-xs font-semibold leading-5 opacity-80", { variants: { status: { @@ -62,23 +64,41 @@ export function Event({ const Component = onEventClick ? "button" : "div"; return ( - onEventClick?.(event)} // Note this is not the button event. It is the calendar event. - className={eventClasses({ - status: options?.status, - disabled, - selected, - borderColor, - })} - style={styles}> -
- {event.title} -
- {eventDuration > 30 && ( -

- {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")} -

- )} -
+ + onEventClick?.(event)} // Note this is not the button event. It is the calendar event. + className={classNames( + eventClasses({ + status: options?.status, + disabled, + selected, + borderColor, + }), + eventDuration > 30 && "flex-col py-1", + options?.className + )} + style={styles}> +
+ {event.title} + {eventDuration <= 30 && !event.options?.hideTime && ( +

+ {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")} +

+ )} +
+ {eventDuration > 30 && !event.options?.hideTime && ( +

+ {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")} +

+ )} + {eventDuration > 45 && event.description && ( +

{event.description}

+ )} +
+
); } diff --git a/packages/features/calendars/weeklyview/components/event/EventList.tsx b/packages/features/calendars/weeklyview/components/event/EventList.tsx index 57cbb47e8c..ed6af0e763 100644 --- a/packages/features/calendars/weeklyview/components/event/EventList.tsx +++ b/packages/features/calendars/weeklyview/components/event/EventList.tsx @@ -1,3 +1,4 @@ +import { useRef } from "react"; import { shallow } from "zustand/shallow"; import dayjs from "@calcom/dayjs"; @@ -19,6 +20,14 @@ export function EventList({ day }: Props) { shallow ); + // Use a ref so we dont trigger a re-render + const longestRef = useRef<{ + start: Date; + end: Date; + duration: number; + idx: number; + } | null>(null); + return ( <> {events @@ -41,47 +50,59 @@ export function EventList({ day }: Props) { const nextEvent = eventsArray[idx + 1]; const prevEvent = eventsArray[idx - 1]; - // Check for overlapping events since this is sorted it should just work. - if (nextEvent) { - const nextEventStart = dayjs(nextEvent.start); - const nextEventEnd = dayjs(nextEvent.end); - // check if next event starts before this event ends - if (nextEventStart.isBefore(eventEnd)) { - // figure out which event has the longest duration - const nextEventDuration = nextEventEnd.diff(nextEventStart, "minutes"); - if (nextEventDuration > eventDuration) { + if (!longestRef.current) { + longestRef.current = { + idx, + start: eventStart.toDate(), + end: eventEnd.toDate(), + duration: eventDuration, + }; + } else if ( + eventDuration > longestRef.current.duration && + eventStart.isBetween(longestRef.current.start, longestRef.current.end) + ) { + longestRef.current = { + idx, + start: eventStart.toDate(), + end: eventEnd.toDate(), + duration: eventDuration, + }; + } + // By default longest event doesnt have any styles applied + if (longestRef.current.idx !== idx) { + if (nextEvent) { + // If we have a next event + const nextStart = dayjs(nextEvent.start); + // If the next event is inbetween the longest start and end make 65% width + if (nextStart.isBetween(longestRef.current.start, longestRef.current.end)) { zIndex = 65; - marginLeft = "auto"; - // 8 looks like a really random number but we need to take into account the bordersize on the event. - // Logically it should be 5% but this causes a bit of a overhang which we don't want. - right = 8; + right = 4; + width = width / 2; + + // If not - we check to see if the next starts within 5 mins of this event - allowing us to do side by side events whenwe have + // close start times + } else if (nextStart.isBetween(eventStart.add(-5, "minutes"), eventStart.add(5, "minutes"))) { + zIndex = 65; + marginLeft = "auto"; + right = 4; width = width / 2; } - } + } else if (prevEvent) { + const prevStart = dayjs(prevEvent.start); - if (nextEventStart.isSame(eventStart)) { - zIndex = 66; + // If the next event is inbetween the longest start and end make 65% width - marginLeft = "auto"; - right = 8; - width = width / 2; - } - } else if (prevEvent) { - const prevEventStart = dayjs(prevEvent.start); - const prevEventEnd = dayjs(prevEvent.end); - // check if next event starts before this event ends - if (prevEventEnd.isAfter(eventStart)) { - // figure out which event has the longest duration - const prevEventDuration = prevEventEnd.diff(prevEventStart, "minutes"); - if (prevEventDuration > eventDuration) { + if (prevStart.isBetween(longestRef.current.start, longestRef.current.end)) { zIndex = 65; marginLeft = "auto"; - right = 8; + right = 4; + // If not - we check to see if the next starts within 5 mins of this event - allowing us to do side by side events whenwe have + // close start times (Inverse of above ) + } else if (eventStart.isBetween(prevStart.add(5, "minutes"), prevStart.add(-5, "minutes"))) { + zIndex = 65; + right = 4; width = width / 2; - if (eventDuration >= 30) { - width = 80; - } } } } @@ -90,6 +111,7 @@ export function EventList({ day }: Props) {
{ const isRTL = () => { - const userLocale = navigator.language; - const userLanguage = new Intl.Locale(userLocale).language; + let userLanguage = "en"; // Default to 'en' if navigator is not defined + + if (typeof window !== "undefined" && typeof navigator !== "undefined") { + const userLocale = navigator.language; + userLanguage = new Intl.Locale(userLocale).language; + } return ["ar", "he", "fa", "ur"].includes(userLanguage); }; diff --git a/packages/features/calendars/weeklyview/state/store.ts b/packages/features/calendars/weeklyview/state/store.ts index 9424eec9b8..af1b52358d 100644 --- a/packages/features/calendars/weeklyview/state/store.ts +++ b/packages/features/calendars/weeklyview/state/store.ts @@ -32,9 +32,7 @@ export const useCalendarStore = create((set) => ({ let events = state.events; if (state.sortEvents) { - events = state.events.sort( - (a, b) => dayjs(a.start).get("milliseconds") - dayjs(b.start).get("milliseconds") - ); + events = state.events.sort((a, b) => dayjs(a.start).valueOf() - dayjs(b.start).valueOf()); } const blockingDates = mergeOverlappingDateRanges(state.blockingDates || []); // We merge overlapping dates so we don't get duplicate blocking "Cells" in the UI diff --git a/packages/features/calendars/weeklyview/types/events.ts b/packages/features/calendars/weeklyview/types/events.ts index bade4cf0c4..493750ea8f 100644 --- a/packages/features/calendars/weeklyview/types/events.ts +++ b/packages/features/calendars/weeklyview/types/events.ts @@ -3,12 +3,16 @@ import type { BookingStatus } from "@calcom/prisma/enums"; export interface CalendarEvent { id: number; title: string; + description?: string; start: Date | string; // You can pass in a string from DB since we use dayjs for the dates. end: Date; source?: string; options?: { status?: BookingStatus; + hideTime?: boolean; allDay?: boolean; borderColor?: string; + className?: string; + "data-test-id"?: string; }; } diff --git a/packages/features/troubleshooter/Troubleshooter.tsx b/packages/features/troubleshooter/Troubleshooter.tsx new file mode 100644 index 0000000000..ad9457fdb1 --- /dev/null +++ b/packages/features/troubleshooter/Troubleshooter.tsx @@ -0,0 +1,70 @@ +import StickyBox from "react-sticky-box"; + +import classNames from "@calcom/lib/classNames"; +import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; + +import { LargeCalendar } from "./components/LargeCalendar"; +import { TroubleshooterHeader } from "./components/TroubleshooterHeader"; +import { TroubleshooterSidebar } from "./components/TroubleshooterSidebar"; +import { useInitalizeTroubleshooterStore } from "./store"; +import type { TroubleshooterProps } from "./types"; + +const extraDaysConfig = { + desktop: 7, + tablet: 4, +}; + +const TroubleshooterComponent = ({ month }: TroubleshooterProps) => { + const isMobile = useMediaQuery("(max-width: 768px)"); + const isTablet = useMediaQuery("(max-width: 1024px)"); + const extraDays = isTablet ? extraDaysConfig.tablet : extraDaysConfig.desktop; + + useInitalizeTroubleshooterStore({ + month: month, + }); + + return ( + <> +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + ); +}; + +export const Troubleshooter = ({ month }: TroubleshooterProps) => { + return ; +}; diff --git a/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx b/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx new file mode 100644 index 0000000000..ee3196b731 --- /dev/null +++ b/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx @@ -0,0 +1,38 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Badge, Button, Switch } from "@calcom/ui"; + +import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer"; + +function AvailabiltyItem() { + const { t } = useLocale(); + return ( + + + Connected + +
+ }> +
+

{t("date_overrides")}

+ +
+ + ); +} + +export function AvailabiltySchedulesContainer() { + const { t } = useLocale(); + return ( +
+

{t("availabilty_schedules")}

+ + +
+ ); +} diff --git a/packages/features/troubleshooter/components/CalendarToggleContainer.tsx b/packages/features/troubleshooter/components/CalendarToggleContainer.tsx new file mode 100644 index 0000000000..4fceb7bf81 --- /dev/null +++ b/packages/features/troubleshooter/components/CalendarToggleContainer.tsx @@ -0,0 +1,121 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Badge, Button, Switch } from "@calcom/ui"; + +import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer"; + +const SELECTION_COLORS = ["#f97316", "#84cc16", "#06b6d4", "#8b5cf6", "#ec4899", "#f43f5e"]; + +interface CalendarToggleItemProps { + title: string; + subtitle: string; + colorDot?: string; + status: "connected" | "not_found"; + calendars?: { + active?: boolean; + name?: string; + }[]; +} +function CalendarToggleItem(props: CalendarToggleItemProps) { + const badgeStatus = props.status === "connected" ? "green" : "orange"; + const badgeText = props.status === "connected" ? "Connected" : "Not found"; + return ( + +
+ + } + suffixSlot={ +
+ + {badgeText} + +
+ }> +
+ {props.calendars?.map((calendar) => { + return ; + })} +
+ + ); +} + +function EmptyCalendarToggleItem() { + const { t } = useLocale(); + + return ( + +
+ + } + suffixSlot={ +
+ + Not found + +
+ }> +
+ +
+ + ); +} + +export function CalendarToggleContainer() { + const { t } = useLocale(); + const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery(); + + const hasConnectedCalendars = data && data?.connectedCalendars.length > 0; + + return ( +
+

{t("calendars_were_checking_for_conflicts")}

+ {hasConnectedCalendars && !isLoading ? ( + <> + {data.connectedCalendars.map((calendar) => { + const foundPrimary = calendar.calendars?.find((item) => item.primary); + // Will be used when getAvailbility is modified to use externalId instead of appId for source. + // const color = SELECTION_COLORS[idx] || "#000000"; + // // Add calendar to color map using externalId (what we use on the backend to determine source) + // addToColorMap(foundPrimary?.externalId, color); + return ( + { + return { + active: item.isSelected, + name: item.name, + }; + })} + /> + ); + })} + + + ) : ( + + )} +
+ ); +} diff --git a/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx b/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx new file mode 100644 index 0000000000..d25c70ada1 --- /dev/null +++ b/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx @@ -0,0 +1,38 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Badge } from "@calcom/ui"; + +import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer"; + +function ConnectedAppsItem() { + return ( + +
+ + } + suffixSlot={ +
+ + Connected + +
+ } + /> + ); +} + +export function ConnectedAppsContainer() { + const { t } = useLocale(); + return ( +
+

{t("other_apps")}

+
+ + +
+
+ ); +} diff --git a/packages/features/troubleshooter/components/EventScheduleItem.tsx b/packages/features/troubleshooter/components/EventScheduleItem.tsx new file mode 100644 index 0000000000..5cd47b2ccb --- /dev/null +++ b/packages/features/troubleshooter/components/EventScheduleItem.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Badge, Label } from "@calcom/ui"; + +import { useTroubleshooterStore } from "../store"; +import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer"; + +export function EventScheduleItem() { + const { t } = useLocale(); + const selectedEventType = useTroubleshooterStore((state) => state.event); + + const { data: schedule } = trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery( + { + eventSlug: selectedEventType?.slug as string, + }, + { + enabled: !!selectedEventType?.slug, + } + ); + + return ( +
+ + } + title={schedule?.name ?? "Loading"} + suffixSlot={ + schedule && ( + + + {t("edit")} + + + ) + } + /> +
+ ); +} diff --git a/packages/features/troubleshooter/components/EventTypeSelect.tsx b/packages/features/troubleshooter/components/EventTypeSelect.tsx new file mode 100644 index 0000000000..dac68067b7 --- /dev/null +++ b/packages/features/troubleshooter/components/EventTypeSelect.tsx @@ -0,0 +1,53 @@ +import { useMemo, useEffect } from "react"; + +import { trpc } from "@calcom/trpc"; +import { SelectField } from "@calcom/ui"; + +import { useTroubleshooterStore } from "../store"; + +export function EventTypeSelect() { + const { data: eventTypes, isLoading } = trpc.viewer.eventTypes.list.useQuery(); + const selectedEventType = useTroubleshooterStore((state) => state.event); + const setSelectedEventType = useTroubleshooterStore((state) => state.setEvent); + + // const selectedEventQueryParam = getQueryParam("eventType"); + + const options = useMemo(() => { + if (!eventTypes) return []; + return eventTypes.map((e) => ({ + label: e.title, + value: e.slug, + id: e.id, + duration: e.length, + })); + }, [eventTypes]); + + useEffect(() => { + if (!selectedEventType && eventTypes && eventTypes[0]) { + const { id, slug, length } = eventTypes[0]; + setSelectedEventType({ + id, + slug, + duration: length, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [eventTypes]); + + return ( + option.value === selectedEventType?.slug) || options[0]} + onChange={(option) => { + if (!option) return; + setSelectedEventType({ + slug: option.value, + id: option.id, + duration: option.duration, + }); + }} + /> + ); +} diff --git a/packages/features/troubleshooter/components/LargeCalendar.tsx b/packages/features/troubleshooter/components/LargeCalendar.tsx new file mode 100644 index 0000000000..7884e38e18 --- /dev/null +++ b/packages/features/troubleshooter/components/LargeCalendar.tsx @@ -0,0 +1,142 @@ +import { useSession } from "next-auth/react"; +import { useMemo } from "react"; + +import dayjs from "@calcom/dayjs"; +import { Calendar } from "@calcom/features/calendars/weeklyview"; +import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; +import { BookingStatus } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc"; + +import { useTimePreferences } from "../../bookings/lib/timePreferences"; +import { useSchedule } from "../../schedules/lib/use-schedule"; +import { useTroubleshooterStore } from "../store"; + +export const LargeCalendar = ({ extraDays }: { extraDays: number }) => { + const { timezone } = useTimePreferences(); + const selectedDate = useTroubleshooterStore((state) => state.selectedDate); + const event = useTroubleshooterStore((state) => state.event); + const calendarToColorMap = useTroubleshooterStore((state) => state.calendarToColorMap); + const { data: session } = useSession(); + const startDate = selectedDate ? dayjs(selectedDate) : dayjs(); + + const { data: busyEvents } = trpc.viewer.availability.user.useQuery( + { + username: session?.user?.username || "", + dateFrom: startDate.startOf("day").utc().format(), + dateTo: startDate + .endOf("day") + .add(extraDays - 1, "day") + .utc() + .format(), + withSource: true, + }, + { + enabled: !!session?.user?.username, + } + ); + + const { data: schedule } = useSchedule({ + username: session?.user.username || "", + eventSlug: event?.slug, + eventId: event?.id, + timezone, + month: startDate.format("YYYY-MM"), + }); + + const endDate = dayjs(startDate) + .add(extraDays - 1, "day") + .toDate(); + + const availableSlots = useMemo(() => { + const availableTimeslots: CalendarAvailableTimeslots = {}; + if (!schedule) return availableTimeslots; + if (!schedule?.slots) return availableTimeslots; + + for (const day in schedule.slots) { + availableTimeslots[day] = schedule.slots[day].map((slot) => ({ + start: dayjs(slot.time).toDate(), + end: dayjs(slot.time) + .add(event?.duration ?? 30, "minutes") + .toDate(), + })); + } + + return availableTimeslots; + }, [schedule, event]); + + const events = useMemo(() => { + if (!busyEvents?.busy) return []; + + // TODO: Add buffer times in here as well just requires a bit of logic for fetching event type and then adding buffer time + // start: dayjs(startTime) + // .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute") + // .toDate(), + // end: dayjs(endTime) + // .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute") + // .toDate(), + + const calendarEvents = busyEvents?.busy.map((event, idx) => { + return { + id: idx, + title: event.title ?? `Busy`, + start: new Date(event.start), + end: new Date(event.end), + options: { + borderColor: + event.source && calendarToColorMap[event.source] ? calendarToColorMap[event.source] : "black", + status: BookingStatus.ACCEPTED, + "data-test-id": "troubleshooter-busy-event", + }, + }; + }); + + if (busyEvents.dateOverrides) { + busyEvents.dateOverrides.forEach((dateOverride) => { + const dateOverrideStart = dayjs(dateOverride.start); + const dateOverrideEnd = dayjs(dateOverride.end); + + if (!dateOverrideStart.isSame(dateOverrideEnd)) { + return; + } + + const dayOfWeekNum = dateOverrideStart.day(); + + const workingHoursForDay = busyEvents.workingHours.find((workingHours) => + workingHours.days.includes(dayOfWeekNum) + ); + + if (!workingHoursForDay) return; + + calendarEvents.push({ + id: calendarEvents.length, + title: "Date Override", + start: dateOverrideStart.add(workingHoursForDay.startTime, "minutes").toDate(), + end: dateOverrideEnd.add(workingHoursForDay.endTime, "minutes").toDate(), + options: { + borderColor: "black", + status: BookingStatus.ACCEPTED, + "data-test-id": "troubleshooter-busy-time", + }, + }); + }); + } + return calendarEvents; + }, [busyEvents, calendarToColorMap]); + + return ( +
+ +
+ ); +}; diff --git a/packages/features/troubleshooter/components/TroubleshooterHeader.tsx b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx new file mode 100644 index 0000000000..90e6318184 --- /dev/null +++ b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx @@ -0,0 +1,80 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useMemo } from "react"; + +import dayjs from "@calcom/dayjs"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, ButtonGroup } from "@calcom/ui"; + +import { useTroubleshooterStore } from "../store"; + +export function TroubleshooterHeader({ extraDays, isMobile }: { extraDays: number; isMobile: boolean }) { + const { t, i18n } = useLocale(); + const selectedDateString = useTroubleshooterStore((state) => state.selectedDate); + const setSelectedDate = useTroubleshooterStore((state) => state.setSelectedDate); + const addToSelectedDate = useTroubleshooterStore((state) => state.addToSelectedDate); + const selectedDate = selectedDateString ? dayjs(selectedDateString) : dayjs(); + const today = dayjs(); + const selectedDateMin3DaysDifference = useMemo(() => { + const diff = today.diff(selectedDate, "days"); + return diff > 3 || diff < -3; + }, [today, selectedDate]); + + if (isMobile) return null; + + const endDate = selectedDate.add(extraDays - 1, "days"); + + const isSameMonth = () => { + return selectedDate.format("MMM") === endDate.format("MMM"); + }; + + const isSameYear = () => { + return selectedDate.format("YYYY") === endDate.format("YYYY"); + }; + const formattedMonth = new Intl.DateTimeFormat(i18n.language, { month: "short" }); + const FormattedSelectedDateRange = () => { + return ( +

+ {formattedMonth.format(selectedDate.toDate())} {selectedDate.format("D")} + {!isSameYear() && , {selectedDate.format("YYYY")} }-{" "} + {!isSameMonth() && formattedMonth.format(endDate.toDate())} {endDate.format("D")},{" "} + + {isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")} + +

+ ); + }; + + return ( +
+
+ + + + )} + +
+
+ ); +} diff --git a/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx b/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx new file mode 100644 index 0000000000..0d5bb956a3 --- /dev/null +++ b/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx @@ -0,0 +1,42 @@ +import type { PropsWithChildren } from "react"; + +import classNames from "@calcom/lib/classNames"; + +interface TroubleshooterListItemContainerProps { + title: string; + subtitle?: string; + suffixSlot?: React.ReactNode; + prefixSlot?: React.ReactNode; + className?: string; +} + +export function TroubleshooterListItemHeader({ + prefixSlot, + title, + subtitle, + suffixSlot, + className, +}: TroubleshooterListItemContainerProps) { + return ( +
+ {prefixSlot} +
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {suffixSlot} +
+ ); +} + +export function TroubleshooterListItemContainer({ + children, + ...rest +}: PropsWithChildren) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx b/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx new file mode 100644 index 0000000000..101656db27 --- /dev/null +++ b/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx @@ -0,0 +1,39 @@ +import { ArrowLeft } from "lucide-react"; +import Link from "next/link"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Skeleton } from "@calcom/ui"; + +import { CalendarToggleContainer } from "./CalendarToggleContainer"; +import { EventScheduleItem } from "./EventScheduleItem"; +import { EventTypeSelect } from "./EventTypeSelect"; + +const BackButtonInSidebar = ({ name }: { name: string }) => { + return ( + + + + {name} + + + ); +}; + +export const TroubleshooterSidebar = () => { + const { t } = useLocale(); + + return ( +
+ + + + +
+ ); +}; diff --git a/packages/features/troubleshooter/layout.tsx b/packages/features/troubleshooter/layout.tsx new file mode 100644 index 0000000000..fdcc968cd4 --- /dev/null +++ b/packages/features/troubleshooter/layout.tsx @@ -0,0 +1,23 @@ +import type { ComponentProps } from "react"; +import React, { Suspense } from "react"; + +import Shell from "@calcom/features/shell/Shell"; +import { ErrorBoundary } from "@calcom/ui"; +import { Loader } from "@calcom/ui/components/icon"; + +export default function TroubleshooterLayout({ + children, + ...rest +}: { children: React.ReactNode } & ComponentProps) { + return ( + }> +
+ + }>{children} + +
+
+ ); +} + +export const getLayout = (page: React.ReactElement) => {page}; diff --git a/packages/features/troubleshooter/store.ts b/packages/features/troubleshooter/store.ts new file mode 100644 index 0000000000..4d5daa2601 --- /dev/null +++ b/packages/features/troubleshooter/store.ts @@ -0,0 +1,110 @@ +import { useEffect } from "react"; +import { create } from "zustand"; + +import dayjs from "@calcom/dayjs"; + +import { updateQueryParam, getQueryParam, removeQueryParam } from "../bookings/Booker/utils/query-param"; + +/** + * Arguments passed into store initializer, containing + * the event data. + */ +type StoreInitializeType = { + month: string | null; +}; + +type EventType = { + id: number; + slug: string; + duration: number; +}; + +export type TroubleshooterStore = { + event: EventType | null; + setEvent: (eventSlug: EventType) => void; + month: string | null; + setMonth: (month: string | null) => void; + selectedDate: string | null; + setSelectedDate: (date: string | null) => void; + addToSelectedDate: (days: number) => void; + initialize: (data: StoreInitializeType) => void; + calendarToColorMap: Record; + addToCalendarToColorMap: (calendarId: string | undefined, color: string) => void; +}; + +/** + * The booker store contains the data of the component's + * current state. This data can be reused within child components + * by importing this hook. + * + * See comments in interface above for more information on it's specific values. + */ +export const useTroubleshooterStore = create((set, get) => ({ + selectedDate: getQueryParam("date") || null, + setSelectedDate: (selectedDate: string | null) => { + // unset selected date + if (!selectedDate) { + removeQueryParam("date"); + return; + } + + const currentSelection = dayjs(get().selectedDate); + const newSelection = dayjs(selectedDate); + set({ selectedDate }); + updateQueryParam("date", selectedDate ?? ""); + + // Setting month make sure small calendar in fullscreen layouts also updates. + if (newSelection.month() !== currentSelection.month()) { + set({ month: newSelection.format("YYYY-MM") }); + updateQueryParam("month", newSelection.format("YYYY-MM")); + } + }, + addToSelectedDate: (days: number) => { + const selectedDate = get().selectedDate; + const currentSelection = selectedDate ? dayjs(get().selectedDate) : dayjs(); + const newSelection = currentSelection.add(days, "day"); + const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); + + if (newSelection.month() !== currentSelection.month()) { + set({ month: newSelection.format("YYYY-MM") }); + updateQueryParam("month", newSelection.format("YYYY-MM")); + } + + set({ selectedDate: newSelectionFormatted }); + updateQueryParam("date", newSelectionFormatted); + }, + event: null, + setEvent: (event: EventType) => { + set({ event }); + updateQueryParam("eventType", event.slug ?? ""); + }, + month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"), + setMonth: (month: string | null) => { + set({ month }); + updateQueryParam("month", month ?? ""); + get().setSelectedDate(null); + }, + initialize: ({ month }: StoreInitializeType) => { + if (month) { + set({ month }); + updateQueryParam("month", month); + } + //removeQueryParam("layout"); + }, + calendarToColorMap: {}, + addToCalendarToColorMap: (calendarId: string | undefined, color: string) => { + if (!calendarId) return; + const calendarToColorMap = get().calendarToColorMap; + calendarToColorMap[calendarId] = color; + set({ calendarToColorMap }); + }, +})); + +export const useInitalizeTroubleshooterStore = ({ month }: StoreInitializeType) => { + const initializeStore = useTroubleshooterStore((state) => state.initialize); + useEffect(() => { + initializeStore({ + month, + }); + }, [initializeStore, month]); +}; diff --git a/packages/features/troubleshooter/types.ts b/packages/features/troubleshooter/types.ts new file mode 100644 index 0000000000..a1029e077b --- /dev/null +++ b/packages/features/troubleshooter/types.ts @@ -0,0 +1,13 @@ +export interface TroubleshooterProps { + /** + * If month is NOT set as a prop on the component, we expect a query parameter + * called `month` to be present on the url. If that is missing, the component will + * default to the current month. + * @note In case you're using a client side router, please pass the value in as a prop, + * since the component will leverage window.location, which might not have the query param yet. + * @format YYYY-MM. + * @optional + */ + month: string | null; + selectedDate?: Date; +} diff --git a/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx b/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx index b886f81a64..f6e6fde73b 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx +++ b/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx @@ -4,6 +4,7 @@ import { ZCreateInputSchema } from "./create.schema"; import { ZDeleteInputSchema } from "./delete.schema"; import { ZScheduleDuplicateSchema } from "./duplicate.schema"; import { ZGetInputSchema } from "./get.schema"; +import { ZGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema"; import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema"; import { ZUpdateInputSchema } from "./update.schema"; @@ -14,6 +15,7 @@ type ScheduleRouterHandlerCache = { update?: typeof import("./update.handler").updateHandler; duplicate?: typeof import("./duplicate.handler").duplicateHandler; getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler; + getScheduleByEventSlug?: typeof import("./getScheduleByEventTypeSlug.handler").getScheduleByEventSlugHandler; }; const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {}; @@ -118,4 +120,21 @@ export const scheduleRouter = router({ input, }); }), + getScheduleByEventSlug: authedProcedure.input(ZGetByEventSlugInputSchema).query(async ({ input, ctx }) => { + if (!UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug) { + UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug = await import( + "./getScheduleByEventTypeSlug.handler" + ).then((mod) => mod.getScheduleByEventSlugHandler); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug({ + ctx, + input, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts new file mode 100644 index 0000000000..50457eff3a --- /dev/null +++ b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts @@ -0,0 +1,69 @@ +import type { PrismaClient } from "@calcom/prisma"; + +import type { TrpcSessionUser } from "../../../../trpc"; +import { getHandler } from "./get.handler"; +import type { TGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema"; + +type GetOptions = { + ctx: { + user: NonNullable; + prisma: PrismaClient; + }; + input: TGetByEventSlugInputSchema; +}; + +const EMPTY_SCHEDULE = [[], [], [], [], [], [], []]; + +export const getScheduleByEventSlugHandler = async ({ ctx, input }: GetOptions) => { + const foundScheduleForSlug = await ctx.prisma.eventType.findFirst({ + where: { + slug: input.eventSlug, + userId: ctx.user.id, + }, + select: { + scheduleId: true, + }, + }); + + try { + // This looks kinda weird that we throw straight in the catch - its so that we can return a default schedule if the user has not completed onboarding @shiraz will loveme for this + if (!foundScheduleForSlug?.scheduleId) { + const foundUserDefaultId = await ctx.prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + select: { + defaultScheduleId: true, + }, + }); + + if (foundUserDefaultId?.defaultScheduleId) { + return await getHandler({ + ctx, + input: { + scheduleId: foundUserDefaultId?.defaultScheduleId, + }, + }); + } + + throw new Error("NOT_FOUND"); + } + return await getHandler({ + ctx, + input: { + scheduleId: foundScheduleForSlug?.scheduleId, + }, + }); + } catch (e) { + console.log(e); + return { + id: -1, + name: "No schedules found", + availability: EMPTY_SCHEDULE, + dateOverrides: [], + timeZone: ctx.user.timeZone || "Europe/London", + workingHours: [], + isDefault: true, + }; + } +}; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts new file mode 100644 index 0000000000..b65f1b5e37 --- /dev/null +++ b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZGetByEventSlugInputSchema = z.object({ + eventSlug: z.string(), +}); + +export type TGetByEventSlugInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/availability/user.handler.ts b/packages/trpc/server/routers/viewer/availability/user.handler.ts index d45c2d85c0..decfe5ca66 100644 --- a/packages/trpc/server/routers/viewer/availability/user.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/user.handler.ts @@ -1,12 +1,15 @@ import { getUserAvailability } from "@calcom/core/getUserAvailability"; +import type { TrpcSessionUser } from "../../../trpc"; import type { TUserInputSchema } from "./user.schema"; type UserOptions = { - ctx: Record; + ctx: { + user: NonNullable; + }; input: TUserInputSchema; }; export const userHandler = async ({ input }: UserOptions) => { - return getUserAvailability(input); + return getUserAvailability(input, undefined); }; From c55b36f2359fa2e4d4e8406eef607812aff48b7c Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 20 Nov 2023 18:01:50 +0530 Subject: [PATCH 083/119] fix: Members count when `team` slug is same as `org` slug (#12124) --- packages/lib/server/queries/teams/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts index 8f39aa2c8c..7ef45e69c2 100644 --- a/packages/lib/server/queries/teams/index.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -44,6 +44,7 @@ export async function getTeamWithMembers(args: { team: { select: { slug: true, + id: true, }, }, }, @@ -153,7 +154,7 @@ export async function getTeamWithMembers(args: { disableImpersonation: m.disableImpersonation, subteams: orgSlug ? m.user.teams - .filter((membership) => membership.team.slug !== orgSlug) + .filter((membership) => membership.team.id !== teamOrOrg.id) .map((membership) => membership.team.slug) : null, avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`, From 4f26ca1a7b0e4fe7eceeaab503b223d73871c95e Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 20 Nov 2023 12:49:38 +0000 Subject: [PATCH 084/119] feat: Base implementation of v2 of avatars (#12369) * feat: Base implementation of v2 of avatars * Make avatarUrl and logoUrl entirely optional * Made necessary backwards compat changes * fix: type errors * Fix: OG image * fix types * Consistency with other behaviour, ux tweak --------- Co-authored-by: Peer Richelsen --- apps/web/components/ui/avatar/UserAvatar.tsx | 4 +- apps/web/pages/[user].tsx | 7 +- apps/web/pages/api/avatar/[uuid].ts | 61 ++++++++++++++ .../web/pages/settings/my-account/profile.tsx | 24 +++--- apps/web/playwright/fixtures/cal.png | Bin 0 -> 43681 bytes .../playwright/settings/upload-avatar.e2e.ts | 56 +++++++++++++ .../test/handlers/requestReschedule.test.ts | 1 + .../settings/layouts/SettingsLayout.tsx | 3 +- packages/lib/getAvatarUrl.ts | 23 ++++-- packages/lib/test/builder.ts | 1 + .../migration.sql | 19 +++++ packages/prisma/schema.prisma | 16 ++++ .../server/middlewares/sessionMiddleware.ts | 1 + .../routers/loggedInViewer/avatar.handler.ts | 11 ++- .../routers/loggedInViewer/me.handler.ts | 1 + .../loggedInViewer/updateProfile.handler.ts | 77 +++++++++--------- packages/ui/components/dialog/Dialog.tsx | 6 +- .../image-uploader/ImageUploader.tsx | 16 +++- 18 files changed, 258 insertions(+), 69 deletions(-) create mode 100644 apps/web/pages/api/avatar/[uuid].ts create mode 100644 apps/web/playwright/fixtures/cal.png create mode 100644 apps/web/playwright/settings/upload-avatar.e2e.ts create mode 100644 packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql diff --git a/apps/web/components/ui/avatar/UserAvatar.tsx b/apps/web/components/ui/avatar/UserAvatar.tsx index 63fa676676..a542fc3d9f 100644 --- a/apps/web/components/ui/avatar/UserAvatar.tsx +++ b/apps/web/components/ui/avatar/UserAvatar.tsx @@ -14,6 +14,6 @@ type UserAvatarProps = Omit, "alt" | "imageS * It is aware of the user's organization to correctly show the avatar from the correct URL */ export function UserAvatar(props: UserAvatarProps) { - const { user, previewSrc, ...rest } = props; - return ; + const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props; + return ; } diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 5a4f46eed0..46a0e5ffa5 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -82,7 +82,7 @@ export function UserPage(props: InferGetServerSidePropsType[]; + users: Pick[]; themeBasis: string | null; markdownStrippedBio: string; safeBio: string; @@ -295,6 +295,7 @@ export const getServerSideProps: GetServerSideProps = async (cont metadata: true, brandColor: true, darkBrandColor: true, + avatarUrl: true, organizationId: true, organization: { select: { @@ -363,6 +364,7 @@ export const getServerSideProps: GetServerSideProps = async (cont image: user.avatar, theme: user.theme, brandColor: user.brandColor, + avatarUrl: user.avatarUrl, darkBrandColor: user.darkBrandColor, allowSEOIndexing: user.allowSEOIndexing ?? true, username: user.username, @@ -397,6 +399,7 @@ export const getServerSideProps: GetServerSideProps = async (cont name: user.name, username: user.username, bio: user.bio, + avatarUrl: user.avatarUrl, away: user.away, verified: user.verified, })), diff --git a/apps/web/pages/api/avatar/[uuid].ts b/apps/web/pages/api/avatar/[uuid].ts new file mode 100644 index 0000000000..2e8a1dfda3 --- /dev/null +++ b/apps/web/pages/api/avatar/[uuid].ts @@ -0,0 +1,61 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { AVATAR_FALLBACK } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; + +const querySchema = z.object({ + uuid: z.string().transform((objectKey) => objectKey.split(".")[0]), +}); + +const handleValidationError = (res: NextApiResponse, error: z.ZodError): void => { + const errors = error.errors.map((err) => ({ + path: err.path.join("."), + errorCode: `error.validation.${err.code}`, + })); + + res.status(400).json({ + message: "VALIDATION_ERROR", + errors, + }); +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const result = querySchema.safeParse(req.query); + if (!result.success) { + return handleValidationError(res, result.error); + } + + const { uuid: objectKey } = result.data; + + let img; + try { + const { data } = await prisma.avatar.findUniqueOrThrow({ + where: { + objectKey, + }, + select: { + data: true, + }, + }); + img = data; + } catch (e) { + // If anything goes wrong or avatar is not found, use default avatar + res.writeHead(302, { + Location: AVATAR_FALLBACK, + }); + + res.end(); + return; + } + + const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", ""); + const imageResp = Buffer.from(decoded, "base64"); + + res.writeHead(200, { + "Content-Type": "image/png", + "Content-Length": imageResp.length, + }); + + res.end(imageResp); +} diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index 99e2418d80..6bd871bf7c 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -9,8 +9,8 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage"; import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; import turndown from "@calcom/lib/turndownService"; @@ -82,19 +82,12 @@ const ProfileView = () => { const { t } = useLocale(); const utils = trpc.useContext(); const { update } = useSession(); + const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const [fetchedImgSrc, setFetchedImgSrc] = useState(""); - - const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, { - onSuccess: async (userData) => { - try { - const res = await fetch(userData.avatar); - if (res.url) setFetchedImgSrc(res.url); - } catch (err) { - setFetchedImgSrc(""); - } - }, + const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, { + enabled: !isLoading && !user?.avatarUrl, }); + const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ onSuccess: async (res) => { await update(res); @@ -226,7 +219,7 @@ const ProfileView = () => { const defaultValues = { username: user.username || "", - avatar: fetchedImgSrc || "", + avatar: getUserAvatarUrl(user), name: user.name || "", email: user.email || "", bio: user.bio || "", @@ -243,6 +236,7 @@ const ProfileView = () => { key={JSON.stringify(defaultValues)} defaultValues={defaultValues} isLoading={updateProfileMutation.isLoading} + isFallbackImg={!user.avatarUrl && !avatarData?.avatar} user={user} userOrganization={user.organization} onSubmit={(values) => { @@ -387,6 +381,7 @@ const ProfileForm = ({ onSubmit, extraField, isLoading = false, + isFallbackImg, user, userOrganization, }: { @@ -394,6 +389,7 @@ const ProfileForm = ({ onSubmit: (values: FormValues) => void; extraField?: React.ReactNode; isLoading: boolean; + isFallbackImg: boolean; user: RouterOutputs["viewer"]["me"]; userOrganization: RouterOutputs["viewer"]["me"]["organization"]; }) => { @@ -432,7 +428,7 @@ const ProfileForm = ({ control={formMethods.control} name="avatar" render={({ field: { value } }) => { - const showRemoveAvatarButton = !checkIfItFallbackImage(value); + const showRemoveAvatarButton = value === null ? false : !isFallbackImg; const organization = userOrganization && userOrganization.id ? { diff --git a/apps/web/playwright/fixtures/cal.png b/apps/web/playwright/fixtures/cal.png new file mode 100644 index 0000000000000000000000000000000000000000..8d300dfa0f407fd270f6fa5abfc191bab241eff2 GIT binary patch literal 43681 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
0lx7!|nArDn>Yw14p zT2~#Vsr*6P)L1p?)xmkYGu}Dhj&fNVuR%u0p(kZeuyd=QSXbw_Vh`WhgYze zdps#-H3NdV#umv{N9I9UZdUGwfrEe2_kq*_*O+n=DJYftz!U0GHK5f5h-%1ax0gkE zeC#dvqw#};UE1NLa!nv}Cci3op~;QkBJ{~ml$J`Ce$2;#g^zQ*tX+L9Lr!fd+kQ@% zCKjBa8J|tO#i35k9&}ldL@Do$5AYbrvQ{td&Atn#eC{ciI+6X%^LJ=gNl%=K0l7RT zi8KY!cdx!lYMW%sZdb>SXf)GcGEquW03V-$ErvQg`!K3E;r1|AhI(@B<33XBE<9sH zuI_%VJEpyDHQ}OfAcqcmn|7=K8{<2u1_OtTX1tM!YV-aSz8g@VnN6LzAci|f3iTXp ziEPt_<~{KyL-dz@+VZU;+Is+~he+~5?k762(G{&}Sw_-<CcM3BEE#)( zOoF->Cq%?>WziUr>LDHsL2_D&f})y4bTEnWG|c2pZ>;#%Y5mM93`>OQ$BihATgFFT zaIiR>*Qv_+FH7hB?1nz4K}A+6vAM!LTo~U{h>AX}gZ$F?O0Tek_8@Xg?o4)4j;x?@ zU;t1F#mH?h9;oNkRTKWSO=)$)=9VQVc4cjUcJde-hQjm2wC=ALK0+TUw-v2^^`!|^ zk3Ad&&$PM%#Iz=xCq3G4jbL)?BtWfA@@@LWBW>x8h1ui7?4v(Zoj8e&somjcjoxFc zs+FCHR1rU(Watg5(}h7V*0j0wA^wx^^&HcCU%??=y=A~{=8scps)H$YSAs?Zn3jCo zFZ+TFy(21XwiR4_-kVxvNFe}4^S?JO*xb`?=+W^%JDd_oFQ$+3jHXAbkmF4%x|SPc&lg5a(8+XDVLPYV@5|h!>w9e)Yuf8rY53Z znO;L@V)S*=mvm@R?s4KR=`-`~M(NDKp2S0`ph}rM5$(D*6XEo!fwjXJ*TXmc6!$`9 z6W#($c{7e!(XVm=-lVI31LmNESy5oXd>gn?CqsD$XY`nNwqNY2E+g5nIPdhL~+W|1w}d~%yAhYsSEtRESho`DCfoyQWdN*j`5S8+fq*5lER9E z1FBI>wyB&7MJwCW0!Q3be%9B8Lp<`O6#9ewDo6PIOZdJ@#U9lC630W~^B7&{sUCZ@ z_?6ptd)nMMcnGOPVh!Y&$h%3fG?twpHM*yIqXYn<3%( zO#>F*4WjngPxC}|Gqo$etG2IoQAcrZS01}&u&KUL_)8A-+OHbT^cUi0Drf(REfLqE9S(Qn6JM7q07LpjyPv z?vZ}mrqOO3g+z_yCM^yJpw0teZN3^dyHFhR_85Ha)#={iNr zgPC6wwJ`Tc~WpYkfvCxtgG*Z(p2zr+(M z5)%=<`qT86j$~KSCUG9cGnHEn(4)o*i<-3Ud%-B_*{Aqn$enPoSPvX74So^Z;)+pY zyH51#_02y&|ED?rhs68WHG3&jV%9f;GBs;NWv4IVrQfW_3%=CqG# z%2D?l^iNfi?Z>nb!Oa}~`G$#G1II61cD}L5e6A^1{X6|^wGa+yR-k9BSD_8lcrW>r z5e%-8-*!p17t#>%tD|0ueeU=rKj=iP_9g|W%}j*gzx$4VT=_4}|K0wt6m#1TzlYX@ z%y2pay6Z2{yCd0~>k_Ce$&~TwUF(;mtT%bfP;JWxGkZ4cM~`A_Ereh#MmCOZp9>s^ zUiWP1!lh-JeHH&!M~e+%(_+B=U3M89!eAiezpcY3sWh6}*x{2hx}U&9fj zBMfMIU$}x7zV@ulo(bROai5A@OLl{q7`a!@A3NLos}0h0_DRY2794(=>(SRVZl!S1 zc-XUfe2Aw#R`#CcEaSHtLfOccLjRLK|BTvC3I==AVT^G#ZDYdQr^kykRtpdIm8ILF z7j#OuO|%g~=;e&khfOX6cPpCXKc{yjC8`i&@*nf%&kX$^Q|K>0+zhGcrgXEH8GyEz zsb3+0D&7q&DBDB=xf3~`97w>Z%H0)j%OHZ2MSHLXdjNAkVZfXs?}}&36Ecv1fsfU- zRzhT1dtRz+;v8ko56^(8p2D+&s8Ql8XK(qQeDWeC^!kcL=0#kl%Eki%7JagQr%Zs6 zzYv4BWa0)9z6UyPbB)koo%&XR@O=7}6LoIKb=EBwg$|ccyr8fygd0^%{>H@L1HI>; zNZzM>4Ob~(fB6;ll8eDZXVb*eHoYiFj`{tN&oFkI>%aXbc(-n9mRRc#f{<~2sQRP4 zhT>m$WFOS75bj1HB3J)03=eK5_1SYNi?+zeSh!OIcm$4o+qQkzs;PA`1%+7*E3=;p ze4bUZ|K2W0lNTwZLI&1f;n}{zt)o z$KZc~jv(<)A3U@rAv;X+F@B;I_IvTZ`iEU?vAiBR+VZ^iB{m0Z0>xrG6L!Zv|CYGP z^CnO5yW&l^Pnrz_j zjFYNxIbZ3~lubhJxfj>eg3M44rkER~vvmr?+I#zkM*g0jyqt?>)Mltov3}ga-mZEsC?V~LJM>Gnx$~V3sq@HBLBitykCdKLbh8F6Fp&( z{C`*D`|6G0W$x7u@wvsDCp+!)hGteC8S^O|=ml2ZiWGLImf=1gU{frwE5yWYukjJ2 z>W;co?MnyUHT^sMEzqFef;+{q_2E)iD}3MT08qDMyaH)!&ty;R5C1Jh&-!V2_S%&{ z_T?rf!W8S!5xG8)tx>p2^k-;&ixE2n>-g#qsR=F?-9O4SzrJm(3;Z*7~c=k_9o&zx($76TbdQ^D(GV2ZTaRnI<|FWA4z&EkvA+@Vug`RxHgda3 zk3(X*b9QrC$MRz5(}Tt%m}*KJb=L+d_9`mAVAdWAAf(MoAFD1cUf}=JYVog)!j8(8 zg?WujkPVmWXWo8Vlub)NqHNy$FnF9D;2Q@_JuBaXmjp7^AA9PXgIbx8Sd?e|l=lk{ z^pYw5UQ`0KoL8~-dM>n^es?LIL2@5p(Qf%-OyTeBa!_^5%VN`A+h;SoNfHQ{yF-r8 zByF$iXCgvc2wU5at(=;fr4^^~NAxHM_{YDq*JF6g#%C&s(CtJWNF+lVig)M=VJDCJ zw;a=buE91|@U+taE%@&RbP|+|PB+2%^Tt!@2F}e+>e(&Dme&^xHCLAM$>EB%iMx@) z=!zGnDI#-|K8qFmwaf5OCj&q0a=6)pzam-Y|K3B74zVEa<$+}1D$e~7ZQw&XT=VS! zb;X(dt{HJh`Hz$xwjr_AzNliH^UzXKyH0wzUbnNT>!ZUN%$B#6!y%Ql{ZGnNPoRQ=3$eyMFr%+DFh@{|zu OL_k#?l?o;6kpBYrs{ { + test("can upload an image", async ({ page, users }) => { + const user = await users.create({}); + await user.apiLogin(); + + await test.step("Can upload an initial picture", async () => { + await page.goto("/settings/my-account/profile"); + + await page.getByTestId("open-upload-avatar-dialog").click(); + + const [fileChooser] = await Promise.all([ + // It is important to call waitForEvent before click to set up waiting. + page.waitForEvent("filechooser"), + // Opens the file chooser. + page.getByTestId("open-upload-image-filechooser").click(), + ]); + + await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`); + + await page.getByTestId("upload-avatar").click(); + + await page.locator("input[name='name']").fill(user.email); + + await page.getByText("Update").click(); + await page.waitForSelector("text=Settings updated successfully"); + + const response = await prisma.avatar.findUniqueOrThrow({ + where: { + teamId_userId: { + userId: user.id, + teamId: 0, + }, + }, + }); + + // todo: remove this; ideally the organization-avatar is updated the moment + // 'Settings updated succesfully' is saved. + await page.waitForLoadState("networkidle"); + + await expect(await page.getByTestId("organization-avatar").innerHTML()).toContain(response.objectKey); + + const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, { + maxRedirects: 0, + }); + + await expect(urlResponse?.status()).toBe(200); + }); + }); +}); diff --git a/apps/web/test/handlers/requestReschedule.test.ts b/apps/web/test/handlers/requestReschedule.test.ts index 09e01ce308..b6ac6cfab8 100644 --- a/apps/web/test/handlers/requestReschedule.test.ts +++ b/apps/web/test/handlers/requestReschedule.test.ts @@ -268,6 +268,7 @@ function getTrpcHandlerData({ user: { ...getSampleUserInSession(), ...user, + avatarUrl: user.avatarUrl || null, } satisfies TrpcSessionUser, }, input: input, diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 95f217cb15..677c43f5e4 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -10,6 +10,7 @@ import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; @@ -145,7 +146,7 @@ const useTabs = () => { if (tab.href === "/settings/my-account") { tab.name = user?.name || "my_account"; tab.icon = undefined; - tab.avatar = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${session?.data?.user?.username}/avatar.png`; + tab.avatar = getUserAvatarUrl(user); } else if (tab.href === "/settings/organizations") { tab.name = orgBranding?.name || "organization"; tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`; diff --git a/packages/lib/getAvatarUrl.ts b/packages/lib/getAvatarUrl.ts index 2c971be827..f1d11aba42 100644 --- a/packages/lib/getAvatarUrl.ts +++ b/packages/lib/getAvatarUrl.ts @@ -6,19 +6,28 @@ import type { User, Team } from "@calcom/prisma/client"; * Gives an organization aware avatar url for a user * It ensures that the wrong avatar isn't fetched by ensuring that organizationId is always passed */ -export const getUserAvatarUrl = (user: Pick) => { - if (!user.username) return AVATAR_FALLBACK; +export const getUserAvatarUrl = ( + user: (Pick & { avatarUrl?: string | null }) | undefined +) => { + if (user?.avatarUrl) { + return user.avatarUrl; + } + if (!user?.username) return AVATAR_FALLBACK; // avatar.png automatically redirects to fallback avatar if user doesn't have one return `${WEBAPP_URL}/${user.username}/avatar.png${ user.organizationId ? `?orgId=${user.organizationId}` : "" }`; }; -export const getOrgAvatarUrl = (org: { - id: Team["id"]; - slug: Team["slug"]; - requestedSlug: string | null; -}) => { +export const getOrgAvatarUrl = ( + org: Pick & { + logoUrl?: string | null; + requestedSlug: string | null; + } +) => { + if (org.logoUrl) { + return org.logoUrl; + } const slug = org.slug ?? org.requestedSlug; return `${WEBAPP_URL}/org/${slug}/avatar.png`; }; diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index e9eb598659..4a2f79c891 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -191,6 +191,7 @@ export const buildUser = >(user?: T): UserPayload allowDynamicBooking: true, availability: [], avatar: "", + avatarUrl: "", away: false, backupCodes: null, bio: null, diff --git a/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql b/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql new file mode 100644 index 0000000000..2d539596fe --- /dev/null +++ b/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql @@ -0,0 +1,19 @@ +-- AlterTable +ALTER TABLE "Team" ADD COLUMN "logoUrl" TEXT; + +-- AlterTable +ALTER TABLE "users" ADD COLUMN "avatarUrl" TEXT; + +-- CreateTable +CREATE TABLE "avatars" ( + "teamId" INTEGER NOT NULL DEFAULT 0, + "userId" INTEGER NOT NULL DEFAULT 0, + "data" TEXT NOT NULL, + "objectKey" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "avatars_objectKey_key" ON "avatars"("objectKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "avatars_teamId_userId_key" ON "avatars"("teamId", "userId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 3130a9d423..8cc61111fa 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -192,6 +192,7 @@ model User { password String? bio String? avatar String? + avatarUrl String? timeZone String @default("Europe/London") weekStart String @default("Sunday") // DEPRECATED - TO BE REMOVED @@ -279,6 +280,7 @@ model Team { /// @zod.min(1) slug String? logo String? + logoUrl String? appLogo String? appIconLogo String? bio String? @@ -1011,3 +1013,17 @@ model TempOrgRedirect { @@unique([from, type, fromOrgId]) } + +model Avatar { + // e.g. NULL(0), organization ID or team logo + teamId Int @default(0) + // Avatar, NULL(0) if team logo + userId Int @default(0) + // base64 string + data String + // different every time to pop the cache. + objectKey String @unique + + @@unique([teamId, userId]) + @@map(name: "avatars") +} diff --git a/packages/trpc/server/middlewares/sessionMiddleware.ts b/packages/trpc/server/middlewares/sessionMiddleware.ts index 46b55b6450..3eae4eb1a8 100644 --- a/packages/trpc/server/middlewares/sessionMiddleware.ts +++ b/packages/trpc/server/middlewares/sessionMiddleware.ts @@ -30,6 +30,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe { + const data = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + select: { + avatar: true, + }, + }); return { - avatar: ctx.user.avatar, + avatar: data?.avatar, }; }; diff --git a/packages/trpc/server/routers/loggedInViewer/me.handler.ts b/packages/trpc/server/routers/loggedInViewer/me.handler.ts index 3b53cfa0c6..4c4b72f648 100644 --- a/packages/trpc/server/routers/loggedInViewer/me.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/me.handler.ts @@ -25,6 +25,7 @@ export const meHandler = async ({ ctx }: MeOptions) => { timeFormat: user.timeFormat, timeZone: user.timeZone, avatar: getUserAvatarUrl(user), + avatarUrl: user.avatarUrl, createdDate: user.createdDate, trialEndsAt: user.trialEndsAt, defaultScheduleId: user.defaultScheduleId, diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 4231d45dc2..c790a446ad 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -1,5 +1,6 @@ import type { Prisma } from "@prisma/client"; import type { GetServerSidePropsContext, NextApiResponse } from "next"; +import { v4 as uuidv4 } from "uuid"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils"; @@ -31,12 +32,35 @@ type UpdateProfileOptions = { input: TUpdateProfileInputSchema; }; +const uploadAvatar = async ({ userId, avatar: data }: { userId: number; avatar: string }) => { + const objectKey = uuidv4(); + + await prisma.avatar.upsert({ + where: { + teamId_userId: { + teamId: 0, + userId, + }, + }, + create: { + userId: userId, + data, + objectKey, + }, + update: { + data, + objectKey, + }, + }); + + return `/api/avatar/${objectKey}.png`; +}; + export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => { const { user } = ctx; const userMetadata = handleUserMetadata({ ctx, input }); const data: Prisma.UserUpdateInput = { ...input, - avatar: input.avatar ? await getAvatarToSet(input.avatar) : null, metadata: userMetadata, }; @@ -114,6 +138,15 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) // when the email changes, the user needs to sign in again. signOutUser = true; } + // don't do anything if avatar is undefined. + if (typeof input.avatar !== "undefined") { + data.avatarUrl = input.avatar + ? await uploadAvatar({ + avatar: await resizeBase64Image(input.avatar), + userId: user.id, + }) + : null; + } const updatedUser = await prisma.user.update({ where: { @@ -129,6 +162,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) metadata: true, name: true, createdDate: true, + avatarUrl: true, locale: true, schedules: { select: { @@ -186,28 +220,11 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) }, }); } - // Revalidate booking pages - // Disabled because the booking pages are currently not using getStaticProps - /*const res = ctx.res as NextApiResponse; - if (typeof res?.revalidate !== "undefined") { - const eventTypes = await prisma.eventType.findMany({ - where: { - userId: user.id, - team: null, - }, - select: { - id: true, - slug: true, - }, - }); - // waiting for this isn't needed - Promise.all( - eventTypes.map((eventType) => res?.revalidate(`/new-booker/${ctx.user.username}/${eventType.slug}`)) - ) - .then(() => console.info("Booking pages revalidated")) - .catch((e) => console.error(e)); - }*/ - return { ...input, signOutUser, passwordReset }; + + // don't return avatar, we don't need it anymore. + delete input.avatar; + + return { ...input, signOutUser, passwordReset, avatarUrl: updatedUser.avatarUrl }; }; const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => { @@ -230,17 +247,3 @@ const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => { // Required so we don't override and delete saved values return { ...userMetadata, ...cleanMetadata }; }; - -async function getAvatarToSet(avatar: string | null | undefined) { - if (avatar === null || avatar === undefined) { - return avatar; - } - - if (!avatar.startsWith("data:image")) { - // Non Base64 avatar currently could only be the dynamic avatar URL(i.e. /{USER}/avatar.png). If we allow setting that URL, we would get infinite redirects on /user/avatar.ts endpoint - log.warn("Non Base64 avatar, ignored it", { avatar }); - // `undefined` would not ignore the avatar, but `null` would remove it. So, we return `undefined` here. - return undefined; - } - return await resizeBase64Image(avatar); -} diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 06971f9745..836367155a 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -177,6 +177,7 @@ export const DialogTrigger = DialogPrimitive.Trigger; export function DialogClose( props: { + "data-testid"?: string; dialogCloseProps?: React.ComponentProps<(typeof DialogPrimitive)["Close"]>; children?: ReactNode; onClick?: (e: React.MouseEvent) => void; @@ -188,7 +189,10 @@ export function DialogClose( return ( {/* This will require the i18n string passed in */} - diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index 1f2c5089e5..6f77756f07 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -170,7 +170,11 @@ export default function ImageUploader({ } }}> - @@ -190,7 +194,9 @@ export default function ImageUploader({
)} {result && } - + {t("community_support")}{" "} + + ); } From ed2ce005c9587c2081d2fac01f4d417bece84f55 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 20 Nov 2023 16:42:20 +0000 Subject: [PATCH 091/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/ar/common.json | 1 + apps/web/public/static/locales/cs/common.json | 1 + apps/web/public/static/locales/de/common.json | 1 + apps/web/public/static/locales/es/common.json | 1 + apps/web/public/static/locales/fr/common.json | 1 + apps/web/public/static/locales/he/common.json | 1 + apps/web/public/static/locales/it/common.json | 1 + apps/web/public/static/locales/ja/common.json | 1 + apps/web/public/static/locales/ko/common.json | 1 + apps/web/public/static/locales/nl/common.json | 1 + apps/web/public/static/locales/pl/common.json | 1 + apps/web/public/static/locales/pt-BR/common.json | 1 + apps/web/public/static/locales/pt/common.json | 1 + apps/web/public/static/locales/ro/common.json | 1 + apps/web/public/static/locales/ru/common.json | 1 + apps/web/public/static/locales/sr/common.json | 1 + apps/web/public/static/locales/sv/common.json | 1 + apps/web/public/static/locales/tr/common.json | 1 + apps/web/public/static/locales/uk/common.json | 1 + apps/web/public/static/locales/vi/common.json | 1 + apps/web/public/static/locales/zh-CN/common.json | 1 + apps/web/public/static/locales/zh-TW/common.json | 1 + 22 files changed, 22 insertions(+) diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index a156291271..438753aad1 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "مستندات المطور", "get_in_touch": "تواصل معنا", "contact_support": "الاتصال بالدعم", + "community_support": "الدعم المجتمعي", "feedback": "الملاحظات", "submitted_feedback": "نشكرك على ملاحظاتك!", "feedback_error": "حدث خطأ عند إرسال الملاحظات", diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index 6f8993cb9f..3090110820 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Dokumentace vývojáře", "get_in_touch": "Kontaktujte nás", "contact_support": "Kontaktujte podporu", + "community_support": "Podpora komunity", "feedback": "Zpětná vazba", "submitted_feedback": "Děkujeme za vaši zpětnou vazbu!", "feedback_error": "Chyba při odesílání zpětné vazby", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 1ab3564dff..a9da73eb13 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Entwickler-Dokumentation", "get_in_touch": "Kontakt aufnehmen", "contact_support": "Support kontaktieren", + "community_support": "Community Support", "feedback": "Feedback", "submitted_feedback": "Vielen Dank für Ihr Feedback!", "feedback_error": "Fehler beim Senden des Feedbacks", diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 271b5e7c18..74f2701815 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentación del desarrollador", "get_in_touch": "Póngase en contacto", "contact_support": "Contactar con Soporte", + "community_support": "Soporte comunitario", "feedback": "Comentarios", "submitted_feedback": "¡Gracias por sus comentarios!", "feedback_error": "Error al enviar comentarios", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index e0bab7ee71..49dc72ca82 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentation pour développeurs", "get_in_touch": "Contactez-nous", "contact_support": "Contacter l'assistance", + "community_support": "Aide communautaire", "feedback": "Commentaires", "submitted_feedback": "Merci pour vos commentaires !", "feedback_error": "Erreur lors de l'envoi du commentaire", diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index fdb11fce61..8940a363c2 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "מסמכי מפתחים", "get_in_touch": "יצירת קשר", "contact_support": "פנייה לתמיכה", + "community_support": "תמיכת קהילה", "feedback": "משוב", "submitted_feedback": "תודה על המשוב!", "feedback_error": "שגיאה בעת שליחת משוב", diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 39c8a82614..bd4b918ace 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentazione sviluppatore", "get_in_touch": "Contattaci", "contact_support": "Contatta il supporto", + "community_support": "Supporto della community", "feedback": "Feedback", "submitted_feedback": "Grazie per il tuo feedback!", "feedback_error": "Errore durante l'invio del feedback", diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 09b9705e4c..60af134a30 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "開発者向けドキュメント", "get_in_touch": "お問い合わせ", "contact_support": "サポートに連絡", + "community_support": "コミュニティサポート", "feedback": "フィードバック", "submitted_feedback": "フィードバックをありがとうございます!", "feedback_error": "フィードバックの送信エラー", diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index 4bae904c16..09688e49f9 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "개발자 문서", "get_in_touch": "연락하기", "contact_support": "지원 문의", + "community_support": "커뮤니티 지원", "feedback": "피드백", "submitted_feedback": "피드백을 주셔서 감사합니다!", "feedback_error": "피드백을 보내는 중 오류 발생", diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index 7aed827cc4..dddbfd107b 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Ontwikkelaarsdocumentatie", "get_in_touch": "Neem contact op", "contact_support": "Neem contact op met de ondersteuning", + "community_support": "Ondersteuning door de community", "feedback": "Feedback", "submitted_feedback": "Bedankt voor uw feedback!", "feedback_error": "Fout bij verzenden van feedback", diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index f228db75b5..833b51d4aa 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Dokumentacja dla programistów", "get_in_touch": "Kontakt", "contact_support": "Skontaktuj się z pomocą techniczną", + "community_support": "Wsparcie społeczności", "feedback": "Opinia", "submitted_feedback": "Dziękujemy za opinię!", "feedback_error": "Podczas wysyłania opinii wystąpił błąd", diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index f4721862d2..6d2372e417 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentação do desenvolvedor", "get_in_touch": "Entrar em contato", "contact_support": "Fale com o suporte", + "community_support": "Suporte da comunidade", "feedback": "Comentário", "submitted_feedback": "Agradecemos o seu comentário!", "feedback_error": "Erro ao enviar comentário", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index f2b075e7fc..8c784c7006 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentação para programadores", "get_in_touch": "Entre em contacto", "contact_support": "Contacte o suporte", + "community_support": "Apoio da Comunidade", "feedback": "Feedback", "submitted_feedback": "Obrigado pelo seu feedback!", "feedback_error": "Erro ao enviar feedback", diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index f294bc25dd..9a5c4a1e07 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentație pentru programatori", "get_in_touch": "Contactați-ne", "contact_support": "Contactați echipa de asistență", + "community_support": "Asistență comunitate", "feedback": "Feedback", "submitted_feedback": "Vă mulțumim pentru feedback!", "feedback_error": "Eroare la trimiterea feedbackului", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 474d6744d9..6a4dc2f2b6 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Документация для разработчиков", "get_in_touch": "Связаться с нами", "contact_support": "Обратиться в службу поддержки", + "community_support": "Поддержка со стороны сообщества", "feedback": "Отзыв", "submitted_feedback": "Спасибо за отзыв!", "feedback_error": "Ошибка при отправке отзыва", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 9801220ae8..d209e645af 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Dokumentacija za razvojne programere", "get_in_touch": "Kontaktirajte nas", "contact_support": "Obratite se podršci", + "community_support": "Podrška zajednice", "feedback": "Povratne informacije", "submitted_feedback": "Hvala vam na povratnim informacijama!", "feedback_error": "Greška pri slanju povratnih informacija", diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index dddf3cf7f4..1dfe0438e3 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Utvecklardokumentation", "get_in_touch": "Kontakta oss", "contact_support": "Kontakta support", + "community_support": "Communitysupport", "feedback": "Feedback", "submitted_feedback": "Tack för din feedback!", "feedback_error": "Det gick inte att skicka feedback", diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 51a402d4ed..48bcfa4819 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Geliştirici Belgeleri", "get_in_touch": "Bize ulaşın", "contact_support": "Destek ile iletişime geçin", + "community_support": "Topluluk Desteği", "feedback": "Geri bildirim", "submitted_feedback": "Geri bildiriminiz için teşekkür ederiz!", "feedback_error": "Geri bildirim gönderilirken bir hata oluştu", diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index c9420f5064..1fc67deab6 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Документація для розробників", "get_in_touch": "Наші контакти", "contact_support": "Служба підтримки", + "community_support": "Підтримка спільноти", "feedback": "Відгук", "submitted_feedback": "Дякуємо за відгук!", "feedback_error": "Не вдалося надіслати відгук", diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index 5b22f88916..b6470ef6b2 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Tài liệu nhà phát triển", "get_in_touch": "Liên lạc", "contact_support": "Liên hệ với bộ phận hỗ trợ", + "community_support": "Hỗ trợ cộng đồng", "feedback": "Phản hồi", "submitted_feedback": "Cám ơn bạn đã phản hồi!", "feedback_error": "Có lỗi khi gửi phản hồi", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index ea01b7fe6d..14dc1b7d4e 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -1099,6 +1099,7 @@ "developer_documentation": "开发人员文档", "get_in_touch": "保持联系", "contact_support": "联系支持", + "community_support": "社区支持", "feedback": "反馈", "submitted_feedback": "感谢您的反馈!", "feedback_error": "发送反馈时出错", diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 055f29449e..41a1af0db3 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "開發人員文件", "get_in_touch": "保持聯絡", "contact_support": "聯絡支援", + "community_support": "社群支援", "feedback": "回饋意見", "submitted_feedback": "感謝您的回饋意見!", "feedback_error": "傳送回饋意見時發生錯誤", From 00553e897bca9b8a63b6854a3121f775a94b2564 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:09:50 +0200 Subject: [PATCH 092/119] fix: alby payment could not be created (#12460) * fix: alby payment could not be created * fixup! fix: alby payment could not be created * fixup! fixup! fix: alby payment could not be created --- packages/app-store/alby/lib/PaymentService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/app-store/alby/lib/PaymentService.ts b/packages/app-store/alby/lib/PaymentService.ts index 9974f1aa25..71e9c3e851 100644 --- a/packages/app-store/alby/lib/PaymentService.ts +++ b/packages/app-store/alby/lib/PaymentService.ts @@ -72,7 +72,10 @@ export class PaymentService implements IAbstractPaymentService { amount: payment.amount, externalId: invoice.paymentRequest, currency: payment.currency, - data: Object.assign({}, { invoice }) as unknown as Prisma.InputJsonValue, + data: Object.assign( + {}, + { invoice: { ...invoice, isPaid: await invoice.isPaid() } } + ) as unknown as Prisma.InputJsonValue, fee: 0, refunded: false, success: false, @@ -84,7 +87,7 @@ export class PaymentService implements IAbstractPaymentService { } return paymentData; } catch (error) { - log.error("Alby: Payment could not be created", bookingId); + log.error("Alby: Payment could not be created", bookingId, JSON.stringify(error)); throw new Error(ErrorCode.PaymentCreationFailure); } } From 404bc0e4d6bb4066712ba21ebf7775b927117360 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 21 Nov 2023 12:13:50 +0000 Subject: [PATCH 093/119] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 49dc72ca82..0bf2d447f7 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -1098,6 +1098,7 @@ "developer_documentation": "Documentation pour développeurs", "get_in_touch": "Contactez-nous", "contact_support": "Contacter l'assistance", + "premium_support": "Assistance Premium", "community_support": "Aide communautaire", "feedback": "Commentaires", "submitted_feedback": "Merci pour vos commentaires !", From 85237c49851584f62eb56707bbe5358e1d88d5e2 Mon Sep 17 00:00:00 2001 From: Ujjwal Goyal <35370133+ujjwalgoyal19@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:26:59 +0530 Subject: [PATCH 094/119] fix: Date overrides UI bug depending on screen size (#12423) * Update DateOverrideInputDialog.tsx fix: Date overrides UI bug depending on screen size (calcom#12406) * chore: remove comment --------- Co-authored-by: madhurgoyal19 <35370133+madhurgoyal19@users.noreply.github.com> Co-authored-by: Udit Takkar --- .../schedules/components/DateOverrideInputDialog.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/features/schedules/components/DateOverrideInputDialog.tsx b/packages/features/schedules/components/DateOverrideInputDialog.tsx index 48612ff4ce..a628e81b0c 100644 --- a/packages/features/schedules/components/DateOverrideInputDialog.tsx +++ b/packages/features/schedules/components/DateOverrideInputDialog.tsx @@ -5,7 +5,6 @@ import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import type { WorkingHours } from "@calcom/types/schedule"; import { Dialog, @@ -210,19 +209,12 @@ const DateOverrideInputDialog = ({ onChange: (newValue: TimeRange[]) => void; value?: TimeRange[]; }) => { - const isMobile = useMediaQuery("(max-width: 768px)"); const [open, setOpen] = useState(false); - { - /* enableOverflow is used to allow overflow when there are too many overrides to show on mobile. - ref:- https://github.com/calcom/cal.com/pull/6215 - */ - } - const enableOverflow = isMobile; return ( {Trigger} - + Date: Tue, 21 Nov 2023 17:14:25 +0200 Subject: [PATCH 095/119] fix: better errors for googlecalendar integration (#12403) --- packages/app-store/googlecalendar/api/add.ts | 48 ++++++++++--------- .../app-store/googlecalendar/api/callback.ts | 17 ++++--- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/app-store/googlecalendar/api/add.ts b/packages/app-store/googlecalendar/api/add.ts index 3a32c968fa..7ed6fcf02d 100644 --- a/packages/app-store/googlecalendar/api/add.ts +++ b/packages/app-store/googlecalendar/api/add.ts @@ -2,6 +2,8 @@ import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState"; @@ -14,28 +16,30 @@ const scopes = [ let client_id = ""; let client_secret = ""; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") { - // Get token from Google Calendar API - const appKeys = await getAppKeysFromSlug("google-calendar"); - if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; - if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; - if (!client_id) return res.status(400).json({ message: "Google client_id missing." }); - if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." }); - const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`; - const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); +async function getHandler(req: NextApiRequest, res: NextApiResponse) { + // Get token from Google Calendar API + const appKeys = await getAppKeysFromSlug("google-calendar"); + if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; + if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; + if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." }); + if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." }); + const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: "offline", - scope: scopes, - // A refresh token is only returned the first time the user - // consents to providing access. For illustration purposes, - // setting the prompt to 'consent' will force this consent - // every time, forcing a refresh_token to be returned. - prompt: "consent", - state: encodeOAuthState(req), - }); + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: "offline", + scope: scopes, + // A refresh token is only returned the first time the user + // consents to providing access. For illustration purposes, + // setting the prompt to 'consent' will force this consent + // every time, forcing a refresh_token to be returned. + prompt: "consent", + state: encodeOAuthState(req), + }); - res.status(200).json({ url: authUrl }); - } + res.status(200).json({ url: authUrl }); } + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index 2b3d2d90b0..3577e9b092 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; @@ -12,24 +14,23 @@ import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; let client_id = ""; let client_secret = ""; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +async function getHandler(req: NextApiRequest, res: NextApiResponse) { const { code } = req.query; const state = decodeOAuthState(req); if (typeof code !== "string") { - res.status(400).json({ message: "`code` must be a string" }); - return; + throw new HttpError({ statusCode: 400, message: "`code` must be a string" }); } if (!req.session?.user?.id) { - return res.status(401).json({ message: "You must be logged in to do this" }); + throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" }); } const appKeys = await getAppKeysFromSlug("google-calendar"); if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; - if (!client_id) return res.status(400).json({ message: "Google client_id missing." }); - if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." }); + if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." }); + if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." }); const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`; @@ -107,3 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) getInstalledAppPath({ variant: "calendar", slug: "google-calendar" }) ); } + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); From 48dde246e92fd2ed94c84bf39389078ab9ec639a Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 21 Nov 2023 22:33:01 +0530 Subject: [PATCH 096/119] test: Add more orgs tests (#12241) --- apps/web/pages/signup.tsx | 4 +- apps/web/playwright/fixtures/clipboard.ts | 34 +++++ apps/web/playwright/fixtures/users.ts | 20 ++- apps/web/playwright/lib/fixtures.ts | 9 ++ apps/web/playwright/lib/testUtils.ts | 11 ++ apps/web/playwright/organization/expects.ts | 28 ++++ .../organization/organization-creation.e2e.ts | 143 ++++++++++++++++++ .../organization-invitation.e2e.ts | 119 +++++++++++++++ package.json | 1 + .../organizations/pages/settings/members.tsx | 18 --- .../components/MemberInvitationModal.tsx | 12 +- .../components/UserTable/UserListTable.tsx | 7 +- .../viewer/organizations/create.handler.ts | 8 +- .../organizations/verifyCode.handler.ts | 6 +- yarn.lock | 12 ++ 15 files changed, 397 insertions(+), 35 deletions(-) create mode 100644 apps/web/playwright/fixtures/clipboard.ts create mode 100644 apps/web/playwright/organization/expects.ts create mode 100644 apps/web/playwright/organization/organization-creation.e2e.ts create mode 100644 apps/web/playwright/organization/organization-invitation.e2e.ts diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 0b0cb0e5a8..041dcd943d 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -37,7 +37,7 @@ type SignupProps = inferSSRProps; const checkValidEmail = (email: string) => z.string().email().safeParse(email).success; const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => { - const [emailUser, emailDomain] = email.split("@"); + const [emailUser, emailDomain = ""] = email.split("@"); const username = emailDomain === autoAcceptEmailDomain ? slugify(emailUser) @@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA methods.clearErrors("apiError"); } - if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) { + if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) { methods.setValue( "username", getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail) diff --git a/apps/web/playwright/fixtures/clipboard.ts b/apps/web/playwright/fixtures/clipboard.ts new file mode 100644 index 0000000000..47cc92d95c --- /dev/null +++ b/apps/web/playwright/fixtures/clipboard.ts @@ -0,0 +1,34 @@ +import type { Page } from "@playwright/test"; + +declare global { + interface Window { + E2E_CLIPBOARD_VALUE?: string; + } +} + +export type Window = typeof window; +// creates the single server fixture +export const createClipboardFixture = (page: Page) => { + return { + reset: async () => { + await page.evaluate(() => { + delete window.E2E_CLIPBOARD_VALUE; + }); + }, + get: async () => { + return getClipboardValue({ page }); + }, + }; +}; + +function getClipboardValue({ page }: { page: Page }) { + return page.evaluate(() => { + return new Promise((resolve, reject) => { + setInterval(() => { + if (!window.E2E_CLIPBOARD_VALUE) return; + resolve(window.E2E_CLIPBOARD_VALUE); + }, 500); + setTimeout(() => reject(new Error("Timeout")), 1000); + }); + }); +} diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 0f07d18507..ae5fbfbec2 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -86,12 +86,14 @@ const createTeamAndAddUser = async ( user, isUnpublished, isOrg, + isOrgVerified, hasSubteam, organizationId, }: { - user: { id: number; username: string | null; role?: MembershipRole }; + user: { id: number; email: string; username: string | null; role?: MembershipRole }; isUnpublished?: boolean; isOrg?: boolean; + isOrgVerified?: boolean; hasSubteam?: true; organizationId?: number | null; }, @@ -103,7 +105,14 @@ const createTeamAndAddUser = async ( }; data.metadata = { ...(isUnpublished ? { requestedSlug: slug } : {}), - ...(isOrg ? { isOrganization: true } : {}), + ...(isOrg + ? { + isOrganization: true, + isOrganizationVerified: !!isOrgVerified, + orgAutoAcceptEmail: user.email.split("@")[1], + isOrganizationConfigured: false, + } + : {}), }; data.slug = !isUnpublished ? slug : undefined; if (isOrg && hasSubteam) { @@ -145,6 +154,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn teamEventSlug?: string; teamEventLength?: number; isOrg?: boolean; + isOrgVerified?: boolean; hasSubteam?: true; isUnpublished?: true; } = {} @@ -292,9 +302,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn if (scenario.hasTeam) { const team = await createTeamAndAddUser( { - user: { id: user.id, username: user.username, role: "OWNER" }, + user: { id: user.id, email: user.email, username: user.username, role: "OWNER" }, isUnpublished: scenario.isUnpublished, isOrg: scenario.isOrg, + isOrgVerified: scenario.isOrgVerified, hasSubteam: scenario.hasSubteam, organizationId: opts?.organizationId, }, @@ -410,6 +421,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { routingForms: user.routingForms, self, apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page), + /** + * @deprecated use apiLogin instead + */ login: async () => login({ ...(await self()), password: user.username }, store.page), logout: async () => { await page.goto("/auth/logout"); diff --git a/apps/web/playwright/lib/fixtures.ts b/apps/web/playwright/lib/fixtures.ts index 2e54268db3..cf66ebb2f9 100644 --- a/apps/web/playwright/lib/fixtures.ts +++ b/apps/web/playwright/lib/fixtures.ts @@ -4,10 +4,12 @@ import type { API } from "mailhog"; import mailhog from "mailhog"; import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { ExpectedUrlDetails } from "../../../../playwright.config"; import { createBookingsFixture } from "../fixtures/bookings"; +import { createClipboardFixture } from "../fixtures/clipboard"; import { createEmbedsFixture } from "../fixtures/embeds"; import { createOrgsFixture } from "../fixtures/orgs"; import { createPaymentsFixture } from "../fixtures/payments"; @@ -28,6 +30,7 @@ export interface Fixtures { emails?: API; routingForms: ReturnType; bookingPage: ReturnType; + clipboard: ReturnType; } declare global { @@ -85,6 +88,8 @@ export const test = base.extend({ const mailhogAPI = mailhog(); await use(mailhogAPI); } else { + //FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it + logger.warn("Mailhog is not enabled - Skipping Emails verification"); await use(undefined); } }, @@ -92,4 +97,8 @@ export const test = base.extend({ const bookingPage = createBookingPageFixture(page); await use(bookingPage); }, + clipboard: async ({ page }, use) => { + const clipboard = createClipboardFixture(page); + await use(clipboard); + }, }); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index b9cf3850d6..7038b656b1 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -1,11 +1,13 @@ import type { Frame, Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import { createHash } from "crypto"; import EventEmitter from "events"; import type { IncomingMessage, ServerResponse } from "http"; import { createServer } from "http"; // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; import type { API, Messages } from "mailhog"; +import { totp } from "otplib"; import type { Prisma } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; @@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees( }); return { user, eventType, booking }; } + +export function generateTotpCode(email: string) { + const secret = createHash("md5") + .update(email + process.env.CALENDSO_ENCRYPTION_KEY) + .digest("hex"); + + totp.options = { step: 90 }; + return totp.generate(secret); +} diff --git a/apps/web/playwright/organization/expects.ts b/apps/web/playwright/organization/expects.ts new file mode 100644 index 0000000000..e5ba1a0e83 --- /dev/null +++ b/apps/web/playwright/organization/expects.ts @@ -0,0 +1,28 @@ +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; +import { JSDOM } from "jsdom"; +// eslint-disable-next-line no-restricted-imports +import type { API, Messages } from "mailhog"; + +import { getEmailsReceivedByUser } from "../lib/testUtils"; + +export async function expectInvitationEmailToBeReceived( + page: Page, + emails: API | undefined, + userEmail: string, + subject: string, + returnLink?: string +) { + if (!emails) return null; + // We need to wait for the email to go through, otherwise it will fail + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(5000); + const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); + expect(receivedEmails?.total).toBe(1); + const [firstReceivedEmail] = (receivedEmails as Messages).items; + expect(firstReceivedEmail.subject).toBe(subject); + if (!returnLink) return; + const dom = new JSDOM(firstReceivedEmail.html); + const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`); + return anchor?.getAttribute("href"); +} diff --git a/apps/web/playwright/organization/organization-creation.e2e.ts b/apps/web/playwright/organization/organization-creation.e2e.ts new file mode 100644 index 0000000000..19b3477026 --- /dev/null +++ b/apps/web/playwright/organization/organization-creation.e2e.ts @@ -0,0 +1,143 @@ +import { expect } from "@playwright/test"; +import path from "path"; + +import { test } from "../lib/fixtures"; +import { generateTotpCode } from "../lib/testUtils"; +import { expectInvitationEmailToBeReceived } from "./expects"; + +test.afterAll(({ users, emails }) => { + users.deleteAll(); + emails?.deleteAll(); +}); + +function capitalize(text: string) { + if (!text) { + return text; + } + return text.charAt(0).toUpperCase() + text.slice(1); +} + +test.describe("Organization", () => { + test("should be able to create an organization and complete onboarding", async ({ + page, + users, + emails, + }) => { + const orgOwner = await users.create(); + const orgDomain = `${orgOwner.username}-org`; + const orgName = capitalize(`${orgOwner.username}-org`); + await orgOwner.apiLogin(); + await page.goto("/settings/organizations/new"); + await page.waitForLoadState("networkidle"); + + await test.step("Basic info", async () => { + // Check required fields + await page.locator("button[type=submit]").click(); + await expect(page.locator(".text-red-700")).toHaveCount(3); + + // Happy path + await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`); + expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName); + expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain); + await page.locator("button[type=submit]").click(); + await page.waitForLoadState("networkidle"); + + // Check admin email about code verification + await expectInvitationEmailToBeReceived( + page, + emails, + `john@${orgOwner.username}-org.com`, + "Verify your email to create an organization" + ); + + await test.step("Verification", async () => { + // Code verification + await expect(page.locator("#modal-title")).toBeVisible(); + await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`)); + await page.locator("button:text('Verify')").click(); + + // Check admin email about DNS pending action + await expectInvitationEmailToBeReceived( + page, + emails, + "admin@example.com", + "New organization created: pending action" + ); + + // Waiting to be in next step URL + await page.waitForURL("/settings/organizations/*/set-password"); + }); + }); + + await test.step("Admin password", async () => { + // Check required fields + await page.locator("button[type=submit]").click(); + await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints + + // Happy path + await page.locator("input[name='password']").fill("ADMIN_user2023$"); + await page.locator("button[type=submit]").click(); + + // Waiting to be in next step URL + await page.waitForURL("/settings/organizations/*/about"); + }); + + await test.step("About the organization", async () => { + // Choosing an avatar + await page.locator('button:text("Upload")').click(); + const fileChooserPromise = page.waitForEvent("filechooser"); + await page.getByText("Choose a file...").click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png")); + await page.locator('button:text("Save")').click(); + + // About text + await page.locator('textarea[name="about"]').fill("This is a testing org"); + await page.locator("button[type=submit]").click(); + + // Waiting to be in next step URL + await page.waitForURL("/settings/organizations/*/onboard-admins"); + }); + + await test.step("On-board administrators", async () => { + // Required field + await page.locator("button[type=submit]").click(); + + // Happy path + await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`); + await page.locator("button[type=submit]").click(); + + // Check if invited admin received the invitation email + await expectInvitationEmailToBeReceived( + page, + emails, + `rick@${orgDomain}.com`, + `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com` + ); + + // Waiting to be in next step URL + await page.waitForURL("/settings/organizations/*/add-teams"); + }); + + await test.step("Create teams", async () => { + // Initial state + await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1); + await expect(page.locator('button:text("Continue")')).toBeDisabled(); + + // Filling one team + await page.locator('input[name="teams.0.name"]').fill("Marketing"); + await expect(page.locator('button:text("Continue")')).toBeEnabled(); + + // Adding another team + await page.locator('button:text("Add a team")').click(); + await expect(page.locator('button:text("Continue")')).toBeDisabled(); + await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1); + await page.locator('input[name="teams.1.name"]').fill("Sales"); + await expect(page.locator('button:text("Continue")')).toBeEnabled(); + + // Finishing the creation wizard + await page.locator('button:text("Continue")').click(); + await page.waitForURL("/event-types"); + }); + }); +}); diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts new file mode 100644 index 0000000000..6561a01e55 --- /dev/null +++ b/apps/web/playwright/organization/organization-invitation.e2e.ts @@ -0,0 +1,119 @@ +import { expect } from "@playwright/test"; + +import { test } from "../lib/fixtures"; +import { expectInvitationEmailToBeReceived } from "./expects"; + +test.describe.configure({ mode: "parallel" }); + +test.afterEach(async ({ users, emails, clipboard }) => { + clipboard.reset(); + await users.deleteAll(); + emails?.deleteAll(); +}); + +test.describe("Organization", () => { + test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => { + const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true }); + const { team: org } = await orgOwner.getOrg(); + await orgOwner.apiLogin(); + await page.goto("/settings/organizations/members"); + await page.waitForLoadState("networkidle"); + + await test.step("To the organization by email (external user)", async () => { + const invitedUserEmail = `rick@domain-${Date.now()}.com`; + await page.locator('button:text("Add")').click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator('button:text("Send invite")').click(); + await page.waitForLoadState("networkidle"); + const inviteLink = await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`, + "signup?token" + ); + + // Check newly invited member exists and is pending + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(1); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!inviteLink) return null; + + // Follow invite link in new window + const context = await browser.newContext(); + const newPage = await context.newPage(); + newPage.goto(inviteLink); + await newPage.waitForLoadState("networkidle"); + + // Check required fields + await newPage.locator("button[type=submit]").click(); + await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints + await newPage.locator("input[name=password]").fill(`P4ssw0rd!`); + await newPage.locator("button[type=submit]").click(); + await newPage.waitForURL("/getting-started?from=signup"); + await context.close(); + await newPage.close(); + + // Check newly invited member is not pending anymore + await page.bringToFront(); + await page.goto("/settings/organizations/members"); + page.locator(`[data-testid="login-form"]`); + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(0); + }); + + await test.step("To the organization by invite link", async () => { + // Get the invite link + await page.locator('button:text("Add")').click(); + await page.locator(`[data-testid="copy-invite-link-button"]`).click(); + const inviteLink = await clipboard.get(); + await page.waitForLoadState("networkidle"); + + // Follow invite link in new window + const context = await browser.newContext(); + const inviteLinkPage = await context.newPage(); + await inviteLinkPage.goto(inviteLink); + await inviteLinkPage.waitForLoadState("networkidle"); + + // Check required fields + await inviteLinkPage.locator("button[type=submit]").click(); + await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints + + // Happy path + await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`); + await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`); + await inviteLinkPage.locator("button[type=submit]").click(); + await inviteLinkPage.waitForURL("/getting-started"); + }); + }); + + test("Invitation (verified)", async ({ browser, page, users, emails }) => { + const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true }); + const { team: org } = await orgOwner.getOrg(); + await orgOwner.apiLogin(); + await page.goto("/settings/organizations/members"); + await page.waitForLoadState("networkidle"); + + await test.step("To the organization by email (internal user)", async () => { + const invitedUserEmail = `rick@example.com`; + await page.locator('button:text("Add")').click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator('button:text("Send invite")').click(); + await page.waitForLoadState("networkidle"); + await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${org.name}'s admin invited you to join the organization ${org.name} on Cal.com` + ); + + // Check newly invited member exists and is pending + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(0); + }); + }); +}); diff --git a/package.json b/package.json index 4f74854f79..586870f28e 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@playwright/test": "^1.31.2", "@snaplet/copycat": "^0.3.0", "@testing-library/jest-dom": "^5.16.5", + "@types/jsdom": "^21.1.3", "@types/jsonwebtoken": "^9.0.3", "c8": "^7.13.0", "checkly": "latest", diff --git a/packages/features/ee/organizations/pages/settings/members.tsx b/packages/features/ee/organizations/pages/settings/members.tsx index 36ba6b64c5..9f40c42a57 100644 --- a/packages/features/ee/organizations/pages/settings/members.tsx +++ b/packages/features/ee/organizations/pages/settings/members.tsx @@ -11,24 +11,6 @@ const MembersView = () => {
- {/* {team && ( - <> - {isInviteOpen && ( - - )} - - )} */}
diff --git a/packages/features/ee/teams/components/MemberInvitationModal.tsx b/packages/features/ee/teams/components/MemberInvitationModal.tsx index 4aeffb573c..fc8fcf90fc 100644 --- a/packages/features/ee/teams/components/MemberInvitationModal.tsx +++ b/packages/features/ee/teams/components/MemberInvitationModal.tsx @@ -25,6 +25,7 @@ import { TextAreaField, } from "@calcom/ui"; import { Link } from "@calcom/ui/components/icon"; +import type { Window as WindowWithClipboardValue } from "@calcom/web/playwright/fixtures/clipboard"; import type { PendingMember } from "../lib/types"; import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton"; @@ -92,8 +93,15 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) const inviteLink = isOrgInvite || (props?.orgMembers && props.orgMembers?.length > 0) ? orgInviteLink : teamInviteLink; - await navigator.clipboard.writeText(inviteLink); - showToast(t("invite_link_copied"), "success"); + try { + await navigator.clipboard.writeText(inviteLink); + showToast(t("invite_link_copied"), "success"); + } catch (e) { + if (process.env.NEXT_PUBLIC_IS_E2E) { + (window as WindowWithClipboardValue).E2E_CLIPBOARD_VALUE = inviteLink; + } + console.error(e); + } }; const options: MembershipRoleOption[] = useMemo(() => { diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index 21abc1bc9a..a2b9ecb3d6 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -204,12 +204,15 @@ export function UserListTable() { id: "teams", header: "Teams", cell: ({ row }) => { - const { teams, accepted } = row.original; + const { teams, accepted, email } = row.original; // TODO: Implement click to filter return (
{accepted ? null : ( - + Pending )} diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.ts index 1e0f9a2e03..1b5509afb5 100644 --- a/packages/trpc/server/routers/viewer/organizations/create.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.ts @@ -7,12 +7,7 @@ import { sendAdminOrganizationNotification } from "@calcom/emails"; import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; -import { - IS_TEAM_BILLING_ENABLED, - RESERVED_SUBDOMAINS, - IS_PRODUCTION, - WEBAPP_URL, -} from "@calcom/lib/constants"; +import { IS_TEAM_BILLING_ENABLED, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants"; import { getTranslation } from "@calcom/lib/server/i18n"; import slugify from "@calcom/lib/slugify"; import { prisma } from "@calcom/prisma"; @@ -175,7 +170,6 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { return { user: { ...createOwnerOrg, password } }; } else { - if (!IS_PRODUCTION) return { checked: true }; const language = await getTranslation(input.language ?? "en", "common"); const secret = createHash("md5") diff --git a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts index 885bb3b6ac..17bfa84be5 100644 --- a/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/verifyCode.handler.ts @@ -2,6 +2,7 @@ import { createHash } from "crypto"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_PRODUCTION } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; import { totpRawCheck } from "@calcom/lib/totp"; import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -21,7 +22,10 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => { if (!user || !email || !code) throw new TRPCError({ code: "BAD_REQUEST" }); - if (!IS_PRODUCTION) return true; + if (!IS_PRODUCTION || process.env.NEXT_PUBLIC_IS_E2E) { + logger.warn(`Skipping code verification in dev/E2E environment`); + return true; + } await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: email, diff --git a/yarn.lock b/yarn.lock index 4e231a4e0d..01cf0964ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13331,6 +13331,17 @@ __metadata: languageName: node linkType: hard +"@types/jsdom@npm:^21.1.3": + version: 21.1.4 + resolution: "@types/jsdom@npm:21.1.4" + dependencies: + "@types/node": "*" + "@types/tough-cookie": "*" + parse5: ^7.0.0 + checksum: 915f619111dadd8d1bb7f12b6736c9d2e486911e1aed086de5fb003e7e40ae1e368da322dc04f2122ef47faf40ca75b9315ae2df3e8011f882dcf84660fb0d68 + languageName: node + linkType: hard + "@types/jsforce@npm:^1.11.0": version: 1.11.0 resolution: "@types/jsforce@npm:1.11.0" @@ -17197,6 +17208,7 @@ __metadata: "@playwright/test": ^1.31.2 "@snaplet/copycat": ^0.3.0 "@testing-library/jest-dom": ^5.16.5 + "@types/jsdom": ^21.1.3 "@types/jsonwebtoken": ^9.0.3 c8: ^7.13.0 checkly: latest From 16c5b070b61343b1b83664eda3ab84abc83be1ff Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:46:03 +0400 Subject: [PATCH 097/119] fix: Admin Logic for event-type API endpoint (#12482) * Fix Admin logic * chore: fix prettier --------- Co-authored-by: Udit Takkar --- apps/api/pages/api/event-types/_post.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/api/pages/api/event-types/_post.ts b/apps/api/pages/api/event-types/_post.ts index 1531485e7b..6eeb59f5c9 100644 --- a/apps/api/pages/api/event-types/_post.ts +++ b/apps/api/pages/api/event-types/_post.ts @@ -316,8 +316,13 @@ async function checkPermissions(req: NextApiRequest) { statusCode: 401, message: "ADMIN required for `userId`", }); + if (!isAdmin && body.teamId) + throw new HttpError({ + statusCode: 401, + message: "ADMIN required for `teamId`", + }); /* Admin users are required to pass in a userId or teamId */ - if (isAdmin && (!body.userId || !body.teamId)) + if (isAdmin && !body.userId && !body.teamId) throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" }); } From 4b060fc2cd12fde1670e7f2db8880c4eaf5c4af3 Mon Sep 17 00:00:00 2001 From: Somay Chauhan Date: Wed, 22 Nov 2023 15:19:37 +0530 Subject: [PATCH 098/119] fix: opening team invite link in email throws error on signup page (#12397) Co-authored-by: Keith Williams --- .../viewer/teams/inviteMember/inviteMember.handler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index 79d560050a..5eaebe0844 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -119,6 +119,11 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) = identifier: usernameOrEmail, token, expires: new Date(new Date().setHours(168)), // +1 week + team: { + connect: { + id: team.id, + }, + }, }, }); From 828092c1d0e773e9e882c64ead31d88e4b3b7b73 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Wed, 22 Nov 2023 10:18:09 +0000 Subject: [PATCH 099/119] chore: fix cal.ai price (#12485) --- packages/app-store/cal-ai/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/cal-ai/config.json b/packages/app-store/cal-ai/config.json index e6718b7b5d..6ec6551057 100644 --- a/packages/app-store/cal-ai/config.json +++ b/packages/app-store/cal-ai/config.json @@ -15,7 +15,7 @@ "__template": "basic", "dirName": "cal-ai", "paid": { - "priceInUsd": 25, + "priceInUsd": 8, "priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP", "mode": "subscription" } From 113195224aff7a9bc9237b22fcacda31c95ed0a9 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Wed, 22 Nov 2023 10:23:54 +0000 Subject: [PATCH 100/119] chore: fixed cal.ai thumbnail (#12486) --- packages/features/tips/Tips.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/tips/Tips.tsx b/packages/features/tips/Tips.tsx index e0e3a14fac..3ac41350c3 100644 --- a/packages/features/tips/Tips.tsx +++ b/packages/features/tips/Tips.tsx @@ -96,7 +96,7 @@ export const tips = [ { id: 12, thumbnailUrl: - "https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2", + "https://cal.com/og-image-cal-ai.jpg", mediaLink: "https://go.cal.com/cal-ai", title: "Cal.ai", description: "Your personal AI scheduling assistant", From af2c6c08441336a0f25ce68194482a8bab8462d0 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Wed, 22 Nov 2023 10:25:56 +0000 Subject: [PATCH 101/119] chore: ignore "platform" in pr-assign-team workflow (#12487) --- .github/workflows/pr-assign-team-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-assign-team-label.yml b/.github/workflows/pr-assign-team-label.yml index f6c02d1fd5..ecb601f75c 100644 --- a/.github/workflows/pr-assign-team-label.yml +++ b/.github/workflows/pr-assign-team-label.yml @@ -13,4 +13,4 @@ jobs: with: repo-token: ${{ secrets.GH_ACCESS_TOKEN }} organization-name: calcom - ignore-labels: "app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier" + ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier" From a3b5263b766fc66bc4e8af371dbbaa55ec047912 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:12:19 +0530 Subject: [PATCH 102/119] chore: reset form on submission (#12465) --- .../features/eventtypes/components/CreateEventTypeDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx index 6fd6483d65..becaf6bcef 100644 --- a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx @@ -125,6 +125,7 @@ export default function CreateEventTypeDialog({ }), "success" ); + form.reset(); }, onError: (err) => { if (err instanceof HttpError) { From 73aa1e8a22c545ee857c4bb33240f618ddc3f4d4 Mon Sep 17 00:00:00 2001 From: Adugna Tadesse Date: Wed, 22 Nov 2023 14:01:29 +0300 Subject: [PATCH 103/119] outlook second account fix (#12013) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- packages/app-store/office365calendar/api/add.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app-store/office365calendar/api/add.ts b/packages/app-store/office365calendar/api/add.ts index 60e06d18b1..e087eab78a 100644 --- a/packages/app-store/office365calendar/api/add.ts +++ b/packages/app-store/office365calendar/api/add.ts @@ -20,6 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) response_type: "code", scope: scopes.join(" "), client_id, + prompt: "select_account", redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`, state, }; From cb7ddc455ad7334228cabb3f03552db22fb378f0 Mon Sep 17 00:00:00 2001 From: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:56:43 +0530 Subject: [PATCH 104/119] chore: Add team invite tests (#12425) Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen --- apps/web/playwright/team/expects.ts | 29 ++++ .../playwright/team/team-invitation.e2e.ts | 124 ++++++++++++++++++ .../ee/teams/components/MemberListItem.tsx | 9 +- .../components/form/inputs/HintOrErrors.tsx | 9 +- 4 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 apps/web/playwright/team/expects.ts create mode 100644 apps/web/playwright/team/team-invitation.e2e.ts diff --git a/apps/web/playwright/team/expects.ts b/apps/web/playwright/team/expects.ts new file mode 100644 index 0000000000..43e02063f6 --- /dev/null +++ b/apps/web/playwright/team/expects.ts @@ -0,0 +1,29 @@ +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; +import { JSDOM } from "jsdom"; +import type { API, Messages } from "mailhog"; + +import { getEmailsReceivedByUser } from "../lib/testUtils"; + +export async function expectInvitationEmailToBeReceived( + page: Page, + emails: API | undefined, + userEmail: string, + subject: string, + returnLink?: string +) { + if (!emails) return null; + + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(10000); + const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); + expect(receivedEmails?.total).toBe(1); + + const [firstReceivedEmail] = (receivedEmails as Messages).items; + + expect(firstReceivedEmail.subject).toBe(subject); + if (!returnLink) return; + const dom = new JSDOM(firstReceivedEmail.html); + const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`); + return anchor?.getAttribute("href"); +} diff --git a/apps/web/playwright/team/team-invitation.e2e.ts b/apps/web/playwright/team/team-invitation.e2e.ts new file mode 100644 index 0000000000..95505bf279 --- /dev/null +++ b/apps/web/playwright/team/team-invitation.e2e.ts @@ -0,0 +1,124 @@ +import { expect } from "@playwright/test"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; + +import { test } from "../lib/fixtures"; +import { localize } from "../lib/testUtils"; +import { expectInvitationEmailToBeReceived } from "./expects"; + +test.describe.configure({ mode: "parallel" }); + +test.afterEach(async ({ users, emails, clipboard }) => { + clipboard.reset(); + await users.deleteAll(); + emails?.deleteAll(); +}); + +test.describe("Team", () => { + test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => { + const t = await localize("en"); + const teamOwner = await users.create(undefined, { hasTeam: true }); + const { team } = await teamOwner.getFirstTeam(); + await teamOwner.apiLogin(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + + await test.step("To the team by email (external user)", async () => { + const invitedUserEmail = `rick_${Date.now()}@domain-${Date.now()}.com`; + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator(`button:text("${t("send_invite")}")`).click(); + await page.waitForLoadState("networkidle"); + const inviteLink = await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${team.name}'s admin invited you to join the team ${team.name} on Cal.com`, + "signup?token" + ); + + //Check newly invited member exists and is pending + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(1); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!inviteLink) return null; + + // Follow invite link to new window + const context = await browser.newContext(); + const newPage = await context.newPage(); + await newPage.goto(inviteLink); + await newPage.waitForLoadState("networkidle"); + + // Check required fields + await newPage.locator("button[type=submit]").click(); + await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3); + await newPage.locator("input[name=password]").fill(`P4ssw0rd!`); + await newPage.locator("button[type=submit]").click(); + await newPage.waitForURL("/getting-started?from=signup"); + await newPage.close(); + await context.close(); + + // Check newly invited member is not pending anymore + await page.bringToFront(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(0); + }); + + await test.step("To the team by invite link", async () => { + const user = await users.create({ + email: `user-invite-${Date.now()}@domain.com`, + password: "P4ssw0rd!", + }); + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator(`[data-testid="copy-invite-link-button"]`).click(); + const inviteLink = await clipboard.get(); + await page.waitForLoadState("networkidle"); + + const context = await browser.newContext(); + const inviteLinkPage = await context.newPage(); + await inviteLinkPage.goto(inviteLink); + await inviteLinkPage.waitForLoadState("domcontentloaded"); + + await inviteLinkPage.locator("button[type=submit]").click(); + await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2); + + await inviteLinkPage.locator("input[name=email]").fill(user.email); + await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!"); + await inviteLinkPage.locator("button[type=submit]").click(); + + await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`); + }); + }); + + test("Invitation (verified)", async ({ browser, page, users, emails }) => { + const t = await localize("en"); + const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true }); + const { team } = await teamOwner.getFirstTeam(); + await teamOwner.apiLogin(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + + await test.step("To the organization by email (internal user)", async () => { + const invitedUserEmail = `rick@example.com`; + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator(`button:text("${t("send_invite")}")`).click(); + await page.waitForLoadState("networkidle"); + await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${teamOwner.name} invited you to join the team ${team.name} on Cal.com` + ); + + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(1); + }); + }); +}); diff --git a/packages/features/ee/teams/components/MemberListItem.tsx b/packages/features/ee/teams/components/MemberListItem.tsx index 2b356747ca..2f2bacfa32 100644 --- a/packages/features/ee/teams/components/MemberListItem.tsx +++ b/packages/features/ee/teams/components/MemberListItem.tsx @@ -152,7 +152,14 @@ export default function MemberListItem(props: Props) { {props.member.role && }
- + {props.member.email} {bookingLink && ( diff --git a/packages/ui/components/form/inputs/HintOrErrors.tsx b/packages/ui/components/form/inputs/HintOrErrors.tsx index a2115f7c56..adc3ce6fca 100644 --- a/packages/ui/components/form/inputs/HintOrErrors.tsx +++ b/packages/ui/components/form/inputs/HintOrErrors.tsx @@ -50,7 +50,10 @@ export function HintsOrErrors({ return (
  • + data-testid="hint-error" + className={ + error !== undefined ? (submitted ? "bg-yellow-200 text-red-700" : "") : "text-green-600" + }> {error !== undefined ? ( submitted ? ( @@ -72,7 +75,9 @@ export function HintsOrErrors({ // errors exist, not custom ones, just show them as is if (fieldErrors) { return ( -
    +
    From d04226ab9aef6ed58f5b39923e383e7fcfcb3e2d Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:39:00 +0200 Subject: [PATCH 105/119] fix: alby payment isPaid always false on create (#12463) --- packages/app-store/alby/lib/PaymentService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/alby/lib/PaymentService.ts b/packages/app-store/alby/lib/PaymentService.ts index 71e9c3e851..c29b08427a 100644 --- a/packages/app-store/alby/lib/PaymentService.ts +++ b/packages/app-store/alby/lib/PaymentService.ts @@ -74,7 +74,7 @@ export class PaymentService implements IAbstractPaymentService { currency: payment.currency, data: Object.assign( {}, - { invoice: { ...invoice, isPaid: await invoice.isPaid() } } + { invoice: { ...invoice, isPaid: false } } ) as unknown as Prisma.InputJsonValue, fee: 0, refunded: false, From 2853288f497bd9710b732b6a8280c0c360eb8561 Mon Sep 17 00:00:00 2001 From: sebzz Date: Wed, 22 Nov 2023 17:19:27 +0530 Subject: [PATCH 106/119] docs: add google credentials in example env (#11695) * docs:add google credentials in example env * docs: add a space after # * chore: update .env.example --------- Co-authored-by: Udit Takkar --- .env.example | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.env.example b/.env.example index 46237514b5..dfa0a49d66 100644 --- a/.env.example +++ b/.env.example @@ -107,6 +107,19 @@ NEXT_PUBLIC_HELPSCOUT_KEY= NEXT_PUBLIC_FRESHCHAT_TOKEN= NEXT_PUBLIC_FRESHCHAT_HOST= +# Google OAuth credentials +# To enable Login with Google you need to: +# 1. Set `GOOGLE_API_CREDENTIALS` above +# 2. Set `GOOGLE_LOGIN_ENABLED` to `true` +# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance +# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications +GOOGLE_LOGIN_ENABLED=false + +# - GOOGLE CALENDAR/MEET/LOGIN +# Needed to enable Google Calendar integration and Login with Google +# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials +GOOGLE_API_CREDENTIALS= + # Inbox to send user feedback SEND_FEEDBACK_EMAIL= From 9a6683e01dace9cbe426ec35084e38201555085d Mon Sep 17 00:00:00 2001 From: Matt Nicolls <2540582+nicolls1@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:04:51 +0100 Subject: [PATCH 107/119] fix: include eventTypeId in BOOKING_CANCELLED event (#12445) --- packages/features/bookings/lib/handleCancelBooking.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 217972f873..7148be943f 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -227,6 +227,7 @@ async function handler(req: CustomRequest) { type: bookingToDelete?.eventType?.slug as string, description: bookingToDelete?.description || "", customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs), + eventTypeId: bookingToDelete.eventTypeId as number, ...getCalEventResponses({ bookingFields: bookingToDelete.eventType?.bookingFields ?? null, booking: bookingToDelete, From 2498785c49b44e13862f75346c245701792200f3 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 22 Nov 2023 08:22:03 -0500 Subject: [PATCH 108/119] chore: Clean Up Delete Credential Selected Calendar Error Message (#12353) --- .../deleteCredential.handler.ts | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts index fb716afb84..e396d5e0d4 100644 --- a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts @@ -1,4 +1,3 @@ -import { Prisma } from "@prisma/client"; import z from "zod"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; @@ -328,52 +327,37 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp } } + // Backwards compatibility. Selected calendars cascade on delete when deleting a credential + // If it's a calendar remove it from the SelectedCalendars + if (credential.app?.categories.includes(AppCategories.calendar)) { + try { + const calendar = await getCalendar(credential); + + const calendars = await calendar?.listCalendars(); + + const calendarIds = calendars?.map((cal) => cal.externalId); + + await prisma.selectedCalendar.deleteMany({ + where: { + userId: user.id, + integration: credential.type as string, + externalId: { + in: calendarIds, + }, + }, + }); + } catch (error) { + console.warn( + `Error deleting selected calendars for userId: ${user.id} integration: ${credential.type}`, + error + ); + } + } + // Validated that credential is user's above await prisma.credential.delete({ where: { id: id, }, }); - - // Backwards compatibility. Selected calendars cascade on delete when deleting a credential - // If it's a calendar remove it from the SelectedCalendars - if (credential.app?.categories.includes(AppCategories.calendar)) { - const selectedCalendars = await prisma.selectedCalendar.findMany({ - where: { - userId: user.id, - integration: credential.type as string, - }, - }); - - if (selectedCalendars.length) { - const calendar = await getCalendar(credential); - - const calendars = await calendar?.listCalendars(); - - if (calendars && calendars.length > 0) { - calendars.map(async (cal) => { - prisma.selectedCalendar - .delete({ - where: { - userId_integration_externalId: { - userId: user.id, - externalId: cal.externalId, - integration: cal.integration as string, - }, - }, - }) - .catch((error) => { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { - console.log( - `Error deleting selected calendars for user ${user.id} and calendar ${credential.appId}. Could not find selected calendar.` - ); - } - console.log( - `Error deleting selected calendars for user ${user.id} and calendar ${credential.appId} with error: ${error}` - ); - }); - }); - } - } - } }; From b762f602144a429c9580b4e3325ce7730161efef Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:15:47 -0500 Subject: [PATCH 109/119] test: Integration Test GCal Primary Calendar (#12011) Co-authored-by: Alex van Andel --- .env.example | 6 + apps/web/playwright/fixtures/users.ts | 12 +- .../lib/CalendarService.test.ts | 20 +- .../googlecalendar/lib/CalendarService.ts | 2 +- .../tests/google-calendar.e2e.ts | 215 ++++++++++++++++++ .../googlecalendar/tests/testUtils.ts | 127 +++++++++++ packages/prisma/seed.ts | 16 ++ turbo.json | 4 + 8 files changed, 389 insertions(+), 13 deletions(-) create mode 100644 packages/app-store/googlecalendar/tests/google-calendar.e2e.ts create mode 100644 packages/app-store/googlecalendar/tests/testUtils.ts diff --git a/.env.example b/.env.example index dfa0a49d66..3690d058f9 100644 --- a/.env.example +++ b/.env.example @@ -250,6 +250,12 @@ AUTH_BEARER_TOKEN_VERCEL= E2E_TEST_APPLE_CALENDAR_EMAIL="" E2E_TEST_APPLE_CALENDAR_PASSWORD="" +# - CALCOM QA ACCOUNT +# Used for E2E tests on Cal.com that require 3rd party integrations +E2E_TEST_CALCOM_QA_EMAIL="qa@example.com" +# Replace with your own password +E2E_TEST_CALCOM_QA_PASSWORD="password" + # - APP CREDENTIAL SYNC *********************************************************************************** # Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations # Under settings/admin/apps ensure that all app secrets are set the same as the parent application diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index ae5fbfbec2..b0d0a48c65 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -396,6 +396,15 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn await prisma.user.delete({ where: { id } }); store.users = store.users.filter((b) => b.id !== id); }, + set: async (email: string) => { + const user = await prisma.user.findUniqueOrThrow({ + where: { email }, + include: userIncludes, + }); + const userFixture = createUserFixture(user, store.page); + store.users.push(userFixture); + return userFixture; + }, }; }; @@ -420,7 +429,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { eventTypes: user.eventTypes, routingForms: user.routingForms, self, - apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page), + apiLogin: async (password?: string) => + apiLogin({ ...(await self()), password: password || user.username }, store.page), /** * @deprecated use apiLogin instead */ diff --git a/packages/app-store/googlecalendar/lib/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/CalendarService.test.ts index 8a416ea6eb..8cf8f5b247 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.test.ts @@ -78,17 +78,15 @@ test("Calendar Cache is being called", async () => { // prismaMock.calendarCache.create.mock. const calendarService = new CalendarService(testCredential); - // @ts-expect-error authedCalendar is a private method, hence the TS error - vi.spyOn(calendarService, "authedCalendar").mockReturnValue( - // @ts-expect-error trust me bro - { - freebusy: { - query: vi.fn().mockReturnValue({ - data: testFreeBusyResponse, - }), - }, - } - ); + vi.spyOn(calendarService, "authedCalendar").mockReturnValue({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore - Mocking the authedCalendar so can't return the actual response + freebusy: { + query: vi.fn().mockReturnValue({ + data: testFreeBusyResponse, + }), + }, + }); await calendarService.getAvailability(new Date().toISOString(), new Date().toISOString(), [ testSelectedCalendar, diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index f3af3a9cff..e01982378b 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -132,7 +132,7 @@ export default class GoogleCalendarService implements Calendar { }; }; - private authedCalendar = async () => { + public authedCalendar = async () => { const myGoogleAuth = await this.auth.getToken(); const calendar = google.calendar({ version: "v3", diff --git a/packages/app-store/googlecalendar/tests/google-calendar.e2e.ts b/packages/app-store/googlecalendar/tests/google-calendar.e2e.ts new file mode 100644 index 0000000000..226b7a61cd --- /dev/null +++ b/packages/app-store/googlecalendar/tests/google-calendar.e2e.ts @@ -0,0 +1,215 @@ +import { expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; + +import dayjs from "@calcom/dayjs"; +import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; +import type { Prisma } from "@calcom/prisma/client"; +import { test } from "@calcom/web/playwright/lib/fixtures"; +import { selectSecondAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils"; + +import metadata from "../_metadata"; +import GoogleCalendarService from "../lib/CalendarService"; +import { createBookingAndFetchGCalEvent, deleteBookingAndEvent, assertValueExists } from "./testUtils"; + +test.describe("Google Calendar", async () => { + test.describe("Test using the primary calendar", async () => { + let qaUsername: string; + let qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }>; + test.beforeAll(async () => { + let runIntegrationTest = false; + + test.skip(!!APP_CREDENTIAL_SHARING_ENABLED, "Credential sharing enabled"); + + if (process.env.E2E_TEST_CALCOM_QA_EMAIL && process.env.E2E_TEST_CALCOM_QA_PASSWORD) { + qaGCalCredential = await prisma.credential.findFirstOrThrow({ + where: { + user: { + email: process.env.E2E_TEST_CALCOM_QA_EMAIL, + }, + type: metadata.type, + }, + select: { + id: true, + }, + }); + + const qaUserQuery = await prisma.user.findFirstOrThrow({ + where: { + email: process.env.E2E_TEST_CALCOM_QA_EMAIL, + }, + select: { + username: true, + }, + }); + + assertValueExists(qaUserQuery.username, "qaUsername"); + qaUsername = qaUserQuery.username; + + if (qaGCalCredential && qaUsername) runIntegrationTest = true; + } + + test.skip(!runIntegrationTest, "QA user not found"); + }); + + test.beforeEach(async ({ page, users }) => { + assertValueExists(process.env.E2E_TEST_CALCOM_QA_EMAIL, "qaEmail"); + + const qaUserStore = await users.set(process.env.E2E_TEST_CALCOM_QA_EMAIL); + + await qaUserStore.apiLogin(process.env.E2E_TEST_CALCOM_QA_PASSWORD); + + // Need to refresh keys from DB + const refreshedCredential = await prisma.credential.findFirst({ + where: { + id: qaGCalCredential?.id, + }, + include: { + user: { + select: { + email: true, + }, + }, + }, + }); + assertValueExists(refreshedCredential, "refreshedCredential"); + + const googleCalendarService = new GoogleCalendarService(refreshedCredential); + + const calendars = await googleCalendarService.listCalendars(); + + const primaryCalendarName = calendars.find((calendar) => calendar.primary)?.name; + assertValueExists(primaryCalendarName, "primaryCalendarName"); + + await page.goto("/apps/installed/calendar"); + + await page.waitForSelector('[title*="Create events on"]'); + await page.locator('[title*="Create events on"]').locator("svg").click(); + await page.locator("#react-select-2-option-0-0").getByText(primaryCalendarName).click(); + }); + + test("On new booking, event should be created on GCal", async ({ page }) => { + const { gCalEvent, gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent( + page as Page, + qaGCalCredential, + qaUsername + ); + + assertValueExists(gCalEvent.start?.timeZone, "gCalEvent"); + assertValueExists(gCalEvent.end?.timeZone, "gCalEvent"); + + // Ensure that the start and end times are matching + const startTimeMatches = dayjs(booking.startTime).isSame( + dayjs(gCalEvent.start.dateTime).tz(gCalEvent.start.timeZone) + ); + const endTimeMatches = dayjs(booking.endTime).isSame( + dayjs(gCalEvent.end?.dateTime).tz(gCalEvent.end.timeZone) + ); + expect(startTimeMatches && endTimeMatches).toBe(true); + + // Ensure that the titles are matching + expect(booking.title).toBe(gCalEvent.summary); + + // Ensure that the attendee is on the event + const bookingAttendee = booking?.attendees[0].email; + const attendeeInGCalEvent = gCalEvent.attendees?.find((attendee) => attendee.email === bookingAttendee); + expect(attendeeInGCalEvent).toBeTruthy(); + + await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid); + }); + + test("On reschedule, event should be updated on GCal", async ({ page }) => { + // Reschedule the booking and check the gCalEvent's time is also changed + // On reschedule gCal UID stays the same + const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent( + page, + qaGCalCredential, + qaUsername + ); + + await page.locator('[data-testid="reschedule-link"]').click(); + + await selectSecondAvailableTimeSlotNextMonth(page); + await page.locator('[data-testid="confirm-reschedule-button"]').click(); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + + const rescheduledBookingUrl = await page.url(); + const rescheduledBookingUid = rescheduledBookingUrl.match(/booking\/([^\/?]+)/); + + assertValueExists(rescheduledBookingUid, "rescheduledBookingUid"); + + // Get the rescheduled booking start and end times + const rescheduledBooking = await prisma.booking.findFirst({ + where: { + uid: rescheduledBookingUid[1], + }, + select: { + startTime: true, + endTime: true, + }, + }); + assertValueExists(rescheduledBooking, "rescheduledBooking"); + + // The GCal event UID persists after reschedule but should get the rescheduled data + const gCalRescheduledEventResponse = await authedCalendar.events.get({ + calendarId: "primary", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + eventId: gCalReference.uid, + }); + + expect(gCalRescheduledEventResponse.status).toBe(200); + + const rescheduledGCalEvent = gCalRescheduledEventResponse.data; + + assertValueExists(rescheduledGCalEvent.start?.timeZone, "rescheduledGCalEvent"); + assertValueExists(rescheduledGCalEvent.end?.timeZone, "rescheduledGCalEvent"); + + // Ensure that the new start and end times are matching + const rescheduledStartTimeMatches = dayjs(rescheduledBooking.startTime).isSame( + dayjs(rescheduledGCalEvent.start?.dateTime).tz(rescheduledGCalEvent.start?.timeZone) + ); + const rescheduledEndTimeMatches = dayjs(rescheduledBooking.endTime).isSame( + dayjs(rescheduledGCalEvent.end?.dateTime).tz(rescheduledGCalEvent.end.timeZone) + ); + expect(rescheduledStartTimeMatches && rescheduledEndTimeMatches).toBe(true); + + // After test passes we can delete the bookings and GCal event + await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid); + + await prisma.booking.delete({ + where: { + uid: rescheduledBookingUid[1], + }, + }); + }); + + test("When canceling the booking, the GCal event should also be deleted", async ({ page }) => { + const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent( + page, + qaGCalCredential, + qaUsername + ); + + // Cancel the booking + await page.locator('[data-testid="cancel"]').click(); + await page.locator('[data-testid="confirm_cancel"]').click(); + // Query for the bookingUID and ensure that it doesn't exist on GCal + + await page.waitForSelector('[data-testid="cancelled-headline"]'); + + const canceledGCalEventResponse = await authedCalendar.events.get({ + calendarId: "primary", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + eventId: gCalReference.uid, + }); + + expect(canceledGCalEventResponse.data.status).toBe("cancelled"); + + // GCal API sees canceled events as already deleted + await deleteBookingAndEvent(authedCalendar, booking.uid); + }); + }); +}); diff --git a/packages/app-store/googlecalendar/tests/testUtils.ts b/packages/app-store/googlecalendar/tests/testUtils.ts new file mode 100644 index 0000000000..5d4920d2b1 --- /dev/null +++ b/packages/app-store/googlecalendar/tests/testUtils.ts @@ -0,0 +1,127 @@ +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; + +import prisma from "@calcom/prisma"; +import type { Prisma } from "@calcom/prisma/client"; +import { bookFirstEvent } from "@calcom/web/playwright/lib/testUtils"; + +import metadata from "../_metadata"; +import GoogleCalendarService from "../lib/CalendarService"; + +/** + * Creates the booking on Cal.com and makes the GCal call to fetch the event. + * Ends on the booking success page + * @param page + * + * @returns the raw GCal event GET response and the booking reference + */ +export const createBookingAndFetchGCalEvent = async ( + page: Page, + qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }> | null, + qaUsername: string +) => { + await page.goto(`/${qaUsername}`); + await bookFirstEvent(page); + + const bookingUrl = await page.url(); + const bookingUid = bookingUrl.match(/booking\/([^\/?]+)/); + assertValueExists(bookingUid, "bookingUid"); + + const [gCalReference, booking] = await Promise.all([ + prisma.bookingReference.findFirst({ + where: { + booking: { + uid: bookingUid[1], + }, + type: metadata.type, + credentialId: qaGCalCredential?.id, + }, + select: { + uid: true, + booking: {}, + }, + }), + prisma.booking.findFirst({ + where: { + uid: bookingUid[1], + }, + select: { + uid: true, + startTime: true, + endTime: true, + title: true, + attendees: { + select: { + email: true, + }, + }, + user: { + select: { + email: true, + }, + }, + }, + }), + ]); + assertValueExists(gCalReference, "gCalReference"); + assertValueExists(booking, "booking"); + + // Need to refresh keys from DB + const refreshedCredential = await prisma.credential.findFirst({ + where: { + id: qaGCalCredential?.id, + }, + include: { + user: { + select: { + email: true, + }, + }, + }, + }); + + expect(refreshedCredential).toBeTruthy(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const googleCalendarService = new GoogleCalendarService(refreshedCredential); + + const authedCalendar = await googleCalendarService.authedCalendar(); + + const gCalEventResponse = await authedCalendar.events.get({ + calendarId: "primary", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + eventId: gCalReference.uid, + }); + + expect(gCalEventResponse.status).toBe(200); + + return { gCalEvent: gCalEventResponse.data, gCalReference, booking, authedCalendar }; +}; + +export const deleteBookingAndEvent = async ( + authedCalendar: any, + bookingUid: string, + gCalReferenceUid?: string +) => { + // After test passes we can delete the booking and GCal event + await prisma.booking.delete({ + where: { + uid: bookingUid, + }, + }); + + if (gCalReferenceUid) { + await authedCalendar.events.delete({ + calendarId: "primary", + eventId: gCalReferenceUid, + }); + } +}; + +export function assertValueExists(value: unknown, variableName?: string): asserts value { + if (!value) { + throw new Error(`Value is not defined: ${variableName}`); + } +} diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index 29d981a3a7..78c6861372 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -455,6 +455,22 @@ async function main() { }, }); + await createUserAndEventType({ + user: { + email: process.env.E2E_TEST_CALCOM_QA_EMAIL || "qa@example.com", + password: process.env.E2E_TEST_CALCOM_QA_PASSWORD || "qa", + username: "qa", + name: "QA Example", + }, + eventTypes: [ + { + title: "15min", + slug: "15min", + length: 15, + }, + ], + }); + await createTeamAndAddUsers( { name: "Seeded Team", diff --git a/turbo.json b/turbo.json index 1156aa13e5..0d7226a8ec 100644 --- a/turbo.json +++ b/turbo.json @@ -209,6 +209,8 @@ "CALCOM_CREDENTIAL_SYNC_ENDPOINT", "CALCOM_ENV", "CALCOM_LICENSE_KEY", + "CALCOM_QA_EMAIL", + "CALCOM_QA_PASSWORD", "CALCOM_TELEMETRY_DISABLED", "CALCOM_WEBHOOK_HEADER_NAME", "CALENDSO_ENCRYPTION_KEY", @@ -222,6 +224,8 @@ "DEBUG", "E2E_TEST_APPLE_CALENDAR_EMAIL", "E2E_TEST_APPLE_CALENDAR_PASSWORD", + "E2E_TEST_CALCOM_QA_EMAIL", + "E2E_TEST_CALCOM_QA_PASSWORD", "E2E_TEST_MAILHOG_ENABLED", "E2E_TEST_OIDC_CLIENT_ID", "E2E_TEST_OIDC_CLIENT_SECRET", From f65c7e413f4ffcad1039304a53b72998d6cb51a0 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Wed, 22 Nov 2023 23:13:25 +0530 Subject: [PATCH 110/119] fix: default organizer bug in managed event type (#11921) --- .../web/components/eventtype/EventTeamTab.tsx | 2 ++ .../playwright/fixtures/regularBookings.ts | 2 +- .../web/playwright/integrations-stripe.e2e.ts | 2 +- .../web/playwright/managed-event-types.e2e.ts | 32 +++++++++++++++++-- apps/web/playwright/payment-apps.e2e.ts | 6 ++-- .../components/EventTypeAppCardInterface.tsx | 2 ++ .../components/EventTypeAppCardInterface.tsx | 3 +- .../features/bookings/lib/handleNewBooking.ts | 5 ++- .../components/ChildrenEventTypeSelect.tsx | 1 + .../features/form-builder/FormBuilder.tsx | 1 + packages/ui/components/form/select/Select.tsx | 2 +- .../ui/components/form/select/components.tsx | 20 +++++++++++- .../ui/components/form/select/selectTheme.ts | 3 +- 13 files changed, 69 insertions(+), 12 deletions(-) diff --git a/apps/web/components/eventtype/EventTeamTab.tsx b/apps/web/components/eventtype/EventTeamTab.tsx index 49917235a3..014fde0ed1 100644 --- a/apps/web/components/eventtype/EventTeamTab.tsx +++ b/apps/web/components/eventtype/EventTeamTab.tsx @@ -76,6 +76,8 @@ const ChildrenEventTypesList = ({
    { onChange && onChange( diff --git a/apps/web/playwright/fixtures/regularBookings.ts b/apps/web/playwright/fixtures/regularBookings.ts index 72c8e44fea..b0a84078e0 100644 --- a/apps/web/playwright/fixtures/regularBookings.ts +++ b/apps/web/playwright/fixtures/regularBookings.ts @@ -204,7 +204,7 @@ export function createBookingPageFixture(page: Page) { placeholder?: string ) => { await page.getByTestId("add-field").click(); - await page.locator("#test-field-type > .bg-default > div > div:nth-child(2)").first().click(); + await page.getByTestId("test-field-type").click(); await page.getByTestId(`select-option-${questionType}`).click(); await page.getByLabel("Identifier").dblclick(); await page.getByLabel("Identifier").fill(identifier); diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index 25a1a33fa6..c9d86ccf0e 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -267,7 +267,7 @@ test.describe("Stripe integration", () => { await page.getByTestId("price-input-stripe").fill("200"); // Select currency in dropdown - await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); + await page.getByTestId("stripe-currency-select").click(); await page.locator("#react-select-2-input").fill("mexi"); await page.locator("#react-select-2-option-81").click(); diff --git a/apps/web/playwright/managed-event-types.e2e.ts b/apps/web/playwright/managed-event-types.e2e.ts index 52e6bf86c6..a0323ed8b7 100644 --- a/apps/web/playwright/managed-event-types.e2e.ts +++ b/apps/web/playwright/managed-event-types.e2e.ts @@ -1,6 +1,9 @@ import { expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; import { test } from "./lib/fixtures"; +import { selectFirstAvailableTimeSlotNextMonth, bookTimeSlot } from "./lib/testUtils"; +import { localize } from "./lib/testUtils"; test.afterEach(({ users }) => users.deleteAll()); @@ -69,15 +72,34 @@ test.describe("Managed Event Types tests", () => { await page.goto("/event-types"); await page.getByTestId("event-types").locator('a[title="managed"]').click(); await page.getByTestId("vertical-tab-assignment").click(); - await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click(); + await page.getByTestId("assignment-dropdown").click(); + await page.getByTestId(`select-option-${memberUser.id}`).click(); await page.locator('[type="submit"]').click(); await page.getByTestId("toast-success").waitFor(); + }); - await adminUser.logout(); + await test.step("Managed event type can use Organizer's default app as location", async () => { + await page.getByTestId("vertical-tab-event_setup_tab_title").click(); + + await page.locator("#location-select").click(); + const optionText = (await localize("en"))("organizer_default_conferencing_app"); + await page.locator(`text=${optionText}`).click(); + await page.locator("[data-testid=update-eventtype]").click(); + await page.getByTestId("toast-success").waitFor(); + await page.waitForLoadState("networkidle"); + + await page.getByTestId("vertical-tab-assignment").click(); + await gotoBookingPage(page); + await selectFirstAvailableTimeSlotNextMonth(page); + await bookTimeSlot(page); + + await expect(page.getByTestId("success-page")).toBeVisible(); }); await test.step("Managed event type has locked fields for added member", async () => { + await adminUser.logout(); + // Coming back as member user to see if there is a managed event present after assignment await memberUser.apiLogin(); await page.goto("/event-types"); @@ -91,3 +113,9 @@ test.describe("Managed Event Types tests", () => { }); }); }); + +async function gotoBookingPage(page: Page) { + const previewLink = await page.getByTestId("preview-button").getAttribute("href"); + + await page.goto(previewLink ?? ""); +} diff --git a/apps/web/playwright/payment-apps.e2e.ts b/apps/web/playwright/payment-apps.e2e.ts index c01bc10ba2..77bf674d92 100644 --- a/apps/web/playwright/payment-apps.e2e.ts +++ b/apps/web/playwright/payment-apps.e2e.ts @@ -77,7 +77,7 @@ test.describe("Payment app", () => { await page.goto(`event-types/${paymentEvent.id}?tabName=apps`); await page.locator("#event-type-form").getByRole("switch").click(); - await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); + await page.getByTestId("stripe-currency-select").click(); await page.getByTestId("select-option-usd").click(); await page.getByTestId("price-input-stripe").click(); @@ -123,10 +123,10 @@ test.describe("Payment app", () => { await page.getByPlaceholder("Price").click(); await page.getByPlaceholder("Price").fill("150"); - await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); + await page.getByTestId("paypal-currency-select").click(); await page.locator("#react-select-2-option-13").click(); - await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click(); + await page.getByTestId("paypal-payment-option-select").click(); await page.getByText("$MXNCurrencyMexican pesoPayment option").click(); await page.getByTestId("update-eventtype").click(); diff --git a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx index 536d159652..db6ba04755 100644 --- a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx @@ -92,6 +92,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + data-testid="stripe-payment-option-select" defaultValue={ paymentOptionSelectValue ? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 2d73a22145..a21848ce2e 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1001,8 +1001,11 @@ async function handler( const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone; const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common"); + + const isManagedEventType = !!eventType.parentId; + // use host default - if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) { + if ((isManagedEventType || isTeamEventType) && locationBodyString === OrganizerDefaultConferencingAppType) { const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined; if (organizerMetadata?.defaultConferencingApp?.appSlug) { diff --git a/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx b/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx index 328644da30..8a6218d197 100644 --- a/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx +++ b/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx @@ -106,6 +106,7 @@ export const ChildrenEventTypeSelect = ({ {children.created && children.owner.username && (
    diff --git a/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts b/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts index 7ee9f4bbb9..b4a0c0f46f 100644 --- a/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/testTrigger.handler.ts @@ -10,7 +10,7 @@ type TestTriggerOptions = { }; export const testTriggerHandler = async ({ ctx: _ctx, input }: TestTriggerOptions) => { - const { url, type, payloadTemplate = null } = input; + const { url, type, payloadTemplate = null, secret = null } = input; const translation = await getTranslation("en", "common"); const language = { locale: "en", @@ -40,8 +40,8 @@ export const testTriggerHandler = async ({ ctx: _ctx, input }: TestTriggerOption }; try { - const webhook = { subscriberUrl: url, payloadTemplate, appId: null, secret: null }; - return await sendPayload(null, type, new Date().toISOString(), webhook, data); + const webhook = { subscriberUrl: url, appId: null, payloadTemplate }; + return await sendPayload(secret, type, new Date().toISOString(), webhook, data); } catch (_err) { const error = getErrorFromUnknown(_err); return { diff --git a/packages/trpc/server/routers/viewer/webhook/testTrigger.schema.ts b/packages/trpc/server/routers/viewer/webhook/testTrigger.schema.ts index 53f92f7e88..faeef8ed25 100644 --- a/packages/trpc/server/routers/viewer/webhook/testTrigger.schema.ts +++ b/packages/trpc/server/routers/viewer/webhook/testTrigger.schema.ts @@ -4,6 +4,7 @@ import { webhookIdAndEventTypeIdSchema } from "./types"; export const ZTestTriggerInputSchema = webhookIdAndEventTypeIdSchema.extend({ url: z.string().url(), + secret: z.string().optional(), type: z.string(), payloadTemplate: z.string().optional().nullable(), }); From 2171a320f50b15e68c44348708c7a28e175ff411 Mon Sep 17 00:00:00 2001 From: zomars Date: Wed, 22 Nov 2023 12:19:16 -0700 Subject: [PATCH 114/119] fix: Locks Stripe version --- apps/web/package.json | 2 +- packages/app-store/package.json | 2 +- packages/app-store/stripepayment/package.json | 2 +- yarn.lock | 27 +++++++------------ 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 8d4fceeb46..0056cc4d2f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -125,7 +125,7 @@ "sanitize-html": "^2.10.0", "schema-dts": "^1.1.0", "short-uuid": "^4.2.0", - "stripe": "^14.3.0", + "stripe": "^9.16.0", "superjson": "1.9.1", "tailwindcss-radix": "^2.6.0", "turndown": "^7.1.1", diff --git a/packages/app-store/package.json b/packages/app-store/package.json index 6cfd20e06a..62225f2b65 100644 --- a/packages/app-store/package.json +++ b/packages/app-store/package.json @@ -26,7 +26,7 @@ "lodash": "^4.17.21", "qs-stringify": "^1.2.1", "react-i18next": "^12.2.0", - "stripe": "^14.3.0" + "stripe": "^9.16.0" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/stripepayment/package.json b/packages/app-store/stripepayment/package.json index dcf922cb2a..95c3e878e3 100644 --- a/packages/app-store/stripepayment/package.json +++ b/packages/app-store/stripepayment/package.json @@ -19,7 +19,7 @@ "@calcom/types": "*", "@stripe/react-stripe-js": "^1.10.0", "@stripe/stripe-js": "^1.35.0", - "stripe": "^14.3.0", + "stripe": "^9.16.0", "uuid": "^8.3.2", "zod": "^3.22.2" }, diff --git a/yarn.lock b/yarn.lock index d1f6feba02..1bdac79184 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3484,7 +3484,7 @@ __metadata: lodash: ^4.17.21 qs-stringify: ^1.2.1 react-i18next: ^12.2.0 - stripe: ^14.3.0 + stripe: ^9.16.0 languageName: unknown linkType: soft @@ -4335,7 +4335,7 @@ __metadata: "@calcom/types": "*" "@stripe/react-stripe-js": ^1.10.0 "@stripe/stripe-js": ^1.35.0 - stripe: ^14.3.0 + stripe: ^9.16.0 ts-node: ^10.9.1 uuid: ^8.3.2 zod: ^3.22.2 @@ -4633,7 +4633,7 @@ __metadata: sanitize-html: ^2.10.0 schema-dts: ^1.1.0 short-uuid: ^4.2.0 - stripe: ^14.3.0 + stripe: ^9.16.0 superjson: 1.9.1 tailwindcss: ^3.3.3 tailwindcss-animate: ^1.0.6 @@ -4755,7 +4755,7 @@ __metadata: remark: ^14.0.2 remark-html: ^14.0.1 remeda: ^1.24.1 - stripe: ^14.3.0 + stripe: ^9.16.0 tailwind-merge: ^1.13.2 tailwindcss: ^3.3.3 ts-node: ^10.9.1 @@ -33265,15 +33265,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.0": - version: 6.11.2 - resolution: "qs@npm:6.11.2" - dependencies: - side-channel: ^1.0.4 - checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b - languageName: node - linkType: hard - "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -37325,13 +37316,13 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^14.3.0": - version: 14.3.0 - resolution: "stripe@npm:14.3.0" +"stripe@npm:^9.16.0": + version: 9.16.0 + resolution: "stripe@npm:9.16.0" dependencies: "@types/node": ">=8.1.0" - qs: ^6.11.0 - checksum: 1aa0dec1fe8cd4c0d2a5378b9d3c69f7df505efdc86b8d6352e194d656129db83b9faaf189b5138fb5fd9a0b90e618dfcff854bb4773d289a0de0b65d0a94cb2 + qs: ^6.10.3 + checksum: d84eb9ef3fa0c50e1b62271bf822d3e9da22272ec7364ae8334db7277e42f657c42c10f6fa535c634c36081e17d1c8c5a1efc509b3747f84bfbe4cf2a94ade4b languageName: node linkType: hard From 36d315343c9a4654c4835234095104b2c875329e Mon Sep 17 00:00:00 2001 From: Manpreet Singh Date: Wed, 22 Nov 2023 11:54:18 -0800 Subject: [PATCH 115/119] fix: adds teamId to team events payload (#12417) --- packages/core/builders/CalendarEvent/class.ts | 2 +- .../bookings/lib/handleCancelBooking.ts | 20 ++++++++++++------- .../features/bookings/lib/handleNewBooking.ts | 2 +- packages/types/Calendar.d.ts | 1 + 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/core/builders/CalendarEvent/class.ts b/packages/core/builders/CalendarEvent/class.ts index 2b33d7223d..8a449dbfd2 100644 --- a/packages/core/builders/CalendarEvent/class.ts +++ b/packages/core/builders/CalendarEvent/class.ts @@ -17,7 +17,7 @@ class CalendarEventClass implements CalendarEvent { organizer!: Person; attendees!: Person[]; description?: string | null; - team?: { name: string; members: Person[] }; + team?: { name: string; members: Person[]; id: number }; location?: string | null; conferenceData?: ConferenceData; additionalInformation?: AdditionalInformation; diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 7148be943f..b71c9aa06d 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -72,7 +72,12 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine hideBranding: true, }, }, - teamId: true, + team: { + select: { + id: true, + name: true, + }, + }, recurringEvent: true, title: true, eventName: true, @@ -151,11 +156,10 @@ async function handler(req: CustomRequest) { const teamId = await getTeamIdFromEventType({ eventType: { - team: { id: bookingToDelete.eventType?.teamId ?? null }, + team: { id: bookingToDelete.eventType?.team?.id ?? null }, parentId: bookingToDelete?.eventType?.parentId ?? null, }, }); - const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId); const subscriberOptions = { @@ -255,7 +259,9 @@ async function handler(req: CustomRequest) { ? [bookingToDelete?.user.destinationCalendar] : [], cancellationReason: cancellationReason, - ...(teamMembers && { team: { name: "", members: teamMembers } }), + ...(teamMembers && { + team: { name: bookingToDelete?.eventType?.team?.name || "Nameless", members: teamMembers, id: teamId! }, + }), seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot, seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees, }; @@ -408,7 +414,7 @@ async function handler(req: CustomRequest) { if (bookingToDelete.location === DailyLocationType) { bookingToDelete.user.credentials.push({ ...FAKE_DAILY_CREDENTIAL, - teamId: bookingToDelete.eventType?.teamId || null, + teamId: bookingToDelete.eventType?.team?.id || null, }); } @@ -540,10 +546,10 @@ async function handler(req: CustomRequest) { let eventTypeOwnerId; if (bookingToDelete.eventType?.owner) { eventTypeOwnerId = bookingToDelete.eventType.owner.id; - } else if (bookingToDelete.eventType?.teamId) { + } else if (bookingToDelete.eventType?.team?.id) { const teamOwner = await prisma.membership.findFirst({ where: { - teamId: bookingToDelete.eventType.teamId, + teamId: bookingToDelete.eventType?.team.id, role: MembershipRole.OWNER, }, select: { diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index a21848ce2e..dd00afc82c 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1079,7 +1079,6 @@ async function handler( }, }; }); - const teamMembers = await Promise.all(teamMemberPromises); const attendeesList = [...invitee, ...guests]; @@ -1887,6 +1886,7 @@ async function handler( evt.team = { members: teamMembers, name: eventType.team?.name || "Nameless", + id: eventType.team?.id ?? 0, }; } diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index b8e3989f71..833855250c 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -160,6 +160,7 @@ export interface CalendarEvent { team?: { name: string; members: TeamMember[]; + id: number; }; location?: string | null; conferenceCredentialId?: number; From 5dc3065a470d86379eb32831a662f267a3fc717b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 22 Nov 2023 15:42:24 -0700 Subject: [PATCH 116/119] v3.5.1 --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 0056cc4d2f..aab6e07a2e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.5.0", + "version": "3.5.1", "private": true, "scripts": { "analyze": "ANALYZE=true next build", From de479bb2da21e55247290313d95116d65a871742 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 23 Nov 2023 11:11:20 +0530 Subject: [PATCH 117/119] fix: getting-started crash and build failure (#12506) --- apps/web/lib/metadata.ts | 1 - apps/web/playwright/teams.e2e.ts | 2 +- packages/app-store/_utils/paid-apps.ts | 2 ++ .../app-store/routing-forms/playwright/tests/basic.e2e.ts | 2 +- packages/lib/hooks/useCompatSearchParams.tsx | 4 +++- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/lib/metadata.ts b/apps/web/lib/metadata.ts index db37af3443..a1adf8d165 100644 --- a/apps/web/lib/metadata.ts +++ b/apps/web/lib/metadata.ts @@ -28,7 +28,6 @@ export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({ { rel: "icon-mask", url: "/safari-pinned-tab.svg", - // @ts-expect-error TODO available in the never Next.js version color: "#000000", }, { diff --git a/apps/web/playwright/teams.e2e.ts b/apps/web/playwright/teams.e2e.ts index 0914cc4eb8..ae0c11d821 100644 --- a/apps/web/playwright/teams.e2e.ts +++ b/apps/web/playwright/teams.e2e.ts @@ -351,7 +351,7 @@ test.describe("Teams - Org", () => { await page.goto(`/team/${team.slug}/${teamEventSlug}`); - await expect(page.locator('[data-testid="404-page"]')).toBeVisible(); + await expect(page.locator("text=This page could not be found")).toBeVisible(); await doOnOrgDomain( { orgSlug: org.slug, diff --git a/packages/app-store/_utils/paid-apps.ts b/packages/app-store/_utils/paid-apps.ts index 6d39c98ff6..1217831a45 100644 --- a/packages/app-store/_utils/paid-apps.ts +++ b/packages/app-store/_utils/paid-apps.ts @@ -40,6 +40,8 @@ export const withPaidAppRedirect = async ({ ? { subscription_data: { trial_period_days: trialDays, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - trial_settings isn't available cc @erik trial_settings: { end_behavior: { missing_payment_method: "cancel" } }, }, } diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts index 7c9f2f9868..f09d5f8345 100644 --- a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts @@ -36,7 +36,7 @@ test.describe("Routing Forms", () => { await page.goto(`apps/routing-forms/route-builder/${formId}`); await disableForm(page); await gotoRoutingLink({ page, formId }); - await expect(page.locator("text=ERROR 404")).toBeVisible(); + await expect(page.locator("text=This page could not be found")).toBeVisible(); }); test("should be able to edit the form", async ({ page }) => { diff --git a/packages/lib/hooks/useCompatSearchParams.tsx b/packages/lib/hooks/useCompatSearchParams.tsx index 032ba115a1..3112bb3827 100644 --- a/packages/lib/hooks/useCompatSearchParams.tsx +++ b/packages/lib/hooks/useCompatSearchParams.tsx @@ -8,7 +8,9 @@ export const useCompatSearchParams = () => { Object.getOwnPropertyNames(params).forEach((key) => { searchParams.delete(key); - const param = params[key]; + // Though useParams is supposed to return a string/string[] as the key's value but it is found to return undefined as well. + // Maybe it happens for pages dir when using optional catch-all routes. + const param = params[key] || ""; const paramArr = typeof param === "string" ? param.split("/") : param; paramArr.forEach((p) => { From 343f8ee3031a628db38ff44fa16c8f3a76dfda02 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 23 Nov 2023 15:39:50 +0000 Subject: [PATCH 118/119] Avatar write and unset, ensure no bad behaviour (#12504) Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --- .../loggedInViewer/updateProfile.handler.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index c790a446ad..60855221eb 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -61,6 +61,8 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const userMetadata = handleUserMetadata({ ctx, input }); const data: Prisma.UserUpdateInput = { ...input, + // DO NOT OVERWRITE AVATAR. + avatar: undefined, metadata: userMetadata, }; @@ -138,14 +140,21 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) // when the email changes, the user needs to sign in again. signOutUser = true; } - // don't do anything if avatar is undefined. - if (typeof input.avatar !== "undefined") { - data.avatarUrl = input.avatar - ? await uploadAvatar({ - avatar: await resizeBase64Image(input.avatar), - userId: user.id, - }) - : null; + // if defined AND a base 64 string, upload and set the avatar URL + if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) { + const avatar = await resizeBase64Image(input.avatar); + data.avatarUrl = await uploadAvatar({ + avatar, + userId: user.id, + }); + // as this is still used in the backwards compatible endpoint, we also write it here + // to ensure no data loss. + data.avatar = avatar; + } + // Unset avatar url if avatar is empty string. + if ("" === input.avatar) { + data.avatarUrl = null; + data.avatar = null; } const updatedUser = await prisma.user.update({ From 0910f65b8729b4d3a5daa73fe4a8c514101823f4 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 23 Nov 2023 16:56:52 +0000 Subject: [PATCH 119/119] v3.5.2 --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index aab6e07a2e..245685bc06 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.5.1", + "version": "3.5.2", "private": true, "scripts": { "analyze": "ANALYZE=true next build",