From 2bb2034823b14127d41fdc498d6896b39f8089db Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 5 Feb 2026 16:44:29 +0100 Subject: [PATCH 01/18] chore: little overhaul - remove unnecessary context, use props instead - split config, make it more typed - add class and style to the widget - remove unnecessary hooks, use simple utils instead - update icons and tiles --- .../src/BarcodeGenerator.icon.dark.png | Bin 636 -> 5590 bytes .../src/BarcodeGenerator.icon.png | Bin 642 -> 5930 bytes .../src/BarcodeGenerator.tile.dark.png | Bin 2053 -> 16583 bytes .../src/BarcodeGenerator.tile.png | Bin 2063 -> 17301 bytes .../src/BarcodeGenerator.tsx | 20 +--- .../src/components/Barcode.tsx | 25 +++-- .../src/components/QRCode.tsx | 51 +++------ .../src/config/Barcode.config.ts | 101 ++++++++++-------- .../src/config/BarcodeContext.tsx | 21 ---- .../src/hooks/useDownloadBarcode.ts | 38 ------- .../src/hooks/useDownloadQRCode.ts | 42 -------- .../src/hooks/useRenderBarcode.ts | 24 ++++- .../src/utils/download-svg.ts | 58 ++++++++++ 13 files changed, 171 insertions(+), 209 deletions(-) delete mode 100644 packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx delete mode 100644 packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts delete mode 100644 packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadQRCode.ts create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/utils/download-svg.ts diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png index b64899dac6f05833dab7cbf3df3cd1694f39a85a..77810f30f81188ad0813316685b9f7e39d4b8d8f 100644 GIT binary patch literal 5590 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 literal 636 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 literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBC{d9b;hE;^%b*2hb1<+l zN-=;;U<6`2Mrks064nT^tz$3Dlfr0M`2s2LA=931RR^sX67*fIbHp)=wkO2>K-^7XY4L>rZ z-8*!KdBzV(W#bE~&R4weKB?+kar9F0oGV>xM84lS>t=7Lz@}up;po}<=ceoTo!vOU z|M!cy;%i&aMRGk~Cwd_2T4%%7luf1b)!7Tb-`w^=<$p}fwarfM3Wic&wOC!w*4)}I zGGRtX74Le6&3q22GV9;JiG5gVo${XH#qqD#{w)0~A)l+sAUQKS@u5}N?OXm8tQ`zt zFRLC-V%jq!tyh_KL)tO!=^_t~nq0d7wMJ2=``Yc99&#G=A6QcN%WC%P|NAzG#`1J9 asg(4+^XhZd6TyEdNp9pnD@ec%1#yZ4S?#yA7dUURLr*IIKubIxbZ{qUTb zkr2NmKLkNSXHK8AfFMrrPYzCAZt!R40UIgshws+uOTG{!unqYS1tq0OfQKkw3!~#u zQL8i!{12y_fvEummBa`zow*RG~2k z)PkQ%2-q82pUI!yJt77jP}8ln_RimPc5Cd$$w01n8<8Nc7X?GPwp?#HI_Y!Lx@RB2 zL|m78zpq2_{vgA*di9yJY3!=Dqv_yv{*U~XT5vT#oSB*V^!f8=qs$KcIqMz^ifE62 zXrioeSIKGyb$Vv-gSU%B$|W5_aqjADCSCLN+%+tGuqKzb5l#=2&1%>f=qg|GrT(*U zN5{mx#wVp(FI~O)>5_ONCkjeko&QxkO>_PIh+ZbwzLlnM^FV(_zkm2j@W+oIr`MT! z#)roA^peXf6mnLV2F98a=86gmq`7M*Z$?`tZl|6cvgd^+3&>=0c4lU#s>QBq*>)&BJ2J!OJ3Bf$Tou{0 z2Oan_8k4K_IE0%ReV0i4bpdyOba;Q_=z6k1uj;)s)w!&=xVV;~u{%l8#nY(%lF@&K zvy$KbMD=&ciC;gJJimVIi_qNPx8lOgeRDSyQ`&aud|el)dIAy6b`ahybLc3FiKQEU zN;*OSN9K0^jlw`zetwj`S%ZN+HY2=q{XiAk!5G3(6Q7<*5b|*;QKZT%6w_DB9^SY@ zqkhM5RKBL)*fr0MhIIDs{W-_u_bp&BRQtj4?4|N2pQk1#U&%#WpG^8tR#EXi=6H{P zVa1-*3yYdt0@cBJKJ~o4fs%=jdi2!DHHkq`eeZN*D(S6H=f|U+qBx=Xa8^%G4~tlmnwn~U0)kAU+jg*PXJ=<;L zQlS5jZy=BK{eS)5|LKKn+yC0x|7Q>Xuh~Ozjp@N;^YZd?9u(Y*A@VI8t&JfWW@hK- zqn(mCq1bMJ^6Uu-V$NC$DnsZXVpRW^uN+ZnYB{)8(aN5Bzk`^cFBxS2wY%G!ziT*& zw<;Py>KSdj?D#;jfn3)7w?5K zEbH@D!f-s<=rsdT4}bqB9?}Gc%B=ENja0i{trg^T z>X$+gI|n!RFt12sr;M$>+hj_JDRVT=y!ja?0`n82Qf#Y}eVEAyebyn)&c80-3TB)A z{XsJ4Nl_bzq?+(_R9eZ2i4O{Bw_Q+1oW&OJ1AQqSTk_Th6@zBrnCzuhvy>5L%iXh* zw95-=omt|l4x?=^!in*|rkn$j5V5r~XlcCi1yt6jCX-1uX=LUttZ%5OqZSpZ@4T@n zd~YNNThrRfWC;=s7!19iE#;Gr&Z_D*0pVl?1F}Y7eKFO>5boWG zrJi)!3V^@;Jg_xGH!p5l94-;$O5sqO2H_mE>AZ>JkY~_;n*M*=jf;Yb+o;;p*^l<^ z3(uDfmGD2v)gD9+rr)$)U6Etch7Bl>*HfHx@{bgl4gdUb>n$8OvZw|P+yzQ{C+NiO z5auO+Ci4R0wo?Byl<$>ivw@lUBddhH_yXlwC=)xlJpD)@y>4M?dH!f^ zLDoo-S;9X@X6xo^LaNhMNEk$iukk^tD<5m53Bk0r^}*K?{SVb7IecG7huQZeEAg&v zG$ao$bL5P!62jP*$lo?>A5Y00cg05p&(~@$th>tc@)6ayLhYAoHWuq<^7X&-YhOJV zEzOxiuilPU6I%CG$Y(eJwJ)3sW_30$)c|mC1K<$1_@FtkTTYQALR1Read;!;r9h{q zw_nv8HwEa{cHpe-E3PxyeK-KEXd{e@7T4mCjK#W%!hUDOmKTe|CL=iKF9QskI6ikf zF`DnB-r8Ez95-s)1yy=A?_{FR@cS3%Daih2 z&NOOX5)5!{CpT(db(vCWqj3E^>Qq3Fb%-QXyc2-el*3#m6j)arUE?U67^{;H9LfA> zc0;m1777Dr;F)(|cPNE0rA+5g$nJEBj6TM$mV;_=5H8nh`IQ`0EV*OmicfyWX$Ye9 zf$dT9FAk7^il?k=!M!8_%sXpM9pt9GHr!WBN3I8dP@p!A~s7&tK{PefUx)i`bgK!m$aYUoqMY{XSv| zW7LzvsmC4}3r$z|vwk-Oz&{P>O{$&%TgNPRBYdMKJ8}(Q7@mUBrVRH+h95;9*YoXPgDO9eK5dB)E0r-&k$H6#j|5s z$B2tn2iZo{$9^4qz-0AnNfMBoJd{!D(`nz6NqXfXf!pHKg{{MOE!IBB2grI+#++So zYk>pAI|X1LevDx42lCAfshx@~;n28^wTzYjyM;B3xEO5G)ARyr$Q)#(yfk%)e0ivH zg^IPmT(XUSUMrt?)H}4)p)znWr2Vza(a^(e1!C*#sg830Zk{9-tE#`hzlQ4HAH!!{ z9CQ=>uxpsybam@)+UwP6qEZv}pM3oR_@y})<)DTWbej@0ONn~ z@qb5a;dIE!%)`qIvwiz^{ru_5^lE$IjMC1a5D>rLArBY?vuBIX5hz_* zS&5I1j;=nfoj;9wcICsHH@9Z$Nm8{qNR6uoUR)Qa$yA878!GX>+HOdd-Mnx+CQ-P3 zW5e+GQa8r~?3YK7c)2F~Gxe~X$*REI&Od}=M_f95Y>^J7c`13Sh{ba zoIUR~*O6(&B1ST2o5agcd@&J!e1bTg9Bcu3DL@&%onHnkpAZxs()ix&XeSP`#?q+( z-T5Y?MA3G^3wUw%UFGGPspnvD|vOYhNVB2bHz@d;;WF~sf&&HncUnpAHyM_t0 z9uO{*Q&aMvV-aUBGXi}VZNt$^CqT^33lZgf?VYaV2X|v8-;T^}pymbsTkldWsLRga zD$e(A!X-dXVq`aYNP;>`D{Gt8f8p`Rj~`*d-!Kp!C#zR@g)X0QBVVFEB98TEC6?Ab6DY{^X!J2! zanAWJC2PIl`Awti0N!lt|Ia~%mf!U4AH-TU{q_k~W5VXB-V%3mz7)0H-L zlb0p!Ww^`E>FCetvuo>a=)fF^k?aCoPC3x^B9a@2AP1x{xge)H2QQl^gxU6F3K>Ak z-V)ZKHJ7DXy=8=`y& zY2XYV??sUDKMv%-?(}vJ2-5NN^(`eV4A+5}j^S^w3ZlC(b`XQDJwnLBhBd>@oyiWW z}tmHzv#iB(R@^98zMppcJmYI z6KK|$O?(*pQL#Uxt;zsyz#a*0Va`oVtS z<-{2yloS5BZW4jknLP$jWz6byO?p^a3jYk7Hb{8)0qb%IE7Y{EnzttRSk93>uOWS! zFfPp-5?3u;V?zp>f-g+pcjvAV52Yi0SN#sn28?=UR@gfBJC9S@;AuZQ-^sXE1pnESPN_R^>fX zuLiZA0X-=HWTS7#8m(LYBCyKNI9_Pyg)2sJ@-+c8+A>&c)zu++9u&d zK0Kn#u>4f%`HV2UILlKJLn-uzi7Yyc!(h#VqP9wWaBYGJI~D7*J;D$NE5skD>Du0SO5~YYAQM z?0q1HV3I!*JV$X8W(@K`BQn-48xd%13lNzv{)`2d%~x3!^W~!!FFHdP91!uI60>P9 zFL4o36#lIFiD&ZAS7av?PkYX4OOmQjpBh{y+VG;C*vn{$Sdmb^WE^4dIlCHAhb>kY zMpw5s7(>3NKnJx6i=CjgDFRLZ>vGd_%rDISyaI^)7!3jwj7(ZZc0q1vY@2T54* zCHjmlYTNu-0`b`E*RKOOCiehj(oX9Q+$UylOsiFL+P=7uAj=IX{a9JA?2*MY{2cn? zoKRPM?v32GI@Nk=_LVUwE&KbJ2GooiHk!bWDXlhStcIzF{g5N1ODU>6XAVRsa24b> zklwv}$AWZDK{z*(k@kwojVzK+c%g=ck6!>w#8>RPmutDls*&ra zX1z7BEJ%78JCpZRw&u*>tRp#iIispkK=BC`vHIt`L9u6sn-o83MK$L}XUl+V<@yGNv_(xRZ>TcO@U!;B)oub@Hk3|% z;}^mjjIlX>x61fL^BMVAZZ}W8J1f6~`Ll~IZG~olP4n#(`aDCNrR6?J?;~xPK`FKF z_hGwR{{Gzr-LLQ-y|CwoYpcdWMh(tJY@L|{xAfY^Pdm!_YkX@|w_tnbLU>hh^iQ8I z>MjmqCNmazF_dFctCa7Lgp2mc1&k45?>*WVf0EUGg!8*3ft(@}tFPVL)y4eY5hL7q zjB8Y%_jc{wl`SBMqJ@v{wqpwo3IC2=YUg{wNG*a z&*&uq$7unmv`YYjF7FmI3fdF!x~`6(!YuBu3R+4UuCcc`Pkv5NU0>Zc!hKJf{#F&o zU*XB6=8s@U{FfH?VlWNAFM?n+I%v*wkEmMjEUtDc9!%PN(V>C$`+I5C06}ENC7)oF zIxDL-V5l~?r$k!W=ZExsoVotGE-??WqRr=K$Msd%w$>?odGc@@cE1KgM+SfKjL4!2 zuWwIG=E%WPJAp^9Pd&Lc35m-nj~|!&Z0KCJV#bs&Z7|1MlI0ZH6HOnCC|+G%y2JX9 z<4-sA1@nKZZdG?iUoVXp7*c%;Nt*!fOf|sYfET06bCfcsAeth#Lc7sFr!OF$f zoj83}H2yIn{PprhW{t;ug5d+N7m@anQrez^mLhM`?h$o&J4wvENof<<327JsNwrfq z_p=>e%odjtUe+5>ynHho@b{}lprqZPG4W|Ui8-gLq7vp*iM_;6oPF%Z@w?$SA1Y(# zZ4@!^DB9;wYUfAq-W`+G??vID?wb7kkNkwNgKgZWG1T|v<#Smsqqm!T-Fa`_WKN(J za1ez3=4fxx6!Ma4OT z?ZSosuwYII!$E~L!|A2?^IpViWwf&oVqk>a&n#edXXfSR;}<=Me$0Y@JOcOwD^58< z6AGXiILIX?b+AM8`?bNg4f6DSTTs@>#2NkQEA=}mz?iv06T1*GN=L_el?(AMh9>K7 zPaQ68y@hq%!z!m*@~Tab|JCk}=n7xcsk8ep6g3Ers=xrCYo8I4-n@SwAM?7+#jtZB z)QA_FGXq1Z8H^0WX3h1$H?rR-h8e|$#K=+QJ+Jc_@~&ljqf>}K94C! z_}&*#DQ{}L#kcnfZ6Bdlk3$f2B%y1DZLw2-nf%qa6g$m&@ti~=)zarb0ZVovIAx`b z$qvjVVbDjmse}1j-2wK*C>ih02oQtIKD8ukQP6MfDb@9`uNyW%$2V` zC7bSk#@@w+BacL!G$i|feE+`XuTMJ=qdU-1ki=GEyT85^0X5zXp|8)c7>nresa78feXFcdTjQ2N9?=WriGx&$>1x2W9a7G^=c+aXrh*&4M8JSCWc$lx7Q0vto}K$WbZsG> z9AYwpDLR13Y3|ullzG2$n8!!ud=@rKkjPsWSDR`44#-sCm&?` zJ|QS91Mdu>3%d=ACwc^r6SINOMux(UgFd$mGdmQ;!&!+>MYZ~DnCXK}yQ=dC-OmP! zWga_DT#}M&z+4HAm(vdc92@fivLk}GL85wzF`W3+iiy2sZ~v}*{pi>e*zTW(qg8?^ zwhz<&mWsC5fSoyp=g?lIHy@uT{N|ijMKn|UizRHifNeQH^b3HD);-_#HA3|?(5PdF zP}ZS7a}3v;k1BkFfp`CouJ%}|(fGE95HE~ekHByxbC#US!Gm_Tmo6O@Y@Il+3J)k) zGnH9WYZW_oOq7+BcqhipB|7l76BzWoL8-^!HbM?ANtSRIVd9#%-$*^l`Ie`@4Ate{ z)PuHrzD*4QI=#7CwR^ozB({!>L5L>Q>@k-~8}Ej#UiDcs%?$?@RTZ=ugzK~s)fy?z z351VGm(`C^Ix|rcNfbfW!Q$(PkTCEMH8eCcnJ2YTj7i z09z%Y$0O5z{`?tEJiaM%^)d-wtc1PQH54!FVA4!p%i#vZ|cY?F)?%h_%Tp|^`#*hN09cCzBMlmNzDNz-Qoa;ix)p^r_^|Ocu41Ttdrd79A8R9 zG&r+PZVFm42hF%nkDM}Kc=f%Tl?%I$WS`RII;z-Rx#l+oPJ1KUyHI=+?#&F^2!fZO zu--N1x0uN6j9==dkRl1R%7;m?Vj@YoZ1e1&NBu78o9Da{b!t2aokajXO1rRwU7 zsGm49@)4cLg>wfZ;udlaG13K1S@%n&=AL%Yiy&9jIZ=QH=gQnk>}dwZOEon)-Ke%1 zov{bJW(K}Xf@RKC9|{JVDFpeoT#*b~Hqu8B=~JLYVq@-*)AHc?Xk;%m`bFBo^oWNZ zrJ3D9Ai+b@T3T8@3trnH6C!x3*jZ)>+W^7VORUch+!Khi0`Q{PI#)BA1gfZX5~J6zetKv(*`x zmK(v_aRXb`oP-hCD~}B`4K$_?WWw!LO3mntor1AJdyvVW7z#T^Z$%uY61JS{@Aah#^CS;$ zyf!o*fxOHOQ?o(SVzL~inUsvuIx0Z%gqpd4?x>Z^>IHY3W12doTi^`6q!g0N4#?L4 zd4<9byv75tEiDbi?H6rre|q`sKumaC(?h5In=3e5hjaM}TU#$%LpQrd4-*yKwtcb! z){0p198Fa4`c{s{C)dZLG6RnSU5`2N*&`z(;oJ!* zjqL|NYi!OEM}>`itNAkC0hJeioqs*w=)OMWmxS!XG1_=d$t+BJXA0k6xlv%=Q1?ij z2PK0j=eSwiGz^I0FsLxT2({4>TUn&ckx{z2&>bYhaD4qd!p}mNmA$&YJIN6Za7Ku_ zRV*S^PLeF!C~l3T`Mf327iR#Y1u4b zWa7}Da$0RcU;rXpNKIQyTibVG@Czv@Zu;R}*Q}+LpLj!1?X>yk7wzmkJghvOoj(ae zlZg(+)b5~<&(IBFxCUgaf;zwT+PUxXmcUUpw)fdr`ZXSgbP$)dIWEWZizf7!a4?&> zt|oaH6fL1-5HuS%ET-PbS1%uy)4{vAjJ(Ouch;yigZz-&k|^bGulEUqCbR15>YO$9aK(`912XuK4N?Im6k!@8 zn=v)ftPx~EwRQ;N6!-pcmK8Hx$L;;Dg~mRc08#qRf< z36T;vTS1ayhDM`1AXATRKc<(PLFk7}TD4}XAulEjiz^3g=Ab|-RxH!j|8e}3-Fozt z;^p0S1dEBn!Kr0MYn_5A=78+#aDbyecCyDCUPxE*Jk^vvT=Qh<2zj=cL+78Z;LNBJ zB*Q`n^n7D_x{71)0@EvRt!CT#*$3Dzn^R;vo8|F2nAt{-(XN>`dZhW``2;L5DT1hp zs-#jFP;a%%`^DjXJrTv0dwVg7&-WKm_mPW#N~5i;d#qyJxS`Zt$SEHW*Mlz8M&R`# z`0bkZl(|%QN83)EL-bbVZYQW6^JheHa4mO77zOt`mz^WGTQa*;lsG@o{~qFg*pAvf zjb|DhLnBy1x#tgTq1C^60_Cn12U#^`&`ylb%M3V{kv(-0KeF)M3Ub;myg7Lw3EXJs zvM)$VMuL^wKQ;D#1&t?ya2T-?ey7FuHKAtS(_fFw$>g>lM+Oqk%e1)I5k`&xcNbBU z|EV$twEnHni~rcss7CDlGlDXd!GWX&Mno^42GaAAz|;5W(Q=G|1B0+hKf9z`3-j|? zAc^YoUjl#|_umMy*|N%UIX|j0YXne!J2#iu?Qh@wU}W`?_}r<&`HYDCz{N>SLfXkD zL=hoGLO)B2X^19jZ5Iv-_O~W!p?_pWIloh5=n{LuX+nPIX1UJw9EUEC+;XO08C$3y zb1rMgYfKrbgQAyRKw$j1J+&5(3y-L+&|`Ov6;?)xHK7u-b@Icnd(%JFV(oj@wa?i`?dMsub?>4p=3?feYd9qnYVvslY;yz-XPT zAf6e=*;7e<4|1t2|FXUjIP6kLq2pjdf=ZgMoYi?u@OhR3srkK572ZNGf8+cqP`aZg z7PmLX4>K*na7+Jw5MkASZ`zlSP*yS%Pk|prOU#kkSDDsjAWJlH zVgD-Qkh}Hr=z^Te6uJEnjnRHuBHm$D4kdF1fT@~lQWBdThTPDt#GH6pgUuUcHwlWA z@P;K_lg%3Q2FSC?I3pQ3xz)^_w0HOa-PzBpgM`6vr~9&v6XF4y0C0?n1WuwLhtA2(!TolJ zB?h1$a07<|yi@nk`Z2*tiA}@>M%z&RHzy?1GrgRoLjVK*!NzbDaW5#RF`a>ckFA5~ zRx_ysPmck3Of0&-hhqHcxFuC;lRik4B0Ci4r9$xly*E&PUuS1MT}lH${zmd=5NFcn z$)9o29=(kw&bq(7>jsp77>#JJ?+CE$wfG0AfVSiAi0$dI26h0UlGU^q0jz1Er8xm# z72Mt}3`G%iHeHG29M5K~V8#vZ4nZM^wrMkLB>VVqyK$4TpupKcBdh4fe;E^`L0-k7 z>IiSfHc+Yc)_Vfv?>6n+Q#6rQ=A0yk(+HkNQkmoMMkfb<7(#v zpe@oO0ScBid`Ne5HbvqL*-4VA({nR3%3ST&L5*`0O{cxPf^qX-Ga6?&Rr4@XT1Lhm zlhMF zApZBxAa05wR0!&GU*xhefA6?|Q+&OzBUWKmoToLnk(<~HcOk@< zlggMrL~|wQkt=x+HwL=ASGDcoIAiT1Y(BBM=4TraP}q9I>Fwg;R1W-epkY@nC+gpe zN4O(Lxl~ZHTp5qTZD<^2VN;h#$_MUkKKH%h?EJD(YULB)PJE!k_IWz2`_T!TF|ujV zH?11!hfwBDP~aQK@`#K=P+mPbv{;NV9IvyI5LYe1G zHLn~ydQ|Bsgui-nYO3e;(vi-d$-_k8><;idDRjC`&)|Dmh+mswX#vgO#NBUkclKAH zF>hwwwMZL~i8!lGHnrP!|G!dE|COy*lb;aeMv41EJ!N=@8#${4%46M z4WFk9B!9$^OJ-+fO60a_O<3AX*>Z~{p+1|~RRtajV9yzy=+rEsx9u2z4D88{82Stk zC}Mwzf4hb!FR&Xo4jH(2PdJDcfO_y^>PM-i*Bv59Q%6kAPjH9r2^H1|9>~6Nr2`c4 z%K=_FPKCY{;`^2P1L4}DKASEwP3wB&p60^oNuzg?9B)W>H)kvT^@XDpatdDTC3Fl; z65e9ls3*Vf0mOFw^pOb2ukn_%v-56H0)NaT^Kd#(?x~qQ1CN}83y&R|NSZq3{q-P} zm=;;iJ`gr8yL1M^?IU?v*gtb8_xT;{#}!Bztd%dN)eLl{BXx*mU+x1(vnsc99nshK z4Az-*s@l7wJ!8Qm*+63wWSoM{4+Ve>!iCCT_S)Ns@lq;)aLl+fZ=MPY9CyRis$A^z z@aF*`=BtB%|H7Q*vqHkf-K{KqyXgBp;lrAGKNZOA3yq{ZFC4-7Di6-*oPRwSSx*FI zj4xXwE}l8n^D!`aX|-q7Y5{I?2r_RwT)RuP;Y0lA#2eZDqgpbuxl~7O0VqkG)B%r% zQKa+H<0q@XpUqSWr_(e2mfne~opR5T7e5D|ZWxLK&>}0L8F*|HCCDGs1{yF((WAF} zmD>iW+b~Uwr0InoTTcoM390IvUgoBenac+V(?6?v9BBq$k&IGjt&hf@7}=cFi*qqM z7;8-g&z>!Dj=R&s$_cF6Q#C(#oSm4Xc6dOyVn26a(BrY-HY%D)8dP=BMTd2zY7b_7 z-vTw~>N3(>`-?LTXv^Ba`)aF@B6&qKPwpdW*E$mc3)KQC>=&DGJXsnUMHqj94AJ^w zhf~*$5T22DUn!hOC~OpN9Pn0D@W-{N3WN%Z(uhz-NzGXa?>$o&G&0dOJPa!Dq>(V^ z)YnqT;_d)lf6x_0;RNJHTjvR^@2JCnSYpGQTtkjZ)AS#ecl=%5QvC(3G@QR1f=|;dk z0poqjq~Q~iDZ{-4#}8p$&i+KZvcNYt?qb*!OKU33t1vY>dQ~+?eHp9Gu;WuiSdt;P z(VHm1Ym*|0M?Ef5F;2sg9Ymd+PPxA&M*LyROWc;$&+7?)STzBJurbNo>9kS@vikCg z56+A60CH{0(4qWf-Wu$2d72%EP>!>lqKKO}FObtGq84**c5J!*Q-z-6hY#tp3Ni!I z`k;W1HPq*`)C35vM@o$o3tzuReV6((hoq$kZ=&*8XuBqtZp}Wa9%r%4;ww+{r_S8+ zJUaTdMb+i3#X?Q?f*ccv%)l)mARuLF$KOr>-wJg{Wmepl4!Gh|3JJfC*!Hs_)l#_J z=Zl!-bw+|9pq&1tD+lDYg_9QteuV?N{l|`>6j^w6%=w?J+rF>n^as8aHv~PdEeta` zqwl534ksY`obu^e0{S0NtgJa3+j^AwZ)uA`$_d8m+&Go5^_rX8zWbNM6So4!D;L>$ zH;u7uDCiCaVRr8@8=9so9>fI1!esi~Adn7`HursP3}oG`fWdlk)62`Tt)JmiiG0iy zRCL~Bmp*$bVA`MVnR{%AEd~GDA8D=kqkS1>`Yd4UX4npA>)@~8%c;NJcmE-Zm9s^L z9QaZCk7!%bCST^*i*3{7U_Wxv_JNp62Ypu1^C&i*%6erktH|8vvb~PLnCrKDBh=M<=nb^IWu}(H5SvUEP`hfHc$__E62Xta^xJJ z2s23+AznzbUI2|h7cm2fzj8~sp`jt5v(~_5FkTt$9(PF$ZoHdAxTS??y6+bj1pLek zon6i*msH;W1(hL}MqGVA`~Juh<_HIp9x$N*T?E`zu?sh-s33zR+~^Wf2{3uK!GFFV zd!W>03In$W`({?I712wzo$jLa3&CrM!1*X5>J(m66Gz`@}{{lj75JPdKXYI@<-<`GhXs5Eb^uA&v@Kx<5R>vMKq# zw6yfOtZPp;ArbxHd6lU%j$=_+G!Lpth%U`eyh|S>r$^7(qN+4aLBYDhZ$Ro)9C=}l z8ZfW>KB&Qy#aHp&G58y@Mbe7wU;Qt@?zIya7?XXXZ{*aUMwJx;J|Uz?6`Ii0Pt$hc zZecqd3IUl}{ad=V9}!*E$P(I#n-riNB?Wx=Qb)kLHh2qea{?!XJKJ|#L72iWYaOD?^oGQ*0MQI=dLTw+eNH{Xs|yYxRb<-dQ-aEl_~qj!J~mc2PIpcmhvv9AqR z_J62(uw9h>VpuK-p}rFDdjEPSUOH36&aWC^IP9;Q<@WUgKN_gFDDUdhnh}2aOHKn( zEVbPun$*&PD>Ds|O1vME?-2`u7ko_)`PA%vGj4|^vsM(*f7e&pfGyZQpGBPxEs{@` zLnvVSot8{?I9|LCYw{3?IMx4R|h`62MV)PpLh<9*a!g+Pfr#5 zszC43qUq_5F$;#8+Y~PiJ%!-XqCZjIb zbZ=Z0QI3y})9oSDm7q(Js1Bc}rOm_Kp<0gly&SyZ%B8WnGn12(r5$*`*Bm>~-+lvr z!{3PCk$ArBqNi}+ug?*>_KdE{2UWWAVmX+{bTfzhIqNlNOR;!9WNkctFI6dR5-Y*v zeYCdF!7p&oHrbjYeSLku9Y5UKIkyt~ zh&?83HV0!WfEUH-{uNu60QzKP>by6mTpGhUVfDg58Q8cx|9T)_gS=iSHfAw^JOn>s zMMOjhH66?7-<*Ql!F|{5u&i~fF}IJ%yeT8>+sZkos#A_wy_QVITnwP>(>k$kT(Ger z>!mHy!`I2ojiaj7V88DDybo+GJ=vV&8uvNxck%Q1JRt-Ie(*y|)#z@Mx$uft{q#!0 zvFb4Rro{67%^5tqUXPY0kmHM}&D6so2HAroo9~^{RRc_F1mkV#~V(9iMcg zg^l%%An6wDflPM0J+gO4s1b}c`uYSndm`)@FLuvso4LY*f+}wsXS*jdDEe_S=fD|rtk98g zQOTRj!Q_uQ8?{U%EEBz2CC+jUu9Ti%TIo9EL-ivhVmEuxFqF1)YqcrJB%&rFI7Q}5 z3D5v>qOMu2op45Wh}m?6ckLjZH_?rspoo+&5Ys{Zj;xjSJK5VpiTl9A6y%{J{p-qI zxyUh8Y8*eimVOHcG1%&b3m1er6cI|CsQm+=I>^jbdIsU#e&-e!4}b_jIRbm@ENG?J zen*N!4Jd_b2QsV|@dS{}P?IF3C5~hM)UVAQ)D|+q$Yl>N1*`d2Ocv))QGSFC9Yo$Q z#dcOK)-a7HPo%|^gU7(d>=B#3yjlHbvcS{2Ir_;|Q1L`FT0bO~Z~Up^aqj9|Zmy#e z11T;ADt5luL+O-h5lR5*QR<9zS-@E{pjdwl`$CL2;xfWgNj?mn^i8)D5Ebehlp$G#%5 zHDTLiy!IdQ{Gn}Pj_IV9)7HZzf5}Py;>j@u(zt05HLCzM&6XC-Dh}J9zvOYZ&+d(; zI;a>~grBfOf^^{dwY4?M7yWkdK?~&f?DwNGr%f`YN83N5*Ky) zN%E{JxRNRr(1#5f+i+$CU>bI>!wC*pnDvV`vNX|#5nBhN{Ga%n*MYGkzavlZRa`=p P7dmsw>}1h#r+fbcq=_I` literal 2053 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 literal 2063 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K54sfsm$=7f9rU5CIbVpxD28NCO+288s%Ds3`13fB;wb|85r0ZJzX3_Dj471HO!N77hnyz znDF9)dGZowgKv>CpBZ{8jN7|ZjYYWI+?ewo|G!z+cV|Xu=-1Ux*~1UMIxYTRP{E;r zfsu)YLjV`!yV8e(l8MXLpSSOeD}MSsf4zP?Tll{DH|upb{QmvBKmK!tSHl5YZjOfP zd+T-!S_lX*usg^J??`VvzIpoRnLpRexNgpU=l1WPZ|-OQ;9c*#7tZUH863#@2tE{e1J^@ZWRh zrr%3HXFh-aTKm^?|9;;5e~tNk`MLJD|F8I0^Ul0M^N~-2GSEL4H^ojpAR@pJd}QVU zHzuZp#i_fs8Z{LdmUS36_Oh@Xa64TV$`tIt;3JsMB+bdu&^ztzDwbsp3^SC@v6u-7 zFi7j(4(0S=WHfM{!eU$zSy&4UFxI5-+Ob}wVGVqtMOF6ZN@ zprpW{^k$aO3t&=_tu|n7>}X)<*pThDz}bPJMHkzxBOvFMp%KJWC;k38EHXKfh9T zD2v=B+yP9v8vCEIYyw99>-E-+D}agOQ?{RiwgSVZf5St1|L60~_p1)cZ`Z5XbZ;%F zRM~Ux)vwjR?^gZ0`|9l9J74}3guH$E`ZN27?boLtwl5C3T+GCBjbqN=%CCY6XXQdv z3pg1W3p!0DCmoOi7BI&`Lz$)n^U(B&$%>q71O*tfV?qZ+3Aot&H`TfP>(qqz@rBxpXTbyMPlA`k)fh6qf8VWpW(S^X2_kjiKldMYqts~U2eK2cfoe)mS3j3^ HP6 - {config.isQRCode ? : } - - ); -} - export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): ReactElement { const config = barcodeConfig(props); - if (!config.value) { + if (!config.codeValue) { return No barcode value provided; } return ( - - - +
+ {config.type === "qrcode" ? : } +
); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index ed7b714f25..8f33e8435d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -1,22 +1,29 @@ import { useRenderBarcode } from "../hooks/useRenderBarcode"; -import { useDownloadBarcode } from "../hooks/useDownloadBarcode"; -import { useBarcodeConfig } from "../config/BarcodeContext"; +import { downloadBarcodeFromRef } from "../utils/download-svg"; +import { BarcodeTypeConfig } from "../config/Barcode.config"; -import { Fragment } from "react"; +import { Fragment, ReactElement } from "react"; -export const BarcodeRenderer = () => { - const ref = useRenderBarcode(); - const { allowDownload, downloadAriaLabel } = useBarcodeConfig(); - const { downloadBarcode } = useDownloadBarcode({ ref }); +interface BarcodeRendererProps { + config: BarcodeTypeConfig; +} + +export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement { + const ref = useRenderBarcode(config); + const { allowDownload, downloadAriaLabel } = config; return ( {allowDownload && ( - )} ); -}; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index 879e0f8462..2e052523a2 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -1,56 +1,33 @@ import { QRCodeSVG } from "qrcode.react"; -import { Fragment, useRef } from "react"; -import { useDownloadQrCode } from "../hooks/useDownloadQRCode"; -import { useBarcodeConfig } from "../config/BarcodeContext"; +import { Fragment, ReactElement, useRef } from "react"; +import { downloadQrCodeFromRef } from "../utils/download-svg"; +import { QRCodeTypeConfig } from "../config/Barcode.config"; -export const QRCodeRenderer = () => { +interface QRCodeRendererProps { + config: QRCodeTypeConfig; +} + +export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { downloadQrCode } = useDownloadQrCode({ ref }); - const { - value, - allowDownload, - qrSize: size, - qrMargin: margin, - qrTitle: title, - qrLevel: level, - qrImageSrc: imageSrc, - qrImageX: imageX, - qrImageY: imageY, - qrImageHeight: imageHeight, - qrImageWidth: imageWidth, - qrImageOpacity: imageOpacity, - qrImageExcavate: imageExcavate, - downloadAriaLabel: downloadAriaLabel - } = useBarcodeConfig(); - const imageSettings = imageSrc - ? { - src: imageSrc, - x: imageX, - y: imageY, - height: imageHeight, - width: imageWidth, - opacity: imageOpacity, - excavate: imageExcavate - } - : undefined; + const { codeValue, allowDownload, size, margin, title, level, downloadAriaLabel, image } = config; return ( {allowDownload && ( )} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index 2e052523a2..952280413d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -10,7 +10,7 @@ interface QRCodeRendererProps { export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { codeValue, allowDownload, size, margin, title, level, downloadAriaLabel, image } = config; + const { codeValue, downloadButton, size, margin, title, level, image } = config; return ( @@ -23,14 +23,14 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { title={title} imageSettings={image} /> - {allowDownload && ( + {downloadButton && ( )} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index 6fa65878b9..4f430b71ba 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -1,5 +1,10 @@ import { BarcodeGeneratorContainerProps, QrLevelEnum } from "../../typings/BarcodeGeneratorProps"; +interface DownloadButtonConfig { + caption?: string; + label?: string; +} + /** Configuration for barcode (non-QR) rendering */ export interface BarcodeTypeConfig { type: "barcode"; @@ -9,8 +14,7 @@ export interface BarcodeTypeConfig { format: string; margin: number; displayValue: boolean; - allowDownload: boolean; - downloadAriaLabel?: string; + downloadButton?: DownloadButtonConfig; // Advanced barcode options enableEan128: boolean; @@ -30,8 +34,7 @@ export interface QRCodeTypeConfig { margin: number; title: string; level: QrLevelEnum; - allowDownload: boolean; - downloadAriaLabel?: string; + downloadButton?: DownloadButtonConfig; image?: { src: string; x: number | undefined; @@ -49,6 +52,13 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon const codeValue = props.codeValue?.value ?? ""; const format = props.codeFormat === "Custom" ? props.customCodeFormat : props.codeFormat; + const downloadButtonConfig = props.allowDownload + ? { + caption: props.downloadButtonCaption?.value, + label: props.downloadButtonAriaLabel?.value + } + : undefined; + if (format === "QRCode") { return { type: "qrcode", @@ -57,8 +67,7 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon margin: props.qrMargin ?? 2, title: props.qrTitle ?? "", level: props.qrLevel ?? "L", - allowDownload: props.allowDownload ?? false, - downloadAriaLabel: props.downloadAriaLabel, + downloadButton: downloadButtonConfig, image: props.qrImageSrc?.status === "available" ? { @@ -82,8 +91,7 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon format, margin: props.codeMargin ?? 2, displayValue: props.displayValue ?? false, - allowDownload: props.allowDownload ?? false, - downloadAriaLabel: props.downloadAriaLabel, + downloadButton: downloadButtonConfig, // Advanced barcode options enableEan128: props.enableEan128 ?? false, diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 8776621ad5..e16b70c49f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -23,7 +23,8 @@ export interface BarcodeGeneratorContainerProps { codeValue: EditableValue; codeFormat: CodeFormatEnum; allowDownload: boolean; - downloadAriaLabel: string; + downloadButtonCaption?: DynamicValue; + downloadButtonAriaLabel?: DynamicValue; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; @@ -65,7 +66,8 @@ export interface BarcodeGeneratorPreviewProps { codeValue: string; codeFormat: CodeFormatEnum; allowDownload: boolean; - downloadAriaLabel: string; + downloadButtonCaption: string; + downloadButtonAriaLabel: string; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; From 6ea329e70f45aa135eb3299a818378901fc96437 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 6 Feb 2026 11:08:08 +0100 Subject: [PATCH 03/18] feat: download as png instead of svg, append hash to filename --- .../src/components/Barcode.tsx | 2 +- .../src/components/QRCode.tsx | 2 +- .../src/config/Barcode.config.ts | 22 ++++++- .../src/utils/download-svg.ts | 31 ++++------ .../src/utils/download-utils.ts | 60 +++++++++++++++++-- 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index b2e57aa9e6..038b9c3729 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -19,7 +19,7 @@ export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index 952280413d..ead5fbd9e2 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -27,7 +27,7 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index ead5fbd9e2..b9191b75f7 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -1,6 +1,6 @@ import { QRCodeSVG } from "qrcode.react"; import { Fragment, ReactElement, useRef } from "react"; -import { downloadQrCodeFromRef } from "../utils/download-svg"; +import { downloadCode } from "../utils/download-code"; import { QRCodeTypeConfig } from "../config/Barcode.config"; interface QRCodeRendererProps { @@ -27,7 +27,7 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement {
- )} - + {buttonPosition === "bottom" && button} + ); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index b9191b75f7..a4dcd2f59a 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -1,6 +1,7 @@ import { QRCodeSVG } from "qrcode.react"; -import { Fragment, ReactElement, useRef } from "react"; +import { ReactElement, useRef } from "react"; import { downloadCode } from "../utils/download-code"; +import { DownloadIcon } from "./icons/DownloadIcon"; import { QRCodeTypeConfig } from "../config/Barcode.config"; interface QRCodeRendererProps { @@ -10,10 +11,24 @@ interface QRCodeRendererProps { export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { codeValue, downloadButton, size, margin, title, level, image } = config; + const { codeValue, downloadButton, size, margin, title, level, image, buttonPosition } = config; + + const button = downloadButton && ( + downloadCode(ref, config.type, downloadButton.fileName)} + > + {downloadButton.caption} + + ); return ( - +
+ {title &&

{title}

} + {buttonPosition === "top" && button} - {downloadButton && ( - - )} - + {buttonPosition === "bottom" && button} +
); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx new file mode 100644 index 0000000000..be0d8ea08e --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx @@ -0,0 +1,21 @@ +import { ReactElement } from "react"; + +export function DownloadIcon(): ReactElement { + return ( + <> + + + ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index f23e3ee153..55648372d2 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -15,6 +15,7 @@ export interface BarcodeTypeConfig { format: string; margin: number; displayValue: boolean; + buttonPosition: "top" | "bottom"; downloadButton?: DownloadButtonConfig; // Advanced barcode options @@ -35,6 +36,7 @@ export interface QRCodeTypeConfig { margin: number; title: string; level: QrLevelEnum; + buttonPosition: "top" | "bottom"; downloadButton?: DownloadButtonConfig; image?: { src: string; @@ -69,6 +71,7 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon margin: props.qrMargin ?? 2, title: props.qrTitle ?? "", level: props.qrLevel ?? "L", + buttonPosition: props.buttonPosition ?? "bottom", downloadButton: downloadButtonConfig, image: props.qrImageSrc?.status === "available" @@ -93,6 +96,7 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon format, margin: props.codeMargin ?? 2, displayValue: props.displayValue ?? false, + buttonPosition: props.buttonPosition ?? "bottom", downloadButton: downloadButtonConfig, // Advanced barcode options diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index e219c30909..373277064b 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -3,4 +3,46 @@ $widget-prefix: "barcode-generator"; .#{$widget-prefix} { display: inline-block; + border-radius: var(--card-border-radius); + + &--as-card { + background-color: var(--card-bg); + border: var(--card-border); + padding: var(--spacing-medium); + } +} + +.qrcode-renderer { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + svg { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + } + + .qrcode-renderer-title { + font-weight: var(--font-weight-normal); + font-size: var(--font-size-small); + color: var(--gray-darker); + margin: 0; + } +} + +.barcode-renderer { + display: flex; + flex-direction: column; + align-items: start; + gap: 12px; + + svg { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + } } diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index e16b70c49f..9c649846ac 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -9,6 +9,8 @@ import { Big } from "big.js"; export type CodeFormatEnum = "CODE128" | "QRCode" | "Custom"; +export type ButtonPositionEnum = "top" | "bottom"; + export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "UPC" | "CODE39" | "ITF14" | "MSI" | "pharmacode" | "codabar" | "CODE93"; export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; @@ -20,11 +22,12 @@ export interface BarcodeGeneratorContainerProps { class: string; style?: CSSProperties; tabIndex?: number; - codeValue: EditableValue; + codeValue: DynamicValue; codeFormat: CodeFormatEnum; allowDownload: boolean; downloadButtonCaption?: DynamicValue; downloadButtonAriaLabel?: DynamicValue; + buttonPosition: ButtonPositionEnum; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; @@ -34,6 +37,7 @@ export interface BarcodeGeneratorContainerProps { addonValue: EditableValue; addonSpacing: number; displayValue: boolean; + showAsCard: boolean; codeWidth: number; codeHeight: number; codeMargin: number; @@ -68,6 +72,7 @@ export interface BarcodeGeneratorPreviewProps { allowDownload: boolean; downloadButtonCaption: string; downloadButtonAriaLabel: string; + buttonPosition: ButtonPositionEnum; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; @@ -77,6 +82,7 @@ export interface BarcodeGeneratorPreviewProps { addonValue: string; addonSpacing: number | null; displayValue: boolean; + showAsCard: boolean; codeWidth: number | null; codeHeight: number | null; codeMargin: number | null; From 7f12039acd31d594143707cb95c556a660837768 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 12 Feb 2026 13:30:31 +0100 Subject: [PATCH 06/18] feat: enhance QR/barcode rendering in design preview mode --- .../src/BarcodeGenerator.editorPreview.tsx | 174 +++++++++++++++++- .../src/BarcodeGenerator.xml | 6 +- .../src/ui/BarcodeGenerator.scss | 2 +- .../src/ui/BarcodeGeneratorPreview.scss | 44 +++++ 4 files changed, 218 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx index 3d0cebc9ba..b0d14c7f9b 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx @@ -1,13 +1,179 @@ -import { ReactElement } from "react"; +import classNames from "classnames"; +import { type CSSProperties, ReactElement, useState } from "react"; +import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; +import { DownloadIcon } from "./components/icons/DownloadIcon"; import BarcodePreviewSVG from "./assets/BarcodeGeneratorPreview.svg"; -export function preview(_props: BarcodeGeneratorPreviewProps): ReactElement { +const defaultDownloadCaption = "Download"; +const qrImagePlaceholder = + "data:image/svg+xml;utf8," + + "" + + "" + + "" + + ""; + +function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElement | null { + if (!props.allowDownload) { + return null; + } + + return ( + + {props.downloadButtonCaption || defaultDownloadCaption} + + ); +} + +function PreviewQrCode(props: BarcodeGeneratorPreviewProps): ReactElement { const doc = decodeURI(BarcodePreviewSVG); + const downloadButton = ; + const qrSize = props.qrSize ?? 128; + // Note: qrMargin is in module units (QR grid cells), not pixels + // The QRCodeSVG component handles margin internally within the specified size + const displaySize = Math.min(qrSize, 400); // Clamped to 400px for preview + const qrImageWidth = props.qrImageWidth ?? 32; + const qrImageHeight = props.qrImageHeight ?? 32; + const qrImageOpacity = props.qrImageOpacity ?? 1; + const qrImageX = props.qrImageX ?? 0; + const qrImageY = props.qrImageY ?? 0; + + const [imageSrcError, setImageSrcError] = useState(false); + + // Resolve the actual image URL from user config + const resolveImageSrc = (): string => { + if (!props.qrImageSrc) { + return qrImagePlaceholder; + } + + if (imageSrcError) { + return qrImagePlaceholder; + } + + // Static image URL + if (props.qrImageSrc.type === "static") { + return props.qrImageSrc.imageUrl; + } + + // Dynamic image (from data entity) - not directly resolvable in preview + // Fall back to placeholder + return qrImagePlaceholder; + }; + + const imageBaseStyle: CSSProperties = props.qrImageCenter + ? { + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + width: qrImageWidth, + height: qrImageHeight + } + : { + left: qrImageX, + top: qrImageY, + width: qrImageWidth, + height: qrImageHeight + }; + + return ( +
+ {props.qrTitle &&

{props.qrTitle}

} + {props.buttonPosition === "top" && downloadButton} +
+ + {props.qrImage && ( + <> + {props.qrImageExcavate && ( + + {props.buttonPosition === "bottom" && downloadButton} +
+ ); +} + +function PreviewBarcode(props: BarcodeGeneratorPreviewProps): ReactElement { + const downloadButton = ; + const codeHeight = props.codeHeight ?? 200; + const displayHeight = Math.min(codeHeight, 400); // Clamped to 400px for preview return ( -
- +
+ {props.buttonPosition === "top" && downloadButton} + + + + + + + + + + + + + + + + + + + + + + + + + + + {props.buttonPosition === "bottom" && downloadButton}
); } + +export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { + const styles = parseStyle(props.style); + const isQrCode = props.codeFormat === "QRCode"; + + return ( +
+ {isQrCode ? : } +
+ ); +} + +export function getPreviewCss(): string { + return require("./ui/BarcodeGeneratorPreview.scss"); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index f7190e06d1..22b8716f0a 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -125,7 +125,7 @@ Code height - Height of the barcode + Height of the barcode. Note: In preview, the max height is 400px. The barcode will render at full height in your application. Margin size @@ -133,11 +133,11 @@ QR Size - The size of the QR box + The size of the QR box. Note: In preview, the max height is 400px. The QR code will render at full size in your application. Margin size - + Number of module units (QR grid cells) to use for margin. Increasing compresses the QR pattern within the fixed size. Note: not visible in preview. Title diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index 373277064b..11bde6ae7f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -2,7 +2,7 @@ $widget-prefix: "barcode-generator"; .#{$widget-prefix} { - display: inline-block; + display: block; border-radius: var(--card-border-radius); &--as-card { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss index e69de29bb2..c440718553 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss @@ -0,0 +1,44 @@ +@use "BarcodeGenerator"; + +.barcode-generator-widget-preview { + width: 100%; + display: inline-block; + + .barcode-generator { + display: flex; + flex-direction: column; + } + + .barcode-preview-graphic { + max-width: 100%; + width: 100%; + height: auto; + display: block; + object-fit: contain; + } + + .barcode-preview-graphic--qr { + width: auto; + } + + .barcode-preview-graphic--barcode { + width: 100%; + } + + .barcode-preview-qr-container { + position: relative; + display: inline-block; + max-width: 100%; + } + + .barcode-preview-qr-image-excavate { + position: absolute; + background-color: #ffffff; + outline: 3px solid #ffffff; + } + + .barcode-preview-qr-image { + position: absolute; + object-fit: contain; + } +} From d19e86a7c4e716ea9fc1870b881681a8006c80e7 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 13 Feb 2026 14:54:30 +0100 Subject: [PATCH 07/18] feat: improve barcode error handling - static value consistency checks - dynamic value warnings and runtime error alert --- .../src/BarcodeGenerator.editorConfig.ts | 97 ++++++++++++++++--- .../src/components/Barcode.tsx | 12 ++- .../src/hooks/useRenderBarcode.ts | 30 +++++- .../src/ui/BarcodeGenerator.scss | 19 +--- 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 8851c36fd8..a1d2e69c69 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -128,28 +128,95 @@ function getActiveFormat(values: BarcodeGeneratorPreviewProps): string { return values.codeFormat; } +function stripQuotes(value: string): string { + // Remove leading/trailing quotes and whitespace from expression values + let trimmed = value.trim(); + // Match and remove surrounding quotes (single or double) + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + trimmed = trimmed.slice(1, -1); + } + return trimmed; +} + +function isDynamicExpression(value: string): boolean { + // Check if the value is a dynamic expression (attribute binding, variable, etc.) + // Dynamic expressions start with $ or contain / paths or are empty + return !value || value.startsWith("$") || value.includes("/"); +} + +function getFormatHint(format: string): string { + const hints: Record = { + EAN13: "EAN-13 requires 12 or 13 numeric digits", + EAN8: "EAN-8 requires 7 or 8 numeric digits", + UPC: "UPC requires 11 or 12 numeric digits", + ITF14: "ITF-14 requires exactly 14 numeric digits", + CODE39: "CODE39: uppercase A-Z, digits, space and - . $ / + % (max 43 chars)", + CODE128: "CODE128: alphanumeric, no control characters (max 80 chars)", + CODE93: "CODE93: alphanumeric, no control characters (max 47 chars)", + MSI: "MSI: numeric only (max 30 digits)", + pharmacode: "Pharmacode: numeric only (max 7 digits)", + codabar: "Codabar: digits, A-D start/stop, and - $ : / . + (max 20 chars)", + QRCode: "QR Code: any text (max 1200 chars recommended)" + }; + return hints[format] || ""; +} + function validateCodeValues(values: BarcodeGeneratorPreviewProps): Problem[] { const problems: Problem[] = []; - const val = values.codeValue ?? ""; - const addon = values.addonValue ?? ""; + const rawVal = values.codeValue ?? ""; + const rawAddon = values.addonValue ?? ""; const format = getActiveFormat(values); - // Only validate static (design-time) values — if empty, skip (user may bind dynamically) - if (!val) { - // still validate addon if present - } else { - const result = validateBarcodeValue(format, val); - if (!result.valid) { - const msg = result.message || "Invalid barcode value for selected format."; - problems.push({ property: "codeValue", severity: "warning", message: msg }); + // Add informational hint for dynamic expressions + if (isDynamicExpression(rawVal) && rawVal) { + const hint = getFormatHint(format); + if (hint) { + problems.push({ + property: "codeValue", + severity: "warning", + message: `Dynamic value provided. Ensure runtime value matches format: ${hint}` + }); } } - // Validate addon value if visible - const addonResult = validateAddonValue(values.addonFormat, addon); - if (!addonResult.valid) { - const msg = addonResult.message || "Invalid addon value."; - problems.push({ property: "addonValue", severity: "warning", message: msg }); + // Only validate static literal values, skip dynamic expressions (attribute bindings, variables, etc.) + if (!isDynamicExpression(rawVal)) { + const val = stripQuotes(rawVal); + if (val) { + const result = validateBarcodeValue(format, val); + if (!result.valid) { + const msg = result.message || "Invalid barcode value for selected format."; + problems.push({ property: "codeValue", severity: "error", message: msg }); + } + } + } + + // Validate addon value if visible and format is selected + if (values.addonFormat !== "None") { + // Add informational hint for dynamic addon expressions + if (isDynamicExpression(rawAddon) && rawAddon) { + const addonHint = + values.addonFormat === "EAN5" + ? "EAN-5 addon requires exactly 5 numeric digits" + : "EAN-2 addon requires exactly 2 numeric digits"; + problems.push({ + property: "addonValue", + severity: "warning", + message: `Dynamic addon value provided. Ensure runtime value matches format: ${addonHint}` + }); + } + + // Validate static addon values + if (!isDynamicExpression(rawAddon)) { + const addon = stripQuotes(rawAddon); + if (addon) { + const addonResult = validateAddonValue(values.addonFormat, addon); + if (!addonResult.valid) { + const msg = addonResult.message || "Invalid addon value."; + problems.push({ property: "addonValue", severity: "error", message: msg }); + } + } + } } return problems; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index 973b38244f..de1187205a 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -10,9 +10,19 @@ interface BarcodeRendererProps { } export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement { - const ref = useRenderBarcode(config); + const { ref, error } = useRenderBarcode(config); const { downloadButton, buttonPosition } = config; + if (error) { + return ( +
+
+ Barcode Error: {error} +
+
+ ); + } + const button = downloadButton && ( => { +export const useRenderBarcode = ( + config: BarcodeTypeConfig +): { ref: RefObject; error: string | null } => { const ref = useRef(null); + const [error, setError] = useState(null); const { codeValue: value, @@ -23,6 +27,22 @@ export const useRenderBarcode = (config: BarcodeTypeConfig): RefObject { if (ref && typeof ref !== "function" && ref.current && value) { + // Validate barcode value at runtime + const validationResult = validateBarcodeValue(format, value); + if (!validationResult.valid) { + setError(validationResult.message || "Invalid barcode value"); + return; + } + + // Validate addon if present + if (addonValue && addonFormat && addonFormat !== "None") { + const addonResult = validateAddonValue(addonFormat, addonValue); + if (!addonResult.valid) { + setError(addonResult.message || "Invalid addon value"); + return; + } + } + try { const renderOptions: BarcodeRenderOptions = { value, @@ -41,8 +61,10 @@ export const useRenderBarcode = (config: BarcodeTypeConfig): RefObject svg { max-width: 100%; max-height: 100%; width: auto; @@ -32,17 +33,3 @@ $widget-prefix: "barcode-generator"; margin: 0; } } - -.barcode-renderer { - display: flex; - flex-direction: column; - align-items: start; - gap: 12px; - - svg { - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; - } -} From b18afd1c195be7b6aed534eef0b050137583176a Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 13 Feb 2026 16:36:33 +0100 Subject: [PATCH 08/18] fix: improve configuration - enable displayValue for addons - improve conditional visibility for certain configurations - improve descriptions --- .../src/BarcodeGenerator.editorConfig.ts | 28 ++++++++++++------- .../src/BarcodeGenerator.xml | 10 +++---- .../src/utils/barcodeRenderer-utils.ts | 4 +-- .../typings/BarcodeGeneratorProps.d.ts | 4 +-- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index a1d2e69c69..73d33beac9 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -35,29 +35,37 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope hidePropertyIn(defaultProperties, values, "enableEan128"); } + // enableFlat is only supported for EAN-13 and EAN-8, and NOT when addons are enabled if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && - values.customCodeFormat !== "EAN13" && - values.customCodeFormat !== "EAN8" && - values.customCodeFormat !== "UPC") + !( + values.codeFormat === "Custom" && + (values.customCodeFormat === "EAN13" || values.customCodeFormat === "EAN8") && + values.addonFormat === "None" + ) ) { hidePropertyIn(defaultProperties, values, "enableFlat"); } + // lastChar is only supported for EAN-13, and NOT when flat is enabled or addons are present if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13") + !( + values.codeFormat === "Custom" && + values.customCodeFormat === "EAN13" && + !values.enableFlat && + values.addonFormat === "None" + ) ) { hidePropertyIn(defaultProperties, values, "lastChar"); } + // EAN addons are only supported for EAN-13, EAN-8, and UPC if ( values.codeFormat === "QRCode" || values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13" && values.customCodeFormat !== "EAN8") + (values.codeFormat === "Custom" && + values.customCodeFormat !== "EAN13" && + values.customCodeFormat !== "EAN8" && + values.customCodeFormat !== "UPC") ) { hidePropertiesIn(defaultProperties, values, ["addonFormat", "addonValue", "addonSpacing"]); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 22b8716f0a..37cce521ce 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -76,11 +76,11 @@
Flat - Enable flat barcode, skip guard bars + Enable flat barcode, skip guard bars. Note: Doesn't work with EAN addons. Last character - Character after the barcode + Character after the barcode. Note: Doesn't work when 'Flat' is enabled or with EAN addons. Mod43 @@ -97,12 +97,10 @@ EAN-2 - + Addon value Value for the addon barcode (5 digits for EAN-5, 2 digits for EAN-2) - - - + Addon spacing diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts index 97802f0428..1830366147 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts @@ -72,8 +72,8 @@ export const createBarcodeWithAddon = ( // Add spacing BarcodeService.blank(addonSpacing); - // Add addon dynamically - BarcodeService[addonFormat](addonValue, { width: 1 }); + // Add addon dynamically with same displayValue setting + BarcodeService[addonFormat](addonValue, { width: 1, displayValue: options.displayValue }); BarcodeService.render(); } diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 9c649846ac..86c3980250 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { DynamicValue, EditableValue, WebImage } from "mendix"; +import { DynamicValue, WebImage } from "mendix"; import { Big } from "big.js"; export type CodeFormatEnum = "CODE128" | "QRCode" | "Custom"; @@ -34,7 +34,7 @@ export interface BarcodeGeneratorContainerProps { lastChar: string; enableMod43: boolean; addonFormat: AddonFormatEnum; - addonValue: EditableValue; + addonValue: DynamicValue; addonSpacing: number; displayValue: boolean; showAsCard: boolean; From ac721077989481db766f3763a6c28ccb9fadb81e Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 16 Feb 2026 16:53:36 +0100 Subject: [PATCH 09/18] refactor: enhance preview component - added svg assets for barcode preview - add asset resolver for image sources - encapsulate svg logic into hook - cleaner main preview component --- .../src/BarcodeGenerator.editorPreview.tsx | 147 ++---------------- .../src/assets/barcodePreview.assets.ts | 80 ++++++++++ .../src/assets/barcodes/codabar.svg | 1 + .../src/assets/barcodes/code128.svg | 1 + .../src/assets/barcodes/code39.svg | 1 + .../src/assets/barcodes/code93.svg | 1 + .../src/assets/barcodes/ean13-ean2.svg | 1 + .../src/assets/barcodes/ean13-ean5.svg | 1 + .../src/assets/barcodes/ean13-flat.svg | 1 + .../src/assets/barcodes/ean13.svg | 1 + .../src/assets/barcodes/ean8-ean2.svg | 1 + .../src/assets/barcodes/ean8-ean5.svg | 1 + .../src/assets/barcodes/ean8-flat.svg | 1 + .../src/assets/barcodes/ean8.svg | 1 + .../src/assets/barcodes/itf14.svg | 1 + .../src/assets/barcodes/msi.svg | 1 + .../src/assets/barcodes/pharmacode.svg | 1 + .../src/assets/barcodes/upc-ean2.svg | 1 + .../src/assets/barcodes/upc-ean5.svg | 1 + .../src/assets/barcodes/upc.svg | 1 + .../src/components/preview/BarcodePreview.tsx | 40 +++++ .../src/components/preview/QRCodePreview.tsx | 73 +++++++++ .../src/hooks/useBarcodePreviewSvg.ts | 93 +++++++++++ .../src/utils/qrcode-preview-utils.ts | 26 ++++ 24 files changed, 339 insertions(+), 138 deletions(-) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx index b0d14c7f9b..7e8f1cf166 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx @@ -1,17 +1,12 @@ import classNames from "classnames"; -import { type CSSProperties, ReactElement, useState } from "react"; +import { ReactElement } from "react"; import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; import { DownloadIcon } from "./components/icons/DownloadIcon"; -import BarcodePreviewSVG from "./assets/BarcodeGeneratorPreview.svg"; +import { BarcodePreview } from "./components/preview/BarcodePreview"; +import { QRCodePreview } from "./components/preview/QRCodePreview"; const defaultDownloadCaption = "Download"; -const qrImagePlaceholder = - "data:image/svg+xml;utf8," + - "" + - "" + - "" + - ""; function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElement | null { if (!props.allowDownload) { @@ -25,138 +20,10 @@ function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElemen ); } -function PreviewQrCode(props: BarcodeGeneratorPreviewProps): ReactElement { - const doc = decodeURI(BarcodePreviewSVG); - const downloadButton = ; - const qrSize = props.qrSize ?? 128; - // Note: qrMargin is in module units (QR grid cells), not pixels - // The QRCodeSVG component handles margin internally within the specified size - const displaySize = Math.min(qrSize, 400); // Clamped to 400px for preview - const qrImageWidth = props.qrImageWidth ?? 32; - const qrImageHeight = props.qrImageHeight ?? 32; - const qrImageOpacity = props.qrImageOpacity ?? 1; - const qrImageX = props.qrImageX ?? 0; - const qrImageY = props.qrImageY ?? 0; - - const [imageSrcError, setImageSrcError] = useState(false); - - // Resolve the actual image URL from user config - const resolveImageSrc = (): string => { - if (!props.qrImageSrc) { - return qrImagePlaceholder; - } - - if (imageSrcError) { - return qrImagePlaceholder; - } - - // Static image URL - if (props.qrImageSrc.type === "static") { - return props.qrImageSrc.imageUrl; - } - - // Dynamic image (from data entity) - not directly resolvable in preview - // Fall back to placeholder - return qrImagePlaceholder; - }; - - const imageBaseStyle: CSSProperties = props.qrImageCenter - ? { - left: "50%", - top: "50%", - transform: "translate(-50%, -50%)", - width: qrImageWidth, - height: qrImageHeight - } - : { - left: qrImageX, - top: qrImageY, - width: qrImageWidth, - height: qrImageHeight - }; - - return ( -
- {props.qrTitle &&

{props.qrTitle}

} - {props.buttonPosition === "top" && downloadButton} -
- - {props.qrImage && ( - <> - {props.qrImageExcavate && ( - - {props.buttonPosition === "bottom" && downloadButton} -
- ); -} - -function PreviewBarcode(props: BarcodeGeneratorPreviewProps): ReactElement { - const downloadButton = ; - const codeHeight = props.codeHeight ?? 200; - const displayHeight = Math.min(codeHeight, 400); // Clamped to 400px for preview - - return ( -
- {props.buttonPosition === "top" && downloadButton} - - - - - - - - - - - - - - - - - - - - - - - - - - - {props.buttonPosition === "bottom" && downloadButton} -
- ); -} - export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { const styles = parseStyle(props.style); const isQrCode = props.codeFormat === "QRCode"; + const downloadButton = ; return (
- {isQrCode ? : } + {isQrCode ? ( + + ) : ( + + )}
); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts new file mode 100644 index 0000000000..e62f30435a --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts @@ -0,0 +1,80 @@ +// Import all barcode SVG files +import code128Svg from "./barcodes/code128.svg"; +import ean13Svg from "./barcodes/ean13.svg"; +import ean13Ean2Svg from "./barcodes/ean13-ean2.svg"; +import ean13Ean5Svg from "./barcodes/ean13-ean5.svg"; +import ean13FlatSvg from "./barcodes/ean13-flat.svg"; +import ean8Svg from "./barcodes/ean8.svg"; +import ean8Ean2Svg from "./barcodes/ean8-ean2.svg"; +import ean8Ean5Svg from "./barcodes/ean8-ean5.svg"; +import ean8FlatSvg from "./barcodes/ean8-flat.svg"; +import upcSvg from "./barcodes/upc.svg"; +import upcEan2Svg from "./barcodes/upc-ean2.svg"; +import upcEan5Svg from "./barcodes/upc-ean5.svg"; +import code39Svg from "./barcodes/code39.svg"; +import itf14Svg from "./barcodes/itf14.svg"; +import msiSvg from "./barcodes/msi.svg"; +import pharmacodeSvg from "./barcodes/pharmacode.svg"; +import codabarSvg from "./barcodes/codabar.svg"; +import code93Svg from "./barcodes/code93.svg"; + +type BarcodeImageVariants = { + default: string; + flat?: string; + EAN2?: string; + EAN5?: string; +}; + +const barcodeImageMap: Record = { + CODE128: { default: code128Svg }, + EAN13: { + default: ean13Svg, + EAN2: ean13Ean2Svg, + EAN5: ean13Ean5Svg, + flat: ean13FlatSvg + }, + EAN8: { + default: ean8Svg, + EAN2: ean8Ean2Svg, + EAN5: ean8Ean5Svg, + flat: ean8FlatSvg + }, + UPC: { + default: upcSvg, + EAN2: upcEan2Svg, + EAN5: upcEan5Svg + }, + CODE39: { default: code39Svg }, + ITF14: { default: itf14Svg }, + MSI: { default: msiSvg }, + pharmacode: { default: pharmacodeSvg }, + codabar: { default: codabarSvg }, + CODE93: { default: code93Svg } +}; + +export function getBarcodeImageUrl( + codeFormat: string, + customCodeFormat: string, + addonFormat: string, + enableFlat: boolean +): string | null { + const format = codeFormat === "Custom" ? customCodeFormat : codeFormat; + const formatMap = barcodeImageMap[format]; + + if (!formatMap) return null; + + if (enableFlat && (format === "EAN13" || format === "EAN8")) { + return formatMap.flat || formatMap.default; + } + + if (addonFormat && addonFormat !== "None" && (format === "EAN13" || format === "EAN8" || format === "UPC")) { + if (addonFormat === "EAN2") { + return formatMap.EAN2 || formatMap.default; + } + if (addonFormat === "EAN5") { + return formatMap.EAN5 || formatMap.default; + } + } + + return formatMap.default || null; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg new file mode 100644 index 0000000000..558073304b --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg @@ -0,0 +1 @@ +1234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg new file mode 100644 index 0000000000..54399b37d1 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg new file mode 100644 index 0000000000..1f8ed586ca --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg @@ -0,0 +1 @@ +HELLO-WORLD \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg new file mode 100644 index 0000000000..5ac6720dc9 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg @@ -0,0 +1 @@ +CODE93EXAMPLE \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg new file mode 100644 index 0000000000..f722a5c843 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg @@ -0,0 +1 @@ +590123412345742 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg new file mode 100644 index 0000000000..c039ce9106 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg @@ -0,0 +1 @@ +590123412345751234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg new file mode 100644 index 0000000000..4c753de60c --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg new file mode 100644 index 0000000000..54399b37d1 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg new file mode 100644 index 0000000000..928a6435e7 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg @@ -0,0 +1 @@ +9638507442 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg new file mode 100644 index 0000000000..216984f0cf --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg @@ -0,0 +1 @@ +9638507451234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg new file mode 100644 index 0000000000..1f031583a0 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg @@ -0,0 +1 @@ +96385074 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg new file mode 100644 index 0000000000..a91fd9f834 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg @@ -0,0 +1 @@ +96385074 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg new file mode 100644 index 0000000000..3d328376ae --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg @@ -0,0 +1 @@ +04006381333931 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg new file mode 100644 index 0000000000..f79b391a1a --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg @@ -0,0 +1 @@ +1234567890 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg new file mode 100644 index 0000000000..f8ee5a5acb --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg @@ -0,0 +1 @@ +123456 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg new file mode 100644 index 0000000000..64e2fcc764 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg @@ -0,0 +1 @@ +12345678901242 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg new file mode 100644 index 0000000000..9c243f4d2e --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg @@ -0,0 +1 @@ +12345678901251234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg new file mode 100644 index 0000000000..3e081cb948 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg @@ -0,0 +1 @@ +123456789012 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx new file mode 100644 index 0000000000..df05d44bc7 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx @@ -0,0 +1,40 @@ +import { ReactElement } from "react"; +import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; +import { useBarcodePreviewSvg } from "../../hooks/useBarcodePreviewSvg"; + +interface BarcodePreviewProps extends BarcodeGeneratorPreviewProps { + downloadButton: ReactElement | null; +} + +export function BarcodePreview(props: BarcodePreviewProps): ReactElement { + const { downloadButton, ...restProps } = props; + const codeHeight = restProps.codeHeight ?? 200; + const displayHeight = Math.min(codeHeight, 400); // Clamped to 400px for preview + + const { imageUrl, displayUrl } = useBarcodePreviewSvg({ + codeFormat: restProps.codeFormat, + customCodeFormat: restProps.customCodeFormat, + addonFormat: restProps.addonFormat, + enableFlat: restProps.enableFlat === true, + displayValue: restProps.displayValue + }); + + return ( +
+ {restProps.buttonPosition === "top" && downloadButton} +
+ {imageUrl ? ( + Barcode preview + ) : ( +
Barcode format not supported
+ )} +
+ {restProps.buttonPosition === "bottom" && downloadButton} +
+ ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx new file mode 100644 index 0000000000..ac8a22ef04 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx @@ -0,0 +1,73 @@ +import { type CSSProperties, ReactElement, useState } from "react"; +import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; +import BarcodePreviewSVG from "../../assets/BarcodeGeneratorPreview.svg"; +import { resolveQRImageSrc } from "../../utils/qrcode-preview-utils"; + +interface QRCodePreviewProps extends BarcodeGeneratorPreviewProps { + downloadButton: ReactElement | null; +} + +export function QRCodePreview(props: QRCodePreviewProps): ReactElement { + const { downloadButton, ...restProps } = props; + const doc = decodeURI(BarcodePreviewSVG); + const qrSize = restProps.qrSize ?? 128; + // Note: qrMargin is in module units (QR grid cells), not pixels + // The QRCodeSVG component handles margin internally within the specified size + const displaySize = Math.min(qrSize, 400); // Clamped to 400px for preview + const qrImageWidth = restProps.qrImageWidth ?? 32; + const qrImageHeight = restProps.qrImageHeight ?? 32; + const qrImageOpacity = restProps.qrImageOpacity ?? 1; + const qrImageX = restProps.qrImageX ?? 0; + const qrImageY = restProps.qrImageY ?? 0; + + const [imageSrcError, setImageSrcError] = useState(false); + + const imageBaseStyle: CSSProperties = restProps.qrImageCenter + ? { + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + width: qrImageWidth, + height: qrImageHeight + } + : { + left: qrImageX, + top: qrImageY, + width: qrImageWidth, + height: qrImageHeight + }; + + return ( +
+ {restProps.qrTitle &&

{restProps.qrTitle}

} + {restProps.buttonPosition === "top" && downloadButton} +
+ + {restProps.qrImage && ( + <> + {restProps.qrImageExcavate && ( + + {restProps.buttonPosition === "bottom" && downloadButton} +
+ ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts new file mode 100644 index 0000000000..fcee148482 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts @@ -0,0 +1,93 @@ +import { useEffect, useMemo, useState } from "react"; +import { getBarcodeImageUrl } from "../assets/barcodePreview.assets"; + +type UseBarcodePreviewSvgOptions = { + codeFormat: string; + customCodeFormat: string; + addonFormat: string; + enableFlat: boolean; + displayValue?: boolean; +}; + +type UseBarcodePreviewSvgResult = { + imageUrl: string | null; + displayUrl: string | null; +}; + +export function useBarcodePreviewSvg(options: UseBarcodePreviewSvgOptions): UseBarcodePreviewSvgResult { + const imageUrl = useMemo( + () => getBarcodeImageUrl(options.codeFormat, options.customCodeFormat, options.addonFormat, options.enableFlat), + [options.codeFormat, options.customCodeFormat, options.addonFormat, options.enableFlat] + ); + + const [modifiedSvgUrl, setModifiedSvgUrl] = useState(null); + + useEffect(() => { + let active = true; + + if (!imageUrl) { + setModifiedSvgUrl(null); + return () => { + active = false; + }; + } + + if (options.displayValue === true) { + setModifiedSvgUrl(null); + return () => { + active = false; + }; + } + + fetch(imageUrl) + .then(response => response.text()) + .then(svgText => { + if (!active) return; + const modifiedSvg = conditionallyModifySVG(svgText, false); + setModifiedSvgUrl(svgToDataUri(modifiedSvg)); + }) + .catch(() => { + if (active) { + setModifiedSvgUrl(null); + } + }); + + return () => { + active = false; + }; + }, [imageUrl, options.displayValue]); + + return { + imageUrl, + displayUrl: modifiedSvgUrl ?? imageUrl + }; +} + +function conditionallyModifySVG(svgString: string, showText: boolean): string { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(svgString, "image/svg+xml"); + + if (doc.getElementsByTagName("parsererror").length > 0) { + return svgString; + } + + const textElements = doc.querySelectorAll("text"); + textElements.forEach(text => { + text.style.display = showText ? "block" : "none"; + }); + + return new XMLSerializer().serializeToString(doc); + } catch { + return svgString; + } +} + +function svgToDataUri(svgString: string): string { + try { + const encodedSvg = encodeURIComponent(svgString); + return `data:image/svg+xml;charset=UTF-8,${encodedSvg}`; + } catch { + return ""; + } +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts new file mode 100644 index 0000000000..2cf1de02cb --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts @@ -0,0 +1,26 @@ +export const QR_IMAGE_PLACEHOLDER = + "data:image/svg+xml;utf8," + + "" + + "" + + "" + + ""; + +// Resolve the actual image URL from user config +export function resolveQRImageSrc(qrImageSrc: any, imageSrcError: boolean): string { + if (!qrImageSrc) { + return QR_IMAGE_PLACEHOLDER; + } + + if (imageSrcError) { + return QR_IMAGE_PLACEHOLDER; + } + + // Static image URL + if (qrImageSrc.type === "static") { + return qrImageSrc.imageUrl; + } + + // Dynamic image (from data entity) - not directly resolvable in preview + // Fall back to placeholder + return QR_IMAGE_PLACEHOLDER; +} From c152c5077ef9d4f2ec4e933667dd93c77f167451 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 16 Feb 2026 17:01:15 +0100 Subject: [PATCH 10/18] refactor: move buttonPosition into button config --- .../barcode-generator-web/src/components/Barcode.tsx | 3 ++- .../barcode-generator-web/src/components/QRCode.tsx | 3 ++- .../barcode-generator-web/src/config/Barcode.config.ts | 9 ++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index de1187205a..fee7437fb6 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -11,7 +11,8 @@ interface BarcodeRendererProps { export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement { const { ref, error } = useRenderBarcode(config); - const { downloadButton, buttonPosition } = config; + const { downloadButton } = config; + const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; if (error) { return ( diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index a4dcd2f59a..c03acfa7fc 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -11,7 +11,8 @@ interface QRCodeRendererProps { export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { codeValue, downloadButton, size, margin, title, level, image, buttonPosition } = config; + const { codeValue, downloadButton, size, margin, title, level, image } = config; + const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; const button = downloadButton && (
Date: Tue, 17 Feb 2026 13:52:35 +0100 Subject: [PATCH 11/18] feat: add structure mode preview image --- .../src/BarcodeGenerator.editorConfig.ts | 11 ++ .../src/assets/structurePreview.svg | 151 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 73d33beac9..d8c6b0374a 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -1,6 +1,8 @@ +import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; import { validateAddonValue, validateBarcodeValue } from "./config/validation"; +import structurePreviewSvg from "./assets/structurePreview.svg"; export type Problem = { property?: string; // key of the property, at which the problem exists @@ -96,6 +98,15 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope return defaultProperties; } +export function getPreview(_: StructurePreviewProps): StructurePreviewProps | null { + return { + type: "Image", + document: decodeURIComponent(structurePreviewSvg.replace("data:image/svg+xml,", "")), + height: 275, + width: 275 + }; +} + export function check(_values: BarcodeGeneratorPreviewProps): Problem[] { const errors: Problem[] = []; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg new file mode 100644 index 0000000000..7e8f05641f --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From eb79fb327354fefb8de1fb97663989a530ad1d7c Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 17 Feb 2026 14:12:00 +0100 Subject: [PATCH 12/18] chore: update changelog --- .../barcode-generator-web/CHANGELOG.md | 2 + .../src/__tests__/BarcodeGenerator.spec.tsx | 1198 ++++++++++++++--- 2 files changed, 978 insertions(+), 222 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md index 21b358e32a..3a3175b3dc 100644 --- a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md +++ b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Error handling for incompatible barcode types +- Enhanced preview for all barcode types - Comprehensive configuration and styling settings for various barcode types - Download functionality for barcodes diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx index 9c04ef785e..4c55ab2069 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -1,277 +1,1031 @@ import "@testing-library/jest-dom"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; // Mock JsBarcode const mockJsBarcode = jest.fn(); -const barcodeDefaultValue = `default barcode value`; jest.mock("jsbarcode", () => mockJsBarcode); // Mock the QRCodeSVG component jest.mock("qrcode.react", () => ({ - QRCodeSVG: ({ value, size }: { value: string; size: number }) => ( -
+ QRCodeSVG: ({ value, size, level, marginSize, title, imageSettings }: any) => ( +
QR Code: {value}
) })); +// Mock download functionality +jest.mock("../utils/download-code", () => ({ + downloadCode: jest.fn() +})); + import BarcodeGenerator from "../BarcodeGenerator"; import { CodeFormatEnum, CustomCodeFormatEnum } from "typings/BarcodeGeneratorProps"; +import { downloadCode } from "../utils/download-code"; -describe("BarcodeGenerator", () => { - const defaultProps = { - name: "barcodeGenerator1", - class: "mx-barcode-generator", - tabIndex: -1, - codeFormat: "QRCode" as CodeFormatEnum, - customCodeFormat: "CODE128" as CustomCodeFormatEnum, - enableEan128: false, - enableFlat: false, - lastChar: "", - enableMod43: false, - allowDownload: false, - downloadAriaLabel: "Download barcode", - displayValue: false, - codeWidth: 2, - codeHeight: 200, - codeMargin: 4, - qrSize: 128, - qrMargin: 2, - qrTitle: "", - qrLevel: "L" as any, - qrImage: false, - qrImageSrc: { status: "unavailable" } as any, - qrImageCenter: true, - qrImageX: 0, - qrImageY: 0, - qrImageHeight: 24, - qrImageWidth: 24, - qrImageOpacity: { toNumber: () => 1 } as any, - qrImageExcavate: true, - addonFormat: "None" as any, - addonValue: { status: "unavailable" } as any, - addonSpacing: 20, - codeValue: new EditableValueBuilder().withValue(barcodeDefaultValue).build() - }; +// Test utilities +const createMockWebImage = (status: "available" | "loading" | "unavailable" = "unavailable"): any => { + if (status === "available") { + return { + status: "available" as const, + value: { uri: "data:image/png;base64,test123" } + } as any; + } + return { status } as any; +}; + +const createBarcodeProps = (overrides: any = {}): any => ({ + name: "barcodeGenerator1", + class: "mx-barcode-generator", + tabIndex: -1, + codeFormat: "QRCode" as CodeFormatEnum, + customCodeFormat: "CODE128" as CustomCodeFormatEnum, + enableEan128: false, + enableFlat: false, + lastChar: "", + enableMod43: false, + allowDownload: false, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + downloadButtonAriaLabel: { status: "available" as const, value: "Download barcode" } as any, + displayValue: false, + showAsCard: false, + codeWidth: 2, + codeHeight: 200, + codeMargin: 4, + qrSize: 128, + qrMargin: 2, + qrTitle: "", + qrLevel: "L" as any, + qrImage: false, + qrImageSrc: createMockWebImage(), + qrImageCenter: true, + qrImageX: 0, + qrImageY: 0, + qrImageHeight: 24, + qrImageWidth: 24, + qrImageOpacity: { toNumber: () => 1 } as any, + qrImageExcavate: true, + addonFormat: "None" as any, + addonValue: { status: "unavailable" as const } as any, + addonSpacing: 20, + buttonPosition: "bottom" as const, + codeValue: new EditableValueBuilder().withValue("test-barcode-value").build(), + ...overrides +}); +describe("BarcodeGenerator", () => { beforeEach(() => { jest.clearAllMocks(); }); - it("renders QR code when value is available", () => { - const props = { - ...defaultProps, - codeValue: { - value: "Hello World", - status: "available" - } as any - }; + // ============= Core Rendering Tests ============= + describe("core rendering", () => { + it("renders QR code when codeValue is available", () => { + const props = createBarcodeProps({ + codeValue: { + value: "Hello World", + status: "available" + } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-value", "Hello World"); + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "128"); + }); + + it("shows fallback message when codeValue is loading", () => { + const props = createBarcodeProps({ + codeValue: { value: "", status: "loading" } as any + }); + + render(); + + expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); + expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + }); + + it("shows fallback message when codeValue is unavailable", () => { + const props = createBarcodeProps({ + codeValue: { value: "", status: "unavailable" } as any + }); + + render(); + + expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); + expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + }); + + it("applies correct CSS classes and tabIndex", () => { + const props = createBarcodeProps({ + class: "custom-class", + tabIndex: 2, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveClass("barcode-generator", "custom-class"); + expect(widget).toHaveAttribute("tabIndex", "2"); + }); + + it("applies card styling when showAsCard is true", () => { + const props = createBarcodeProps({ + showAsCard: true, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveClass("barcode-generator--as-card"); + }); + }); + + // ============= Barcode Format Tests ============= + describe("barcode formats", () => { + it("renders CODE128 barcode correctly", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "123456789", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456789", + expect.objectContaining({ format: "CODE128" }) + ); + }); + + it("renders CODE39 barcode with uppercase letters and special characters", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "CODE39" as CustomCodeFormatEnum, + codeValue: { value: "ABC-123", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "ABC-123", + expect.objectContaining({ format: "CODE39" }) + ); + }); + + it("renders CODE93 barcode", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "CODE93" as CustomCodeFormatEnum, + codeValue: { value: "CODE93VALUE", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "CODE93VALUE", + expect.objectContaining({ format: "CODE93" }) + ); + }); + + it("renders EAN-13 barcode with 13 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "1234567890128", + expect.objectContaining({ format: "EAN13" }) + ); + }); + + it("renders EAN-8 barcode with 8 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN8" as CustomCodeFormatEnum, + codeValue: { value: "12345678", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "12345678", + expect.objectContaining({ format: "EAN8" }) + ); + }); + + it("renders UPC barcode with 12 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "UPC" as CustomCodeFormatEnum, + codeValue: { value: "123456789012", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456789012", + expect.objectContaining({ format: "UPC" }) + ); + }); + + it("renders ITF-14 barcode with exactly 14 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "ITF14" as CustomCodeFormatEnum, + codeValue: { value: "12345678901234", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "12345678901234", + expect.objectContaining({ format: "ITF14" }) + ); + }); + + it("renders MSI barcode with numeric digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "MSI" as CustomCodeFormatEnum, + codeValue: { value: "123456", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456", + expect.objectContaining({ format: "MSI" }) + ); + }); + + it("renders Pharmacode barcode with numeric digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "pharmacode" as CustomCodeFormatEnum, + codeValue: { value: "1234567", status: "available" } as any + }); - render(); + render(); - expect(screen.getByTestId("qr-code")).toBeInTheDocument(); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-value", "Hello World"); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "128"); + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "1234567", + expect.objectContaining({ format: "pharmacode" }) + ); + }); + + it("renders Codabar barcode", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "codabar" as CustomCodeFormatEnum, + codeValue: { value: "123-456", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123-456", + expect.objectContaining({ format: "codabar" }) + ); + }); }); - it("shows no barcode message when data is loading", () => { - const props = { - ...defaultProps, - codeValue: { - value: "", - status: "loading" - } as any - }; + // ============= QR Code Tests ============= + describe("QR code rendering", () => { + it("renders QR code with custom size", () => { + const props = createBarcodeProps({ + qrSize: 256, + codeValue: { value: "Custom Size QR", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "256"); + }); + + it("renders QR code with custom margin", () => { + const props = createBarcodeProps({ + qrMargin: 5, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-margin", "5"); + }); + + it("renders QR code with all error correction levels", () => { + const levels: any[] = ["L", "M", "Q", "H"]; + + levels.forEach(level => { + const props = createBarcodeProps({ + qrLevel: level, + codeValue: { value: "test", status: "available" } as any + }); + + const { unmount } = render(); - render(); + expect(screen.getAllByTestId("qr-code")[0]).toHaveAttribute("data-level", level); + unmount(); + }); + }); + + it("renders QR code with title", () => { + const props = createBarcodeProps({ + qrTitle: "QR Code Title", + codeValue: { value: "test", status: "available" } as any + }); - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + render(); + + expect(screen.getByText("QR Code Title")).toBeInTheDocument(); + }); }); - it("shows no barcode message when data is unavailable", () => { - const props = { - ...defaultProps, - codeValue: { - value: "", - status: "unavailable" - } as any - }; + // ============= QR Image Overlay Tests ============= + describe("QR image overlay functionality", () => { + it("renders QR code with image overlay when qrImage is true", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "true"); + }); + + it("renders QR code with centered image overlay", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageCenter: true, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with positioned image overlay", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageCenter: false, + qrImageX: 10, + qrImageY: 20, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); - render(); + it("renders QR code with image overlay custom dimensions", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageWidth: 50, + qrImageHeight: 50, + codeValue: { value: "test", status: "available" } as any + }); - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with image overlay opacity", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageOpacity: { toNumber: () => 0.75 } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with image excavation enabled", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageExcavate: true, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("does not render image overlay when qrImageSrc is unavailable", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("unavailable"), + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + // QR code should render but without image + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "false"); + }); }); - it("renders CODE128 barcode when format is not QR", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as CodeFormatEnum, - codeValue: { - value: "123456789", - status: "available" - } as any - }; - - render(); - - // Should not render QR code - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - - // Should have called JsBarcode - expect(mockJsBarcode).toHaveBeenCalledWith( - expect.any(Object), // SVG element - "123456789", - { - format: "CODE128", - width: 2, - height: 200, - margin: 4, - displayValue: false, - ean128: false, - flat: false, - lastChar: "", - mod43: false - } - ); + // ============= Download Button Tests ============= + describe("download button functionality", () => { + it("does not render download button when allowDownload is false", () => { + const props = createBarcodeProps({ + allowDownload: false, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("renders download button with custom caption", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Export Code" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByText("Export Code")).toBeInTheDocument(); + }); + + it("renders download button with correct aria-label for QR code", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonAriaLabel: { status: "available" as const, value: "Download QR code" } as any, + codeFormat: "QRCode" as CodeFormatEnum, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByRole("button")).toHaveAttribute("aria-label", "Download QR code"); + }); + + it("renders download button at top position", () => { + const props = createBarcodeProps({ + allowDownload: true, + buttonPosition: "top" as const, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + + const renderer = container.querySelector(".qrcode-renderer"); + expect(renderer).toBeInTheDocument(); + // Get all children + const children = Array.from((renderer as HTMLElement).children); + // Download button should be first child + const firstChild = children[0] as HTMLElement; + expect(firstChild).toHaveClass("mx-link"); + }); + + it("renders download button at bottom position", () => { + const props = createBarcodeProps({ + allowDownload: true, + buttonPosition: "bottom" as const, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toBeInTheDocument(); + }); + + it("calls downloadCode when download button is clicked", () => { + const mockDownloadCode = downloadCode as jest.Mock; + + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeFormat: "QRCode" as CodeFormatEnum, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + expect(mockDownloadCode).toHaveBeenCalled(); + }); + + it("renders download button with icon and caption", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Save" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveTextContent("Save"); + }); }); - it("renders QR code with custom size", () => { - const props = { - ...defaultProps, - qrSize: 256, - codeValue: { - value: "Custom Size QR", - status: "available" - } as any - }; + // ============= Barcode Display Options Tests ============= + describe("barcode display options", () => { + it("passes displayValue option to JsBarcode correctly", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: true, + codeValue: { value: "DISPLAY123", status: "available" } as any + }); - render(); + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "DISPLAY123", + expect.objectContaining({ displayValue: true }) + ); + }); + + it("does not display value when displayValue is false", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: false, + codeValue: { value: "NODISPLAY", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "NODISPLAY", + expect.objectContaining({ displayValue: false }) + ); + }); + + it("applies custom width to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeWidth: 3, + codeValue: { value: "WIDTH_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "WIDTH_TEST", + expect.objectContaining({ width: 3 }) + ); + }); + + it("applies custom height to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeHeight: 300, + codeValue: { value: "HEIGHT_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "HEIGHT_TEST", + expect.objectContaining({ height: 300 }) + ); + }); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "256"); + it("applies custom margin to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeMargin: 8, + codeValue: { value: "MARGIN_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "MARGIN_TEST", + expect.objectContaining({ margin: 8 }) + ); + }); }); - it("passes displayValue option to JSBarcode for non-QR codes", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - displayValue: true, - codeValue: { - value: "DISPLAY123", - status: "available" - } as any - }; - - render(); - - expect(mockJsBarcode).toHaveBeenCalledWith( - expect.any(Object), - "DISPLAY123", - expect.objectContaining({ - displayValue: true - }) - ); + // ============= Advanced Barcode Options Tests ============= + describe("advanced barcode options", () => { + it("applies EAN-128 encoding when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableEan128: true, + codeValue: { value: "EAN128TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "EAN128TEST", + expect.objectContaining({ ean128: true }) + ); + }); + + it("applies flat mode when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableFlat: true, + codeValue: { value: "FLATTEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "FLATTEST", + expect.objectContaining({ flat: true }) + ); + }); + + it("applies MOD43 checksum when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableMod43: true, + codeValue: { value: "MOD43TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "MOD43TEST", + expect.objectContaining({ mod43: true }) + ); + }); + + it("applies custom last character", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + lastChar: "X", + codeValue: { value: "LASTCHARTEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "LASTCHARTEST", + expect.objectContaining({ lastChar: "X" }) + ); + }); }); - it("handles JSBarcode errors gracefully", () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - mockJsBarcode.mockImplementation(() => { - throw new Error("Invalid barcode format"); + // ============= EAN Addon Tests ============= + describe("EAN addon functionality", () => { + it("supports EAN-5 addon format", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN5: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "EAN5" as any, + addonSpacing: 25, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN13).toHaveBeenCalledWith("1234567890128", expect.any(Object)); + expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(25); + expect(mockBarcodeInstance.EAN5).toHaveBeenCalledWith("12345", expect.any(Object)); + expect(mockBarcodeInstance.render).toHaveBeenCalled(); + }); + + it("supports EAN-2 addon format", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN2: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12", status: "available" } as any, + addonFormat: "EAN2" as any, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN13).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN2).toHaveBeenCalledWith("12", expect.any(Object)); + }); + + it("does not apply addon when addonFormat is None", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "None" as any, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith(expect.any(Object), "1234567890128", expect.any(Object)); }); - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - codeValue: { - value: "INVALID", - status: "available" - } as any - }; + it("applies custom addon spacing", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN5: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); - render(); + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "EAN5" as any, + addonSpacing: 40, + codeValue: { value: "1234567890128", status: "available" } as any + }); - expect(consoleSpy).toHaveBeenCalledWith("Error generating barcode:", expect.any(Error)); - consoleSpy.mockRestore(); + render(); + + expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(40); + }); }); - it("applies correct CSS class and tabIndex", () => { - const props = { - ...defaultProps, - class: "mx-barcode-generator custom-class", - tabIndex: 5, - codeValue: { - value: "CSS Test", - status: "available" - } as any - }; - - const { container } = render(); - - const widget = container.firstChild as HTMLElement; - expect(widget).toHaveClass("barcode-generator"); - expect(widget).toHaveAttribute("tabIndex", "5"); + // ============= Error Handling Tests ============= + describe("error handling", () => { + it("renders error message when JsBarcode throws", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Invalid barcode value"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "INVALID", status: "available" } as any + }); + + render(); + + expect(screen.getByText(/Barcode Error:/)).toBeInTheDocument(); + expect(screen.getByText(/Invalid barcode value/)).toBeInTheDocument(); + }); + + it("renders alert role for error message", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Format error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "TEST", status: "available" } as any + }); + + render(); + + const alert = screen.getByRole("alert"); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass("alert-danger"); + }); + + it("clears error when valid barcode value is provided after error", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Initial error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "BAD", status: "available" } as any + }); + + const { unmount } = render(); + + expect(screen.getByText(/Barcode Error:/)).toBeInTheDocument(); + + // Clean up first render to avoid duplicate DOM + unmount(); + + // Mock now succeeds + mockJsBarcode.mockReset(); + + const goodProps = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "GOOD", status: "available" } as any + }); + + render(); + + expect(screen.queryByText(/Barcode Error:/)).not.toBeInTheDocument(); + }); }); - it("uses fallback values when props are missing", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - codeValue: { - value: "DEFAULT_TEST", - status: "available" - } as any - }; - - // Component uses nullish coalescing to provide defaults - render(); - - expect(mockJsBarcode).toHaveBeenCalledWith(expect.any(Object), "DEFAULT_TEST", { - format: "CODE128", - width: 2, // from defaultProps - height: 200, // from defaultProps - margin: 4, // from defaultProps - displayValue: false, - ean128: false, - flat: false, - lastChar: "", - mod43: false + // ============= Accessibility Tests ============= + describe("accessibility", () => { + it("renders QR code title as semantic element when provided", () => { + const props = createBarcodeProps({ + qrTitle: "Invoice QR Code", + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const title = screen.getByText("Invoice QR Code"); + expect(title).toBeInTheDocument(); + expect(title.tagName).toBe("H3"); + }); + + it("does not render title when qrTitle is empty", () => { + const props = createBarcodeProps({ + qrTitle: "", + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.queryByRole("heading")).not.toBeInTheDocument(); + }); + + it("download button has proper semantics", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download Barcode" } as any, + downloadButtonAriaLabel: { + status: "available" as const, + value: "Download current barcode as PNG" + } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("aria-label", "Download current barcode as PNG"); + expect(button).toHaveTextContent("Download Barcode"); + }); + + it("download button is keyboard accessible", async () => { + const mockDownloadCode = downloadCode as jest.Mock; + + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + const user = userEvent.setup(); + + await user.click(button); + + expect(mockDownloadCode).toHaveBeenCalled(); + }); + + it("error messages have alert role for screen readers", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Test error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "TEST", status: "available" } as any + }); + + render(); + + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + it("barcode widget container is focusable when tabIndex is set", () => { + const props = createBarcodeProps({ + tabIndex: 0, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveAttribute("tabIndex", "0"); }); }); - it("supports EAN addon functionality", () => { - const mockBarcodeInstance = { - EAN13: jest.fn().mockReturnThis(), - blank: jest.fn().mockReturnThis(), - EAN5: jest.fn().mockReturnThis(), - render: jest.fn() - }; - - mockJsBarcode.mockReturnValue(mockBarcodeInstance); - - const props = { - ...defaultProps, - codeFormat: "Custom" as CodeFormatEnum, - customCodeFormat: "EAN13" as any, - addonValue: { - value: "12345", - status: "available" - } as any, - addonFormat: "EAN5" as any, - addonSpacing: 25, - codeValue: { - value: "1234567890128", - status: "available" - } as any - }; - - render(); - - expect(mockJsBarcode).toHaveBeenCalled(); - expect(mockBarcodeInstance.EAN13).toHaveBeenCalledWith("1234567890128", expect.any(Object)); - expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(25); - expect(mockBarcodeInstance.EAN5).toHaveBeenCalledWith("12345", expect.any(Object)); - expect(mockBarcodeInstance.render).toHaveBeenCalled(); + // ============= Integration Tests ============= + describe("integration scenarios", () => { + it("renders QR code with download, title, and image overlay", () => { + const props = createBarcodeProps({ + allowDownload: true, + qrTitle: "Secure QR", + qrImage: true, + qrImageSrc: createMockWebImage("available"), + downloadButtonCaption: { status: "available" as const, value: "Save QR" } as any, + codeValue: { value: "secure-data", status: "available" } as any + }); + + render(); + + expect(screen.getByText("Secure QR")).toBeInTheDocument(); + expect(screen.getByText("Save QR")).toBeInTheDocument(); + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "true"); + }); + + it("renders barcode with all advanced options enabled", () => { + const mockBarcodeInstance = { + render: jest.fn() + }; + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: true, + showAsCard: true, + enableEan128: true, + enableFlat: true, + enableMod43: true, + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Export" } as any, + codeWidth: 3, + codeHeight: 250, + codeMargin: 5, + lastChar: "Z", + codeValue: { value: "FULL_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "FULL_TEST", + expect.objectContaining({ + displayValue: true, + ean128: true, + flat: true, + mod43: true, + width: 3, + height: 250, + margin: 5, + lastChar: "Z" + }) + ); + expect(screen.getByText("Export")).toBeInTheDocument(); + }); }); }); From 9ded9b7e0b444590572577b33412e8dbc3aad03f Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 19 Feb 2026 11:58:32 +0100 Subject: [PATCH 13/18] fix: improve error handling and messaging for barcodes --- .../src/components/Barcode.tsx | 3 +- .../src/hooks/useRenderBarcode.ts | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index fee7437fb6..a5f25ae164 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -18,7 +18,8 @@ export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement return (
- Barcode Error: {error} + Unable to generate barcode. Please check the barcode value and format + configuration.
); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts index a1dae82d97..f08fab510d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -5,9 +5,9 @@ import { validateAddonValue, validateBarcodeValue } from "../config/validation"; export const useRenderBarcode = ( config: BarcodeTypeConfig -): { ref: RefObject; error: string | null } => { +): { ref: RefObject; error: boolean } => { const ref = useRef(null); - const [error, setError] = useState(null); + const [error, setError] = useState(false); const { codeValue: value, @@ -30,7 +30,14 @@ export const useRenderBarcode = ( // Validate barcode value at runtime const validationResult = validateBarcodeValue(format, value); if (!validationResult.valid) { - setError(validationResult.message || "Invalid barcode value"); + const errorMsg = validationResult.message || "Invalid barcode value"; + // Log detailed error for developers + console.error( + `[Barcode Generator] Validation failed for format "${format}":`, + errorMsg, + `\nProvided value: "${value}"` + ); + setError(true); return; } @@ -38,7 +45,14 @@ export const useRenderBarcode = ( if (addonValue && addonFormat && addonFormat !== "None") { const addonResult = validateAddonValue(addonFormat, addonValue); if (!addonResult.valid) { - setError(addonResult.message || "Invalid addon value"); + const errorMsg = addonResult.message || "Invalid addon value"; + // Log detailed error for developers + console.error( + `[Barcode Generator] Addon validation failed for format "${addonFormat}":`, + errorMsg, + `\nProvided addon value: "${addonValue}"` + ); + setError(true); return; } } @@ -61,10 +75,18 @@ export const useRenderBarcode = ( }; renderBarcode(ref, renderOptions); - setError(null); // Clear any previous errors + setError(false); // Clear any previous errors } catch (error) { const errorMsg = error instanceof Error ? error.message : "Error generating barcode"; - setError(errorMsg); + // Log detailed error for developers + console.error( + `[Barcode Generator] Rendering failed:`, + errorMsg, + `\nFormat: "${format}"`, + `\nValue: "${value}"`, + error + ); + setError(true); } } }, [ From a0f8b20c98193230c0e0bc16d3a8ab2374291bf5 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 19 Feb 2026 16:29:13 +0100 Subject: [PATCH 14/18] refactor: simplify preview dom and styling classes - simplified preview DOM - removed redundant css classes - merged remaining preview styles into main stylesheet - renamed qr overlay property and css classes - updated tests error messages to check for new messages --- .../src/BarcodeGenerator.editorConfig.ts | 24 +++---- .../src/BarcodeGenerator.editorPreview.tsx | 12 ++-- .../src/BarcodeGenerator.xml | 20 +++--- .../src/__tests__/BarcodeGenerator.spec.tsx | 12 ++-- .../src/components/preview/BarcodePreview.tsx | 24 +++---- .../src/components/preview/QRCodePreview.tsx | 66 +++++++++---------- .../src/config/Barcode.config.ts | 16 ++--- .../src/ui/BarcodeGenerator.scss | 25 +++++++ .../src/ui/BarcodeGeneratorPreview.scss | 44 ------------- .../typings/BarcodeGeneratorProps.d.ts | 36 +++++----- 10 files changed, 126 insertions(+), 153 deletions(-) delete mode 100644 packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index d8c6b0374a..144aaf29f3 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -17,19 +17,19 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope if (values.codeFormat === "QRCode") { hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); } else { - hidePropertiesIn(defaultProperties, values, ["qrImage", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); + hidePropertiesIn(defaultProperties, values, ["qrOverlay", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); } - if (values.codeFormat !== "QRCode" || !values.qrImage) { + if (values.codeFormat !== "QRCode" || !values.qrOverlay) { hidePropertiesIn(defaultProperties, values, [ - "qrImageSrc", - "qrImageCenter", - "qrImageWidth", - "qrImageHeight", - "qrImageX", - "qrImageY", - "qrImageOpacity", - "qrImageExcavate" + "qrOverlaySrc", + "qrOverlayCenter", + "qrOverlayWidth", + "qrOverlayHeight", + "qrOverlayX", + "qrOverlayY", + "qrOverlayOpacity", + "qrOverlayExcavate" ]); } @@ -87,8 +87,8 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope hidePropertyIn(defaultProperties, values, "enableMod43"); } - if (values.qrImageCenter) { - hidePropertiesIn(defaultProperties, values, ["qrImageX", "qrImageY"]); + if (values.qrOverlayCenter) { + hidePropertiesIn(defaultProperties, values, ["qrOverlayX", "qrOverlayY"]); } if (values.codeFormat !== "Custom") { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx index 7e8f1cf166..44f78544d7 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx @@ -27,13 +27,9 @@ export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { return (
{isQrCode ? ( @@ -46,5 +42,5 @@ export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { } export function getPreviewCss(): string { - return require("./ui/BarcodeGeneratorPreview.scss"); + return require("./ui/BarcodeGenerator.scss"); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 37cce521ce..ede58f677c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -151,39 +151,39 @@ H - - Image + + Overlay image Include an image on top the QR code - + Image source URL or path to the image to display on the QR code - + Center image Center the image in the QR code - + Image X position Horizontal position of the image - + Image Y position Vertical position of the image - + Image height Height of the image in pixels - + Image width Width of the image in pixels - + Image opacity Opacity of the image (0.0 to 1.0) - + Excavate background Remove QR code dots behind the image diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx index 4c55ab2069..a6553fdedd 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -379,7 +379,7 @@ describe("BarcodeGenerator", () => { render(); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "true"); + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); }); it("renders QR code with centered image overlay", () => { @@ -825,8 +825,8 @@ describe("BarcodeGenerator", () => { render(); - expect(screen.getByText(/Barcode Error:/)).toBeInTheDocument(); - expect(screen.getByText(/Invalid barcode value/)).toBeInTheDocument(); + expect(screen.getByText(/Unable to generate barcode/)).toBeInTheDocument(); + expect(screen.getByRole("alert")).toBeInTheDocument(); }); it("renders alert role for error message", () => { @@ -858,7 +858,7 @@ describe("BarcodeGenerator", () => { const { unmount } = render(); - expect(screen.getByText(/Barcode Error:/)).toBeInTheDocument(); + expect(screen.getByText(/Unable to generate barcode/)).toBeInTheDocument(); // Clean up first render to avoid duplicate DOM unmount(); @@ -873,7 +873,7 @@ describe("BarcodeGenerator", () => { render(); - expect(screen.queryByText(/Barcode Error:/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Unable to generate barcode/)).not.toBeInTheDocument(); }); }); @@ -984,7 +984,7 @@ describe("BarcodeGenerator", () => { expect(screen.getByText("Secure QR")).toBeInTheDocument(); expect(screen.getByText("Save QR")).toBeInTheDocument(); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "true"); + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); }); it("renders barcode with all advanced options enabled", () => { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx index df05d44bc7..6e1433b3b4 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx @@ -22,18 +22,18 @@ export function BarcodePreview(props: BarcodePreviewProps): ReactElement { return (
{restProps.buttonPosition === "top" && downloadButton} -
- {imageUrl ? ( - Barcode preview - ) : ( -
Barcode format not supported
- )} -
+ {imageUrl ? ( + Barcode preview + ) : ( +
+ Barcode format not supported +
+ )} {restProps.buttonPosition === "bottom" && downloadButton}
); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx index ac8a22ef04..ccd9faedbb 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx @@ -14,59 +14,55 @@ export function QRCodePreview(props: QRCodePreviewProps): ReactElement { // Note: qrMargin is in module units (QR grid cells), not pixels // The QRCodeSVG component handles margin internally within the specified size const displaySize = Math.min(qrSize, 400); // Clamped to 400px for preview - const qrImageWidth = restProps.qrImageWidth ?? 32; - const qrImageHeight = restProps.qrImageHeight ?? 32; - const qrImageOpacity = restProps.qrImageOpacity ?? 1; - const qrImageX = restProps.qrImageX ?? 0; - const qrImageY = restProps.qrImageY ?? 0; + const qrOverlayWidth = restProps.qrOverlayWidth ?? 32; + const qrOverlayHeight = restProps.qrOverlayHeight ?? 32; + const qrOverlayOpacity = restProps.qrOverlayOpacity ?? 1; + const qrOverlayX = restProps.qrOverlayX ?? 0; + const qrOverlayY = restProps.qrOverlayY ?? 0; const [imageSrcError, setImageSrcError] = useState(false); - const imageBaseStyle: CSSProperties = restProps.qrImageCenter + const imageBaseStyle: CSSProperties = restProps.qrOverlayCenter ? { left: "50%", top: "50%", transform: "translate(-50%, -50%)", - width: qrImageWidth, - height: qrImageHeight + width: qrOverlayWidth, + height: qrOverlayHeight } : { - left: qrImageX, - top: qrImageY, - width: qrImageWidth, - height: qrImageHeight + left: qrOverlayX, + top: qrOverlayY, + width: qrOverlayWidth, + height: qrOverlayHeight }; return (
{restProps.qrTitle &&

{restProps.qrTitle}

} {restProps.buttonPosition === "top" && downloadButton} -
+ + {restProps.qrOverlay && ( setImageSrcError(true)} + style={{ + ...imageBaseStyle, + opacity: qrOverlayOpacity, + ...(restProps.qrOverlayExcavate && { + backgroundColor: "#ffffff", + outline: "3px solid #ffffff" + }) + }} /> - {restProps.qrImage && ( - <> - {restProps.qrImageExcavate && ( - + )} {restProps.buttonPosition === "bottom" && downloadButton}
); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index a260ddd8c6..4c1b73a60d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -74,15 +74,15 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon level: props.qrLevel ?? "L", downloadButton: downloadButtonConfig, image: - props.qrImageSrc?.status === "available" + props.qrOverlaySrc?.status === "available" ? { - src: props.qrImageSrc.value.uri, - x: props.qrImageX === 0 ? undefined : props.qrImageX, - y: props.qrImageY === 0 ? undefined : props.qrImageY, - height: props.qrImageHeight ?? 24, - width: props.qrImageWidth ?? 24, - opacity: props.qrImageOpacity?.toNumber() ?? 1, - excavate: props.qrImageExcavate ?? true + src: props.qrOverlaySrc.value.uri, + x: props.qrOverlayX === 0 ? undefined : props.qrOverlayX, + y: props.qrOverlayY === 0 ? undefined : props.qrOverlayY, + height: props.qrOverlayHeight ?? 24, + width: props.qrOverlayWidth ?? 24, + opacity: props.qrOverlayOpacity?.toNumber() ?? 1, + excavate: props.qrOverlayExcavate ?? true } : undefined }; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index ee268cd384..1f44c800ee 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -3,6 +3,7 @@ $widget-prefix: "barcode-generator"; .#{$widget-prefix} { display: block; + width: 100%; border-radius: var(--card-border-radius); &--as-card { @@ -14,6 +15,7 @@ $widget-prefix: "barcode-generator"; .qrcode-renderer, .barcode-renderer { + position: relative; display: flex; flex-direction: column; align-items: center; @@ -33,3 +35,26 @@ $widget-prefix: "barcode-generator"; margin: 0; } } + +// Preview graphics for barcode and QR code +.qrcode-preview-image { + max-width: 100%; + width: auto; + height: auto; + display: block; + object-fit: contain; +} + +.barcode-preview-image { + max-width: 100%; + width: 100%; + height: auto; + display: block; + object-fit: contain; +} + +// Overlay image for QR codes (positioned absolutely) +.qrcode-preview-overlay { + position: absolute; + object-fit: contain; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss deleted file mode 100644 index c440718553..0000000000 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss +++ /dev/null @@ -1,44 +0,0 @@ -@use "BarcodeGenerator"; - -.barcode-generator-widget-preview { - width: 100%; - display: inline-block; - - .barcode-generator { - display: flex; - flex-direction: column; - } - - .barcode-preview-graphic { - max-width: 100%; - width: 100%; - height: auto; - display: block; - object-fit: contain; - } - - .barcode-preview-graphic--qr { - width: auto; - } - - .barcode-preview-graphic--barcode { - width: 100%; - } - - .barcode-preview-qr-container { - position: relative; - display: inline-block; - max-width: 100%; - } - - .barcode-preview-qr-image-excavate { - position: absolute; - background-color: #ffffff; - outline: 3px solid #ffffff; - } - - .barcode-preview-qr-image { - position: absolute; - object-fit: contain; - } -} diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 86c3980250..64e0209ccb 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -45,15 +45,15 @@ export interface BarcodeGeneratorContainerProps { qrMargin: number; qrTitle: string; qrLevel: QrLevelEnum; - qrImage: boolean; - qrImageSrc: DynamicValue; - qrImageCenter: boolean; - qrImageX: number; - qrImageY: number; - qrImageHeight: number; - qrImageWidth: number; - qrImageOpacity: Big; - qrImageExcavate: boolean; + qrOverlay: boolean; + qrOverlaySrc: DynamicValue; + qrOverlayCenter: boolean; + qrOverlayX: number; + qrOverlayY: number; + qrOverlayHeight: number; + qrOverlayWidth: number; + qrOverlayOpacity: Big; + qrOverlayExcavate: boolean; } export interface BarcodeGeneratorPreviewProps { @@ -90,13 +90,13 @@ export interface BarcodeGeneratorPreviewProps { qrMargin: number | null; qrTitle: string; qrLevel: QrLevelEnum; - qrImage: boolean; - qrImageSrc: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null; - qrImageCenter: boolean; - qrImageX: number | null; - qrImageY: number | null; - qrImageHeight: number | null; - qrImageWidth: number | null; - qrImageOpacity: number | null; - qrImageExcavate: boolean; + qrOverlay: boolean; + qrOverlaySrc: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null; + qrOverlayCenter: boolean; + qrOverlayX: number | null; + qrOverlayY: number | null; + qrOverlayHeight: number | null; + qrOverlayWidth: number | null; + qrOverlayOpacity: number | null; + qrOverlayExcavate: boolean; } From 5c47b3bff616fcdde3ca301cdd89448fbb887d67 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 26 Feb 2026 17:49:30 +0100 Subject: [PATCH 15/18] fix: implement minor fixes and polish - anchor changed to button - createad DownloadButton component - changed preview max height to 200px - change icon for structure preview - changed dynamic value description to generic - added configurable filename - error state recovery on revalidation --- .../src/BarcodeGenerator.editorConfig.ts | 12 ++---- .../src/BarcodeGenerator.xml | 14 ++++--- .../src/__tests__/BarcodeGenerator.spec.tsx | 2 +- .../src/components/Barcode.tsx | 14 +++---- .../src/components/DownloadButton.tsx | 16 ++++++++ .../src/components/QRCode.tsx | 14 +++---- .../src/components/icons/DownloadIcon.tsx | 2 +- .../src/components/preview/BarcodePreview.tsx | 2 +- .../src/components/preview/QRCodePreview.tsx | 2 +- .../src/config/Barcode.config.ts | 17 +++++++- .../src/hooks/useRenderBarcode.ts | 6 +++ .../src/ui/BarcodeGenerator.scss | 40 +++++++++++++++++++ .../typings/BarcodeGeneratorProps.d.ts | 2 + 13 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 144aaf29f3..0af9c4962f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -2,7 +2,6 @@ import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/st import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; import { validateAddonValue, validateBarcodeValue } from "./config/validation"; -import structurePreviewSvg from "./assets/structurePreview.svg"; export type Problem = { property?: string; // key of the property, at which the problem exists @@ -98,13 +97,10 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope return defaultProperties; } -export function getPreview(_: StructurePreviewProps): StructurePreviewProps | null { - return { - type: "Image", - document: decodeURIComponent(structurePreviewSvg.replace("data:image/svg+xml,", "")), - height: 275, - width: 275 - }; +export function getPreview(_: StructurePreviewProps, _isDarkMode: boolean): StructurePreviewProps | null { + // Return null to use the widget icon (BarcodeGenerator.icon.png or BarcodeGenerator.icon.dark.png) + // based on the user's theme settings + return null; } export function check(_values: BarcodeGeneratorPreviewProps): Problem[] { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index ede58f677c..7291f849d5 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -10,7 +10,7 @@ Dynamic value - String to encode in the QR code + String to encode as a barcode or QR code @@ -27,7 +27,7 @@ Adds a download button - Download button text + Button text Download @@ -35,13 +35,17 @@ - Download button aria-label + Button aria-label Download code as file Sla code op als bestand + + File name + Custom filename for the downloaded file (without extension). If empty, generates automatically based on format and value. + Button position Position of the download button relative to the barcode @@ -123,7 +127,7 @@ Code height - Height of the barcode. Note: In preview, the max height is 400px. The barcode will render at full height in your application. + Height of the barcode. Note: In preview, the max height is 200px. The barcode will render at full height in your application. Margin size @@ -131,7 +135,7 @@ QR Size - The size of the QR box. Note: In preview, the max height is 400px. The QR code will render at full size in your application. + The size of the QR box. Note: In preview, the max height is 200px. The QR code will render at full size in your application. Margin size diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx index a6553fdedd..86ee9225dd 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -518,7 +518,7 @@ describe("BarcodeGenerator", () => { const children = Array.from((renderer as HTMLElement).children); // Download button should be first child const firstChild = children[0] as HTMLElement; - expect(firstChild).toHaveClass("mx-link"); + expect(firstChild).toHaveClass("barcode-generator-download-button"); }); it("renders download button at bottom position", () => { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index a5f25ae164..a9bb5ff9da 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -1,7 +1,7 @@ import { useRenderBarcode } from "../hooks/useRenderBarcode"; import { downloadCode } from "../utils/download-code"; import { BarcodeTypeConfig } from "../config/Barcode.config"; -import { DownloadIcon } from "./icons/DownloadIcon"; +import { DownloadButton } from "./DownloadButton"; import { ReactElement } from "react"; @@ -26,15 +26,11 @@ export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement } const button = downloadButton && ( -
downloadCode(ref, config.type, downloadButton.fileName)} - > - {downloadButton.caption} - + ariaLabel={downloadButton.label} + caption={downloadButton.caption} + /> ); return ( diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx new file mode 100644 index 0000000000..fdfd79a018 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx @@ -0,0 +1,16 @@ +import { ReactElement } from "react"; +import { DownloadIcon } from "./icons/DownloadIcon"; + +interface DownloadButtonProps { + onClick: () => void; + ariaLabel?: string; + caption?: string; +} + +export function DownloadButton({ onClick, ariaLabel, caption }: DownloadButtonProps): ReactElement { + return ( + + ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index c03acfa7fc..55999d731f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -1,7 +1,7 @@ import { QRCodeSVG } from "qrcode.react"; import { ReactElement, useRef } from "react"; import { downloadCode } from "../utils/download-code"; -import { DownloadIcon } from "./icons/DownloadIcon"; +import { DownloadButton } from "./DownloadButton"; import { QRCodeTypeConfig } from "../config/Barcode.config"; interface QRCodeRendererProps { @@ -15,15 +15,11 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; const button = downloadButton && ( - downloadCode(ref, config.type, downloadButton.fileName)} - > - {downloadButton.caption} - + ariaLabel={downloadButton.label} + caption={downloadButton.caption} + /> ); return ( diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx index be0d8ea08e..f2acb48537 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx @@ -6,7 +6,7 @@ export function DownloadIcon(): ReactElement { No barcode value provided; + return {props.emptyMessage?.value || "No barcode value provided"}; } return ( diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 7291f849d5..676cc4bef5 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -22,6 +22,14 @@ Custom + + Empty message + + + No barcode value provided + Geen barcodewaarde opgegeven + + Allow download Adds a download button @@ -111,6 +119,17 @@ Space between main barcode and addon (in pixels) + + + Log Level + Choose the log level for in the case of failure for generating the barcode. Info will display generic error message on the UI and Debug will gives detailed information on the developer console. + + None + Info + Debug + + + diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index a9bb5ff9da..94816d2045 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -17,10 +17,12 @@ export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement if (error) { return (
-
- Unable to generate barcode. Please check the barcode value and format - configuration. -
+ {config.logLevel !== "None" && ( +
+ Unable to generate barcode. Please check the barcode value and format + configuration. +
+ )}
); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index 1d4d15fb03..5aecb12775 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -7,16 +7,21 @@ interface DownloadButtonConfig { buttonPosition: "top" | "bottom"; } -/** Configuration for barcode (non-QR) rendering */ -export interface BarcodeTypeConfig { - type: "barcode"; +type codeType = "barcode" | "qrcode"; + +export interface CodeBaseTypeConfig extends Pick { + type: T; codeValue: string; + margin: number; + downloadButton?: DownloadButtonConfig; +} + +/** Configuration for barcode (non-QR) rendering */ +export interface BarcodeTypeConfig extends CodeBaseTypeConfig<"barcode"> { width: number; height: number; format: string; - margin: number; displayValue: boolean; - downloadButton?: DownloadButtonConfig; // Advanced barcode options enableEan128: boolean; @@ -29,14 +34,10 @@ export interface BarcodeTypeConfig { } /** Configuration for QR code rendering */ -export interface QRCodeTypeConfig { - type: "qrcode"; - codeValue: string; +export interface QRCodeTypeConfig extends CodeBaseTypeConfig<"qrcode"> { size: number; - margin: number; title: string; level: QrLevelEnum; - downloadButton?: DownloadButtonConfig; image?: { src: string; x: number | undefined; @@ -63,15 +64,21 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon } : undefined; + const baseConfig: CodeBaseTypeConfig = { + type: format === "QRCode" ? "qrcode" : "barcode", + codeValue, + margin: props.codeMargin ?? 2, + logLevel: props.logLevel, + downloadButton: downloadButtonConfig + }; + if (format === "QRCode") { return { + ...baseConfig, type: "qrcode", - codeValue, size: props.qrSize ?? 128, - margin: props.qrMargin ?? 2, title: props.qrTitle ?? "", level: props.qrLevel ?? "L", - downloadButton: downloadButtonConfig, image: props.qrOverlaySrc?.status === "available" ? { @@ -88,14 +95,12 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon } return { + ...baseConfig, type: "barcode", - codeValue, width: props.codeWidth ?? 128, height: props.codeHeight ?? 128, format, - margin: props.codeMargin ?? 2, displayValue: props.displayValue ?? false, - downloadButton: downloadButtonConfig, // Advanced barcode options enableEan128: props.enableEan128 ?? false, diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts index cefb142de3..0ef6797816 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -2,6 +2,13 @@ import { BarcodeTypeConfig } from "../config/Barcode.config"; import { RefObject, useEffect, useRef, useState } from "react"; import { type BarcodeRenderOptions, renderBarcode } from "../utils/barcodeRenderer-utils"; import { validateAddonValue, validateBarcodeValue } from "../config/validation"; +import { LogLevelEnum } from "../../typings/BarcodeGeneratorProps"; + +function printError(message: string, logLevel: LogLevelEnum) { + if (logLevel === "Debug") { + console.error(`[Barcode Generator] ${message}`); + } +} export const useRenderBarcode = ( config: BarcodeTypeConfig @@ -35,10 +42,10 @@ export const useRenderBarcode = ( if (!validationResult.valid) { const errorMsg = validationResult.message || "Invalid barcode value"; // Log detailed error for developers - console.error( - `[Barcode Generator] Validation failed for format "${format}":`, - errorMsg, - `\nProvided value: "${value}"` + + printError( + `Validation failed for format "${format}": ${errorMsg} \nProvided value: "${value}"`, + config.logLevel ); setError(true); return; @@ -50,10 +57,9 @@ export const useRenderBarcode = ( if (!addonResult.valid) { const errorMsg = addonResult.message || "Invalid addon value"; // Log detailed error for developers - console.error( - `[Barcode Generator] Addon validation failed for format "${addonFormat}":`, - errorMsg, - `\nProvided addon value: "${addonValue}"` + printError( + `Addon validation failed for format "${addonFormat}": ${errorMsg} \nProvided addon value: "${addonValue}"`, + config.logLevel ); setError(true); return; @@ -82,13 +88,7 @@ export const useRenderBarcode = ( } catch (error) { const errorMsg = error instanceof Error ? error.message : "Error generating barcode"; // Log detailed error for developers - console.error( - `[Barcode Generator] Rendering failed:`, - errorMsg, - `\nFormat: "${format}"`, - `\nValue: "${value}"`, - error - ); + printError(`Rendering failed: ${errorMsg} \nFormat: "${format}" \nValue: "${value}"`, config.logLevel); setError(true); } } else if (!value) { diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index ca0ee33637..941382fc36 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -15,6 +15,8 @@ export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "UPC" | "CODE3 export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; +export type LogLevelEnum = "None" | "Info" | "Debug"; + export type QrLevelEnum = "L" | "M" | "Q" | "H"; export interface BarcodeGeneratorContainerProps { @@ -24,6 +26,7 @@ export interface BarcodeGeneratorContainerProps { tabIndex?: number; codeValue: DynamicValue; codeFormat: CodeFormatEnum; + emptyMessage?: DynamicValue; allowDownload: boolean; downloadButtonCaption?: DynamicValue; downloadButtonAriaLabel?: DynamicValue; @@ -37,6 +40,7 @@ export interface BarcodeGeneratorContainerProps { addonFormat: AddonFormatEnum; addonValue: DynamicValue; addonSpacing: number; + logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; codeWidth: number; @@ -70,6 +74,7 @@ export interface BarcodeGeneratorPreviewProps { translate: (text: string) => string; codeValue: string; codeFormat: CodeFormatEnum; + emptyMessage: string; allowDownload: boolean; downloadButtonCaption: string; downloadButtonAriaLabel: string; @@ -83,6 +88,7 @@ export interface BarcodeGeneratorPreviewProps { addonFormat: AddonFormatEnum; addonValue: string; addonSpacing: number | null; + logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; codeWidth: number | null; From a9cff84d242623284b8a89e83a76085a6c6e81cc Mon Sep 17 00:00:00 2001 From: gjulivan Date: Thu, 12 Mar 2026 11:46:30 +0100 Subject: [PATCH 17/18] fix: code cleanup and update --- .../src/BarcodeGenerator.editorConfig.ts | 4 ++-- .../src/BarcodeGenerator.xml | 2 +- .../src/components/QRCode.tsx | 4 ++-- .../src/config/Barcode.config.ts | 20 ++++++++++++------- .../src/config/validation.ts | 20 ++++++++++++------- .../src/hooks/useRenderBarcode.ts | 15 +++++--------- .../src/utils/barcodeRenderer-utils.ts | 5 +++-- .../src/utils/helpers.ts | 7 +++++++ 8 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/utils/helpers.ts diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 5633fe1b52..c68c23ec0c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -1,6 +1,6 @@ import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; -import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; +import { BarcodeGeneratorPreviewProps, CodeFormatEnum, CustomCodeFormatEnum } from "../typings/BarcodeGeneratorProps"; import { validateAddonValue, validateBarcodeValue } from "./config/validation"; export type Problem = { @@ -144,7 +144,7 @@ export function check(_values: BarcodeGeneratorPreviewProps): Problem[] { return errors.concat(valueProblems); } -function getActiveFormat(values: BarcodeGeneratorPreviewProps): string { +function getActiveFormat(values: BarcodeGeneratorPreviewProps): CodeFormatEnum | CustomCodeFormatEnum { if (values.codeFormat === "Custom") { return values.customCodeFormat || "CODE128"; } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 676cc4bef5..7b7f49764c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -176,7 +176,7 @@
Overlay image - Include an image on top the QR code + Include an image overlay on the QR code Image source diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index 55999d731f..ac95a395bd 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -11,7 +11,7 @@ interface QRCodeRendererProps { export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { codeValue, downloadButton, size, margin, title, level, image } = config; + const { codeValue, downloadButton, size, margin, title, level, overlay } = config; const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; const button = downloadButton && ( @@ -33,7 +33,7 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { level={level} marginSize={margin} title={title} - imageSettings={image} + imageSettings={overlay} /> {buttonPosition === "bottom" && button}
diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index 5aecb12775..ac1caf1e6e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -1,4 +1,10 @@ -import { BarcodeGeneratorContainerProps, QrLevelEnum } from "../../typings/BarcodeGeneratorProps"; +import { + AddonFormatEnum, + BarcodeGeneratorContainerProps, + CodeFormatEnum, + CustomCodeFormatEnum, + QrLevelEnum +} from "../../typings/BarcodeGeneratorProps"; interface DownloadButtonConfig { caption?: string; @@ -7,9 +13,9 @@ interface DownloadButtonConfig { buttonPosition: "top" | "bottom"; } -type codeType = "barcode" | "qrcode"; +type CodeType = "barcode" | "qrcode"; -export interface CodeBaseTypeConfig extends Pick { +export interface CodeBaseTypeConfig extends Pick { type: T; codeValue: string; margin: number; @@ -20,7 +26,7 @@ export interface CodeBaseTypeConfig extends Pick { width: number; height: number; - format: string; + format: CodeFormatEnum | CustomCodeFormatEnum; displayValue: boolean; // Advanced barcode options @@ -29,7 +35,7 @@ export interface BarcodeTypeConfig extends CodeBaseTypeConfig<"barcode"> { lastChar: string; enableMod43: boolean; addonValue: string; - addonFormat: string; + addonFormat: AddonFormatEnum | null | undefined; addonSpacing: number; } @@ -38,7 +44,7 @@ export interface QRCodeTypeConfig extends CodeBaseTypeConfig<"qrcode"> { size: number; title: string; level: QrLevelEnum; - image?: { + overlay?: { src: string; x: number | undefined; y: number | undefined; @@ -79,7 +85,7 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon size: props.qrSize ?? 128, title: props.qrTitle ?? "", level: props.qrLevel ?? "L", - image: + overlay: props.qrOverlaySrc?.status === "available" ? { src: props.qrOverlaySrc.value.uri, diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts index 14b9538fd7..bcca55073d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts @@ -1,11 +1,17 @@ -export type ValidationResult = { - valid: boolean; - // `message` is a plain, non-localized message shown in Studio if validation fails. - message?: string; -}; +import { AddonFormatEnum, CodeFormatEnum, CustomCodeFormatEnum } from "../../typings/BarcodeGeneratorProps"; + +export type ValidationResult = + | { + valid: true; + } + | { + valid: false; + // `message` is a plain, non-localized message shown in Studio if validation fails. + message: string; + }; /** Validate barcode value for a given format. */ -export function validateBarcodeValue(format: string, value: string): ValidationResult { +export function validateBarcodeValue(format: CustomCodeFormatEnum | CodeFormatEnum, value: string): ValidationResult { // If no value is present at design time, assume dynamic binding will provide it at runtime. if (!value) { return { valid: true }; @@ -135,7 +141,7 @@ export function validateBarcodeValue(format: string, value: string): ValidationR } /** Validate addon (EAN-5 / EAN-2) values. */ -export function validateAddonValue(addonFormat: string | null | undefined, value: string): ValidationResult { +export function validateAddonValue(addonFormat: AddonFormatEnum | null | undefined, value: string): ValidationResult { if (!addonFormat || addonFormat === "None") { return { valid: true }; } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts index 0ef6797816..76fda107a1 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -2,13 +2,7 @@ import { BarcodeTypeConfig } from "../config/Barcode.config"; import { RefObject, useEffect, useRef, useState } from "react"; import { type BarcodeRenderOptions, renderBarcode } from "../utils/barcodeRenderer-utils"; import { validateAddonValue, validateBarcodeValue } from "../config/validation"; -import { LogLevelEnum } from "../../typings/BarcodeGeneratorProps"; - -function printError(message: string, logLevel: LogLevelEnum) { - if (logLevel === "Debug") { - console.error(`[Barcode Generator] ${message}`); - } -} +import { printError } from "../utils/helpers"; export const useRenderBarcode = ( config: BarcodeTypeConfig @@ -40,7 +34,7 @@ export const useRenderBarcode = ( // Validate barcode value at runtime const validationResult = validateBarcodeValue(format, value); if (!validationResult.valid) { - const errorMsg = validationResult.message || "Invalid barcode value"; + const errorMsg = validationResult.message; // Log detailed error for developers printError( @@ -55,7 +49,7 @@ export const useRenderBarcode = ( if (addonValue && addonFormat && addonFormat !== "None") { const addonResult = validateAddonValue(addonFormat, addonValue); if (!addonResult.valid) { - const errorMsg = addonResult.message || "Invalid addon value"; + const errorMsg = addonResult.message; // Log detailed error for developers printError( `Addon validation failed for format "${addonFormat}": ${errorMsg} \nProvided addon value: "${addonValue}"`, @@ -108,7 +102,8 @@ export const useRenderBarcode = ( enableMod43, addonValue, addonFormat, - addonSpacing + addonSpacing, + config.logLevel ]); return { ref, error }; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts index 1830366147..0e8fcef61b 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts @@ -1,5 +1,6 @@ import JsBarcode from "jsbarcode"; import { type ForwardedRef } from "react"; +import { AddonFormatEnum } from "typings/BarcodeGeneratorProps"; interface BarcodeMethodOptions { width?: number; @@ -42,7 +43,7 @@ export interface BarcodeRenderOptions { lastChar?: string; mod43?: boolean; addonValue?: string; - addonFormat?: string; + addonFormat?: AddonFormatEnum | null | undefined; addonSpacing?: number; } @@ -54,7 +55,7 @@ export const createBarcodeWithAddon = ( value: string, mainFormat: string, addonValue: string, - addonFormat: string, + addonFormat: AddonFormatEnum, options: BarcodeOptions, addonSpacing: number ): void => { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/helpers.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/helpers.ts new file mode 100644 index 0000000000..f4f6c5f27e --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/helpers.ts @@ -0,0 +1,7 @@ +import { LogLevelEnum } from "../../typings/BarcodeGeneratorProps"; + +export function printError(message: string, logLevel: LogLevelEnum) { + if (logLevel === "Debug") { + console.error(`[Barcode Generator] ${message}`); + } +} From 296c529d02c611f376c404e22c81443528c569e2 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Thu, 12 Mar 2026 13:29:31 +0100 Subject: [PATCH 18/18] fix: rearrange configs --- .../src/BarcodeGenerator.editorConfig.ts | 9 +- .../src/BarcodeGenerator.xml | 170 ++++++++++-------- .../src/components/QRCode.tsx | 2 +- .../src/config/Barcode.config.ts | 4 +- .../typings/BarcodeGeneratorProps.d.ts | 16 +- 5 files changed, 113 insertions(+), 88 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index c68c23ec0c..b7bbccc7e9 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -16,7 +16,14 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope if (values.codeFormat === "QRCode") { hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); } else { - hidePropertiesIn(defaultProperties, values, ["qrOverlay", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); + hidePropertiesIn(defaultProperties, values, [ + "qrOverlay", + "qrSize", + "qrMargin", + "qrLevel", + "qrTitle", + "showTitle" + ]); } if (values.codeFormat !== "QRCode" || !values.qrOverlay) { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 7b7f49764c..7aaec31075 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -119,6 +119,22 @@ Space between main barcode and addon (in pixels) + + + Level + The Error Correction Level to use + + L + M + Q + H + + + + QR Size + The size of the QR box. Note: In preview, the max height is 200px. The QR code will render at full size in your application. + + Log Level @@ -132,84 +148,82 @@ - - Display value - Display the value below the code - - - Show as card - Display the widget with a border, background and padding - - - Bar width - Width of a single bar - - - Code height - Height of the barcode. Note: In preview, the max height is 200px. The barcode will render at full height in your application. - - - Margin size - In pixels - - - QR Size - The size of the QR box. Note: In preview, the max height is 200px. The QR code will render at full size in your application. - - - Margin size - Number of module units (QR grid cells) to use for margin. Increasing compresses the QR pattern within the fixed size. Note: not visible in preview. - - - Title - Used for accessibility reasons - - - Level - The Error Correction Level to use - - L - M - Q - H - - - - Overlay image - Include an image overlay on the QR code - - - Image source - URL or path to the image to display on the QR code - - - Center image - Center the image in the QR code - - - Image X position - Horizontal position of the image - - - Image Y position - Vertical position of the image - - - Image height - Height of the image in pixels - - - Image width - Width of the image in pixels - - - Image opacity - Opacity of the image (0.0 to 1.0) - - - Excavate background - Remove QR code dots behind the image - + + + Display value + Display the value below the code + + + Show as card + Display the widget with a border, background and padding + + + Bar width + Width of a single bar + + + Code height + Height of the barcode. Note: In preview, the max height is 200px. The barcode will render at full height in your application. + + + Margin size + In pixels + + + Margin size + Number of module units (QR grid cells) to use for margin. Increasing compresses the QR pattern within the fixed size. Note: not visible in preview. + + + Title + Used for accessibility + + QR Code + QR-Code + + + + Show title + Display title on top of QR Code + + + Overlay image + Include an image overlay on the QR code + + + + + Image source + URL or path to the image to display on the QR code + + + Center image + Center the image in the QR code + + + Image X position + Horizontal position of the image + + + Image Y position + Vertical position of the image + + + Image height + Height of the image in pixels + + + Image width + Width of the image in pixels + + + Image opacity + Opacity of the image (0.0 to 1.0) + + + Excavate background + Remove QR code dots behind the image + + diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index ac95a395bd..4b9df98b8e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -24,7 +24,7 @@ export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { return (
- {title &&

{title}

} + {config.showTitle &&

{title}

} {buttonPosition === "top" && button} { export interface QRCodeTypeConfig extends CodeBaseTypeConfig<"qrcode"> { size: number; title: string; + showTitle: boolean; level: QrLevelEnum; overlay?: { src: string; @@ -83,7 +84,8 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon ...baseConfig, type: "qrcode", size: props.qrSize ?? 128, - title: props.qrTitle ?? "", + showTitle: props.showTitle, + title: props.qrTitle.status === "available" ? props.qrTitle.value : "QR Code", level: props.qrLevel ?? "L", overlay: props.qrOverlaySrc?.status === "available" diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 941382fc36..45a5d32e89 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -15,10 +15,10 @@ export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "UPC" | "CODE3 export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; -export type LogLevelEnum = "None" | "Info" | "Debug"; - export type QrLevelEnum = "L" | "M" | "Q" | "H"; +export type LogLevelEnum = "None" | "Info" | "Debug"; + export interface BarcodeGeneratorContainerProps { name: string; class: string; @@ -40,16 +40,17 @@ export interface BarcodeGeneratorContainerProps { addonFormat: AddonFormatEnum; addonValue: DynamicValue; addonSpacing: number; + qrLevel: QrLevelEnum; + qrSize: number; logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; codeWidth: number; codeHeight: number; codeMargin: number; - qrSize: number; qrMargin: number; - qrTitle: string; - qrLevel: QrLevelEnum; + qrTitle: DynamicValue; + showTitle: boolean; qrOverlay: boolean; qrOverlaySrc: DynamicValue; qrOverlayCenter: boolean; @@ -88,16 +89,17 @@ export interface BarcodeGeneratorPreviewProps { addonFormat: AddonFormatEnum; addonValue: string; addonSpacing: number | null; + qrLevel: QrLevelEnum; + qrSize: number | null; logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; codeWidth: number | null; codeHeight: number | null; codeMargin: number | null; - qrSize: number | null; qrMargin: number | null; qrTitle: string; - qrLevel: QrLevelEnum; + showTitle: boolean; qrOverlay: boolean; qrOverlaySrc: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null; qrOverlayCenter: boolean;