From 152ed63d131b6986135be97b6033d2e1ae00e60f Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 5 Feb 2026 16:44:29 +0100 Subject: [PATCH 01/14] 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 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 P7dmsw>}1h#r+fbcq=_I` literal 2053 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K54sfsm$=7f9rU5CIbVpxD28NCO+288s%Ds3`13fB;wb|85r2Ud%8G=R4~51YuG34F2EYF zR`Gy)iP1{Gr&BunU$}f~yVbPtV#3WI^*7E>ly(jc-T$agM>HUQ-;aIx0R@K! zT#O3Q#_gNCZRGmp)7S6({Q0qkAMd*w`_2EO625=`F2DcT$0-br;bH;|>-WT#D;Fp! zF!(jtsC_ua6u&<1T>7)sNpbgeK4iUrzPf(-L-U4b<;!n!=m1rBH%?W*prgPrYkRU8 zQ#UKifuw||oF?2H4bR@r=sl3c$hbiwqLS$>6Vrl9>onyJ5e^JGY+>^pHa9S2-0P#2 z5qkCN57M9ieEq)O{_A_5KcC;8_pj~$|K|Ujs>|u6e{bf`TmLQoZE@+m%K4V}YwEWC zTT}k^{P};I@72_m{mc2k`Pb*2_ZgOT7&rE^upDqZT^7m|?7-k7n9d~4$X- zWep57l+Ll32?;Pr>)sCK^kHN)aGk@MqoTlI7PW1)&WjTR->lEKsg}( z{JScdRlT6705l+B)PSKD1~sn z{QUmfdw0$UpU+RfD%XFU{R6Os{eLFX_dKUSgmS^x=U3euOsfN?9?%6AD!SKFJeab9 z45{!?&O1&H40n=z+f+8VIWVj#3>_FHpfdA!IN#nY;Z1w%kKSxwZxelZP1foItM|@d zZxzqi@SW}cdNa-Xzuv4-J4+u|tp8TGFXfH$0aZPHwGUBDOb@nxduhg^$NA@+JHz~m v`V*@@o@LpyKYyFSV<9|s5=831{s(!7vdG@3r5v2m=DSkI zNDVpOEN4&1_a?ZB+V{&X>l#%KV^8aRsIu_9oWY@ya(X!0^h59hB`jlnJ>(Ri?Ek`_ ziIQyGiF<1~gS)c)mor4R3NobV^#;r+$;P)s?E6MP1+BNMmw1ZmXIMoiy^g;`G)FHj z?c29nCG1*6YwL%b^_9t}ZRR>+ zTd|&|>bF%-jHKY^<>#^kC({?q73eU6fS8`K8$S;9b8yqWv#~5<9&r5}w_1n2oNFr| zG3#4!?o^!kDb=d2H0pWZly2sEhR zP^PK3j^zcHGHOqccXjg*MJh~o^D>Hhk#b1BK# zU?b}$YF3N6U`Iqd*s$nqQ54G=j-$GaZ9U$PZI9lMT$;xvTrZ8>gjZMWl(Is zF<43SmRvR`RVX+iAb0bPi$?;P5D-HbPRN%G%>evcra*{KSC4*a#EPtG<>6uCt9m*Ks1C z!oR$2IGppiO7E@DyaMz5)aR4+K#g(mWF=|4BHX86GF^HE;moT2)`?o=k|SLjS^R@< zG0Sy6cjTJ(sW*H^`*E^ADQPZ_waoHTITcVK&_#ub-VIz`$}TIUb_rL*Pxl(IDhUG> zH9e0OeIkzM7e`fj*5w%g5hw(DmenfD)$n&67q6W0nYAccr=B!mdd4_95_CBeQEO){ zRpSs@3HG|F4XH_tHmsUUcyu_&E^X+13!&jZFe9V{<|tMS=hBmGSJHa2Ck5S#O^Pep zaW1ofY$TZ8Z?CEUPCBGlCI8;6Hhz@qD*3|J438BoJ}ShP&r#&Cd>j8(Cd+Wa)z#{& zDCy)n>Z`90IN4aR;{p|gcbqw^n|;1Xfvn#>mHnxfvXF-oJG_3PYECMzZ|t35-F`gV z7b}%$6bY$1rRo$#5gc?7a=&0RhswzzYEtybtv2sHhK?i8o;FoUxn%Rv4{WR6Rb>bUthvzk;4vgo+lRHna zC%T@JVFw1sSsXCdirD`<>;;(Z|Cwd`pZWDa69VaNV`zIarem|M6h7S99psVJs{s|J z7*WME-S+R2=Nt*bfYI!T*>$FuL;GS$P15EWyz~SI;GhJP*?y#VH_t9lsYvuU|L$PB z70pjzZBd~;T~5c#y&oG&$rr*%V!lwFrxV9yBFM9C9|33Rj)i=4Z}BL+-nDd zZBhw!-NkNa8Q2tgw1f+Y+kWt9{?J5-$tQ(`^tUe$n{uwK*YL2UK3k^D{azmq+H>k( z@NArAHO|Hd+z73$T8p54mSvO8|QN+PLtICX%Zb>+Va4@QW ztw3kF=nos7(v1;7ZM%tN z)+-<5Y7WJL|I@~vQ`ib4`oXDK%1XZGTU_e6S3 z{xmtax3{zAEOHO{-u>Yf!M=r3j7GfHEEq>qyK%>p+olB{qp@f6N%X0@qk`LMcKIfD z&e@@hgDH6O7tF;MPw5wjZ>xP@I!5=sKDlmfc-tMVY1!7j6AonL6xO8B%bnU}Jh{s$jklUG1ZXhF%yWR(R;4wwQYQ(6D6|sHt_96>@j_k6#Rw?*BhS0gMzz zD=l=sBAivbbM}eNRU6jK_DW(gmy1u*n)b2{{^N14mWJB3;qVycPR-~>u4;2ij|SmS zx9Q!NsrGV>b6;sSyV3)66SCR3=LIaZOZ+zP!-qI9W^CR%uFE#q1*4T0M{_%=m;5z4 zS9L~7rB>)5ACaL_cq%@_9~kv02sdD9X?bf-pn6Yh%7Q(U>*3DG zWq;W1QG;X<$|04dRy|4UiF@^n&-4&dp0DX143Ji9S#z6M_A-W}us})`)xl{t8ZI z=P>Z(-T2kizTG`dC$h;2EOU4&`BT;bX%GdB}KQJK#<5x-_(chm*%m& z2d>t0og=!46%eETJmdG81J@}jB`G<))jISi1DE7;pMUGc(+_DltcM-7CBcS}nw z>Eg!>qDY7Xz!#?8An>&H@ASK0dj0wydP#wMxE|VO{CCug^S=%hAp~8z0Q-@r7!eTL zw*LD?YQZQU_~)aYQfCHV*{SC5X(nxB*SN8>FF zv=?yu#1ZMFfo5-794yc_7nyd5%e-?pCp0fOU4ZYySt~PK*b!h`m&fWpDzv3L)Q&FliCCQ{Xg(JSv~kd_`4z-GirdkXp2i?XL*pET-c z+d#%=v{h{>cZ4$^`&ZIm{Sf9k-SJCItDeh67W0;9O05f$kMpoxJtU9<;a(VVFnthE zpi5PT!GY>vz+`t#IlX8;j+ZAm_mv@m3`uYi|3L!&9sAAz9stQ3( zKL$rmSwQt{1yJu_A}&#~SVC+$J^UBtLbM0^^Xjx^YRVt^GgkqJgwp7FIOBxz# zjzN+_;9N08Ny)WLrsiF3;L4w3!k;hE%6wLK=@C|FzK*=kQoA>s=KvqovHhE#i{XN-Ar zyD~i^Gn?t6ihgWRq&k>{@FvfEb2cu#h9@Yb)bb)$s^>jcnj^q+W4*6`JKnvNU3N`O zvQdAKFsjVMWAcVDL%I1+REUc>4AYR>$xNM`+o`MlI6l5KGH!jb*lLlbyMS6!zxRd^ zC0FcWirs8a>WS>0cVnW*orURk?tAJ)rP%A^m(3eV>j|XuGj=~aIU7DsrO#$(_>h{t z0__wz=iH`}#DyzbGw62J4Su@<1qc2QEREMG~0qC z-7E1PEghdh*5mX*2;dRhALb0QrZ4eZP74+{27kZWv1R5zb0~Z0S~}^X6$4IN81$Mb z4Ri1IOvby^w-}Q9V3eL@>i3AtnEm@9egUChg7q zbc@7RPYH>2=zRm*pq3!Io|LLSu>fyyC?&W8e{Ng4FLBSoKW^^T8A?*tco>m1EZUJ~ zz3}(`opmHm`VV;GSXJ94!NQ@z@l&Yh+V_gbPL;ecz8$``n_K}!i^7yiJO{?$m2TEL zYV_7v`nBRle9m#>xT*r72@QRhr6|MMm$wi%r9gZKW$e_oV~57)*Q%;cRQt?DWjIQ&b`QqF7>u^pPpCs& zSHRylp9O!it&$7PI=7#KY`De=$rXbnMO)pj68^VShxOdM$<6`|gVj#OVK$`dds}Hl z8yA?45L000+Zhl)vJ-TfK^24b*Jo3@x4YOGpMk37C+L-c3I{4Zd)3x;_`NVnUv%5P$oKfh@qFG%mA zywS5E5-_x%(V0G2m;@!7Rk`MVT5lTj%eyU}k5Q;JB3nEzduW(lIBMI0A6oWI5DCN1 zj(^Jp6}%AJ@C#B#geUiU^(W(@W+82-GAW{V!l86SfwsnxiC%+@5s(NNjgIKs z5?a?LZLWz)$0+9wM)eg?tzLgaNl$S-WuT|8zr-ItQ4To^0?Sz*lIXMbu#4E4RN$dA z{$2yeiG*(aT#6WOQ2JUxeRwTBHYrkYI=?HUbN!W~C%;X1HMdmgZ?j@Q-gsYpQnodwA(e$P_A|o3HxF<3jZQ}YM;u- z)_=?>_@z9{QIO&8h7I;D`_{jIw$6vrO7X({rWP-?Nd&KT*q>xJ!B)V2P@Tv^J=nRXiGLHzdqwNagOZP>H(OFlNA zWxDO>?a7{sv2M{{W(ZFZI!%5N5#1tKS)O~;8kb4p3r-nW3V5+$6 z5rJzqJZ2)T{nN(X^gI47tu(AuWui!OU#wBN}a zk5NeDBE{C#G9aWs8V(z49D>|?S)5DtTg$=~<_Pf=?+PyGZ1?QJA^yjnh*?}9rCR;(^Z)Sv7J};J)KM1U^U?ZSsWpl$^u1d9|Agr3l&c6XX&jQ$2v8lXC10M zZlm69XGp<5lo{5RxZKIrd3Y$D#cOv&2RaJt$<#l{CHSPC##9Yv+U9#ax>tZcyTy}- zohcCx_6WjOup192%3Sz2bNli2B*8Nlf+9TW8(JN*rlQ>=g6!KQY~TVJI;#O@^LpZ+ zTRqrF^F8Kd8dUjWZO-p8c5f++BcsrXK08TDuQv}t_97g0>X2eZH*D1$MP>$7f?@oB z)jLp$*J-`R`-9Cpn&Fhq*s9m2`<>5&wbaOA6m?TyysBv~Bb9 z?Z91GdHf5{-TQm3<6gL8Q4you!hk_>5j(&)O@nNo&X-z5LUD&5Il}h#tlQ6M+8!O% zWBk*{j{zU}e+fEa+Tt>T$r`2~&Ntd!D^2#Pdpn(Rb_B+iU6O3$ul(iR01mquWzFGEjQ#DSr2(Ecw?GhAm|9@1a!gsP^mu~@9v$fr=I&k)9Ju-Sy!l}M>AOAd zJM|&8qVAXY2o11oDrM$x7n@TZOy1I$V67^h#@ ztLU>Iq^O?O*adq+(kFoLH-2S80ENtt4%6pk+!F%*0`(#tcwxy+N42r*t7Z!l#61fe}FMHHuwDLyb$51^fA!r?% zK%{}Tl_#5N=%AVoB4sMXDE>*t6LY z`mcj8dh2l88+o4>1SUA^zyuS_;k^dZi`oVkqUXbC(X`d~EVSj7tDEi*UirNn zB;E2OWN{GRkRFLHrd6XYce1(pTkL$1ee00^l3O>TK@ZGPVnA-7T^d2HdD;VtR_g`B zLK~#^9cZEo8vze+f}`mpL~@MsBTmeu4Q~TC!5luk&Aj0g`ZZ+Q$6vGw;wjbK?%x%U zazhQ7yuAFFI1`E+r_Kw|p8Cun2q_1>C%0deeJ4|Yr6zGL0KI;%atE>WVc%Rcl>aJu znKl`(8iZ1M21~qo@j`p*Gx=5t^7C{^RRrGQlNvI5I(6cl7cVP1d^dSQjd*z<0H{qo z+48rS3iq=_X4sv#|E_l+FQ}Z&9{c#)S#3KFjGGWXJG`60xL)n!78d3wq4dnIZ@fyr z=$GfDe%pMDQYN{gqR@`K`x?X4^-5*!(K6p6EI^kCSFLEqZvBn4_E(8tQ$jSAk0R^rE=TSIZtJexb)t`{!Tu%_4ddQwQAX#u;0aIXZEs@(fhlj3(x()tfhLLg82}UMPecEKXo3{*lMXOgt%_NM4t(HNITm@wC1={ zww&=-{}lk(UP1Vr&ww#1HGs1KxZ^_JXROK|)zfB_No-_^4_l#{7X$?ZU*sL-A&3@bKnfOI`YowcGr!KB#82}Tsv^$ zOKW#+IhdYCX6k9D@_*r}6(bYNC_Qr}N|moxP8`{*}4mB{0K z1IvjsRmk&m$V_=aa%SLBE6U(5$DWZOTdG#|sxiVBag!Ig_c!aJK2QgZX%*D34Wl(d zet+a5!4NTch;yPPMUgBhU=6M@%dQ3zivr7t{ruC6_WR=iHqf`q!)Eje&&B#5$V?`9m1|%X z?A8njkl6op62g$fqZ#Lsne+!uJ}!Hq^2MtN$D7ef+QnDPj?cUfwnt&$Koxgt`kHQc zrhfK6)ijv%{)doFTJmDK``!drm?0Wb;WLZg1abj&RP`^bwo|N53sLvXIb1>Xods-3$}g)Etw+Td}6oan!P1j&|1?mQ2+@WH5{{nfAWQU&Fz= zduFsZK}YA17BZ~QZozjSZ8*9uUz49QQ&WJ@yZYN2ia=a=2-r{U2yv5^<{-RzSMxuaw~gym{+xJ~V|P~`2b9Fstl>z^UvP)T@7?k5ly*7T`&E{2|A=9X zr*A`{xn)G)Ha*uG3fjn$W#Z;E*Wg=YeO^2Qd;6N?XBCjX}{~8NU{0gNr z5bT+m zdx(*scQCM^_rzwFtjkIVSh<^p`tNh(SKEfBcL<(sM zTZ^`L`niI!-Bb?a0x;FE%b)B};aTwB(Ws-k^f%3@YWUArvR=>)54!h75)|BVm=VkJ z1>1*e$cc)I$W)zS0}oGyN;V-)htH?<9)Q+Vl*%6M)nC8ALhlBEWSEgv-S28eqM!q3 zCG|3!KCRO<> zEG%toT2L>mWevVjvCbLsZx5q_5t66DQjSd{xjyY|P@6KS@AAI87*^|*uX>N(QVX=T z#iV&VX#4o}j0oY(=Eq#6KGmkU(Pf4mOzFHr579wy9+NJId2+H{x8sABVEyQ*)!N8PXc(|Igil*Imb{Q10T`h^?=!sZcb51EyPbNh?DIe zmyr9xR2n3O0Q!mgc(ee0m(FnaZJgqwFK;bL?dZV)@%R}@zbvh2Xpp5Z9q}fzzSTTPu#j(`fA?fL-uqmD--X#(Fh>goeF?j@BxX|a%bL4K&@ZcY z^?QFVWA`88x$1G3HXSrxF$=sAhZoVBTGl?gHm4{%9V`(v*#Q#930e}R<{N{C*X{~& zx;%YHo$6m6E_gjSEoL$nVqfLJGp>SlDhp1qrh>F%MXGn!6F^$t`V#JVDZq;+5Jo!2 z^$Zgs5&_4TXKK*}&y}P}{_2E3O>PnFk1^FzM%M$z_p*`=f@K?xjLR-*Wi8W}KRGzt zeztHqIp}S*+}|AeQdFM~!M=hXMC4g%w)K^VE*yxLH?0GF|9vth5(%1x;e(B4*85gu zC*D3`lat5|GwQu_w((gJ4=zpe{c|l>uAGFbdr$t&mH$+-5#5_D6|p=e8h!E^le-Yi zHdoY7LgQA|Wp2|6^GPZG25(cP#N+vA4-11v=L}H!_|8<{cxzX$y43Dp%7kB)+tdWH**$B=D zz%kMPSidFtS60~xnQ8AS^?zEs&fS6teJZ@eHn}peXh9q@i@xuOsl~R%;jUT1&O|*Q zrB7UZ5Brw`W5000%Hon5DdydB+H#WgA-z2P?7wU>9^#I}{$81Ia@o0tlfwnqCqK*U zxKQv#s`UcKj(ykbA($m&TH5I$7x~4;q#u@)4m16oWSqEBq>)c|&}CfS88F`YP4ICw z9btNm&Q#$!MBrpffdQL6v)?K5TZHmGXFiy3B^bML{CV8QMDqgeL2usrOr}X0(_(yk zwfvSFIaq#2@Ez=~rXKERwiU60ygPao=~q~&!$iEr@;g##lGdQVnVp?I(UcZCZ=Mpb zs!}LH+nVODcUB|sQP2Lv=!`LM-|(&W?fC0e*Z*>@uKJ5RoDaWbZg&l>J-cTbbfJGz zGttO&W!m}eT*1>ZlSQ3?IQhB1=M?*WY8wJh-;ppWcs(wlNvE8SW&KCiM0hPJ0mWFk_Hdpt+M>%D62Z`)=|)KSev0sR0Xe@D$MOXK`22db5 zz{b24&KvGVs1+R8J(;%kST@IywQr&fP?wQU>FCplHunW6(;B`Sx4^aDCbM6poNjxl zP@b4=BR@6Mu6PI`c?!(z_ysm3*^Z3fIqw*sAYvD;5;f%*|E15g?#@MA%AGn5mP~qu z`X&OiFCohv5Z{J*2`MqfIpz}PG^t4rxCtD zUH@y+Pg@)H>hgbIhq|=V!{w*a7tjL;H5h8gy(snJ!WfGprbak2-Cv?Ui@4Bo4@`yL z{MEZNO_v&d5al<}FJ+kNAeS@nD#lvvH)a%=I4!5(va9ZAu&#Fgr-k>@VWEh0J2cM! zTKC9AhrW}@zl_m|*c!g>XNK&e2sPd8Ir&O=r@fBc|2k{{7|!>k{i?SoIuCU*XZ7_> zbjVxS+PV^X>0)7dDic+#WXr57FR$Yyn1rl0mObtF%Y3nQw?mMjJPGDzl=v)1mhigv zObRmfZctIRgNm^ZA3_oq8OLrd`9J$68tgHbyt8L27kpJVX}y*{h^qqZ(rHTT_y&G2 zTLCiCxo}CeO0gFIDqd^Ka1^nk0~x!x1O*9)b5op~1`0Fks*iLTA)%rEVvji_?ng;O zWu@P*jUTa3w4!m36Q%11%;^cfL~7A+d_*==0j>Nc$PE%c-1WFQ=-kJFQO?nSnCq9l zMfZfKkiz0->p%Kajwk*gN!K%#Vc$tZ@fJL zXa=}4urd8UP<}F@$!wugC2Jb^QKamd2<2Jl#X!T<0~RPVnrhly_{j(vzyG{RyFDF^ zkc6QX#{vJ>u4@tXv|lfvVWO_?Df@`ZeTZKsDz*x$2xBQ@W#vJ%~h2MTd5-czrAV_$l414~j+0s|9D7%D9 zVi*tj${$WFGse@!T;iWc;;uaRmLH@F9ypGQ{%@J|fOms=U^J!9b?%}GF4p_*hZ2W~ zsZYmazfpj5)MO@b|B#h3*|IozPTSaMj4Aa9$f-MWcG&~NarF%>I%-7!Xf{YWf%jIz z&xZB+wnE>F^=ueY%TWmgf3$~FpfGP=)XsN=oCYY_4CU&wFSD>%GLJJ9p-Va_`4%(j z$YLytase4f1)n!MzlR}Kx<^+^%j@cHh7mThWPj3LNCE?rGkJ{P*(7=>weibcJTEH# zvZCT>{TT)O5y$HOif(rhLwvED+YfJey+8i6X{rb9lqw5G-Em^bjpDOO#Tk}0Hunvy z#D|GAiM9KgXJKyKK02#CswpbD&tt-Ib0+DB zX)zTavfgUR<&m~L4x+BVhH1-uvpz44YJ7bZ-@mwilqq4a2Eusz$5CCq$aw1F>uJ|h zcVIN1%$CLpkn@MtBVlKFe(x!=)l{zsm+Ym*t?|5mD*wrkQutK%cmANQNSPR^mjO!b zUF^mxZ>8RV6Bjj;^%kR!)nalKBR4;z@XyR&X4&_tB!TQSujzahzu^p-lPr zF4a+tYqkGEuqht|Z$TTjcQ)oPm*u8{u*q^^Gq&KnG9uN()bJuvb+3ag@$L}z+iyI! zH*W2|BnOu>sp2pXm8FJDZ>MBn&DqAk&ay`RAy5kRY zW(BBUORYs;W2C!qBmWq&hEpQ%eK3Fg29@%~Cx4sfxRudM@wMpd3?Zip)cXByEhUrt z-+FlSam4$aUqIq!^z6y_qw6VjjyIW8kLdQdn~6qHJVY;N_}DO|W+wFvh|Sve88=m- zFW20%RQJ32-wH)dGxS(c%^La9RQ~jK{wxk{I8TWRlLuv8uSk++Ri%_`>7L^>GDsMi z77xbuJWNV->Y#oIPM6d&&hP&^JXoAcx%Z@@YR$kE70}e6T~}jPNU8d6us0^~78KHi zbKYQt#1#s*mdg-w*MPKf7@f3_)_q>|bqqf?4{va67i$uIL^azgkq)q7-lj(S#f@Fr2 zcA{0l+p(V6Ike*|u#{L2eH?wvCErs}jBac9_MVGJ-z}C@7*Nl==EtojYScR$3eHYe z>E6uZ4k(Sip;PMUai~lmrcz?o@V9o@^g;gVPHr#3A>ZI)6u(4w$&O*w4-pMJW>^Q< z!vIHC@X!-S@=V;N?!Z-KS^n^kA3D8H&)%Zsf}9(`Jj7?4%qiX-vj+VIsRWT9RutQ~ z2++uN(0G)HwD3AFbg9OJ7aUaCu7Nr*k!q^|1h_e$p%msJjthem9Wz$6uSE16*vdD) zy2-6}hL4G9?`9P>H%UsX_XE|K2iPq&y$>vyH+y|YE0KgNdf8+&cz=++9a%=U48FY= z^vw*~c1*lWsoFJ&0;@nF5kVw-Q!Y2n-)qDA47lQ#m<8JNe(XOuQkhL5<2%C!{&t^p z013NZLCw9EQb;vE1r{Q3yP>P=m0w8V<^X=`aRztts4FG@AGt#ufK{QNVSj;F|4$)I zvfLC4%xgS1KLtxQUA3f11YMv*+noby8ETkJa-vJn&BmQ1ByJ<&t4kMNKzYJ;BT0H9+tKw zC}thGL1~yq!Mt#NX|MPYjSU^NYp-?yPmg2%qC>a_DTOZgb%Z;!-{tuVN{U7%+Xsrx z@fAn-hdNfi^B2eplPBgdh)dpqEv__zxYIjqn)5{0na; z2*fv7^2&FB;JPx>KEvsG!WCAZt>o+O3{Qf^CXCk;CE^?{6lbKHhH9WSGey6Xf&pr} zx)Lm-=cb|$vr!7HXtUZ4o?aPXp$TEw!APAl!|BjKGS%k-0dY`ic5EbOcJFylPWpH} z-Mp$_fc;ULtKS0tGj;_9ucxXPD4~{ZXaiuak;fBi;q}437jwERl%6C>_u~z`gqjVO zA6ZgTxe943#{je9!d|YJ;qpms+>fC{TLt~t(%amtt4jXSG*4?*_H0#>wC3vx!xT%} zjG0rwz6c}1ehY~ zNG(`+ezc&h+$S((>!=TUSYqR}UkciI;yr zCW2;~aD4#|VC4x2+bfmD@iO0npxU*c2dS4-??|J4^X2B95!7*lB9RefCKi{e-^cgY z45-_k;oRaB1KC@;*!{0)!F|d&bv7RPu50qc{-8Fsk+#{AAeoQhB7%vDPioaeE zNsX=P9IM-z5yFMRu{4I2O7f&Ru83mSobaeeZ*U3T`?X*j8Pe46w8==?svy;Fy%%@b z0`^{1XtlK5-N|ex%;L2KT0FjCXMhIsMzA7whU_g zGHq#`iao0O`dLpM$Y|S};y@x5AARK}2L(24v=amoSkc9Fk5iV$-S$^GXp4tTqdxK^ zR>|lD%^T3#@stwPcQTm>P^b}t$_*_Gjx`8gnGQ?w9RIY2KeW=qilB#h%HS_Iu12>j z@z)#1Wt?EYc9dv`JrX48ZTaMip}{`>;~xYES$a-T6{xb81#*n}7Q{xo>!k|&+hYq0 z3$B&)UP86Svy;R3c)n6q6B)mPDGaSFH!A#K^d0)sFFJUgks zI{x%Zna@bZpe+A0xOR^1R&jK)ooRVrqb~nzxO`v-q$0|1thW%K(?C)!zG|`Yjp6>+ zPjjHJv&CiEFs@bgDW&YN-Vmb??eR4!Tt=7Le9QL-xo9%_`ha6Wp9Go#cH5KYpnora zcRul6;PC}xB$bTcslxB<(bg0rFJ?az5BSygV?A9yKX+fS^$0fOdTIalRWywpTSXPd zm3D!7WQ2`-58v!%Bc$kc=3^I+(~dNQGDppu3=V$+h9^AgYt*yu>ubEhU&;S{t7qjQ zY+4UB7MN3N)0Z+H{}Ob3$qxAN@T+`%+)EFf{Zid=pSmNxH@5+yT`&$#5KgV(6tb7d zE#D=PtE3p@JY8JwyjkyS<~EuVS3dcLfOdv6rxVW>N!0>joCzVaml;uwIRVACw^$sF z>X$%0(fE_JcV|MG8_{|Za8J$~E=)JCFPN%t);w_YKPdkN@y*}m<$GAIRgOehUnu?A z%ZGfl1Ib~6S!k9&`GM0l1U`zHKAgS4-f+F|f~cnledMpd=Y4dK zRFin;o8@&&leKmpTPYTM(0hp;47-~jNf+MVv3lPiriQNu`4WVKAG=U%t2lj33qo{r_HkNJ#-NUot*ER zwJ~hxt4Eisg71i8U}nvg(Uv2E{tn^mAi973 z;`kbO=l)(}TKITp^`73}UU_5&8n=mNW7BO?gMG*$Jw?EXYTqk-)h4LmtC(Z0 zmAU4!t3Q`VBK=fo`w8P)5%)Yx6MN-Q-uPXg4ad9*T8t_7AHa<@wc7)C=*Ndc?Kxb6 zn!N6sFNgKAItH@8+^=cDu6;A$hLI9tOt3656urDS-1)SdNZA9Qya@h;0XLZ|jx~H3 zynihtV}E>Qeul}U+E1MXZ0X(>To5XtoK}>$rZJ^YJ~m$Tx^pyn@Ak1>zkB%gEl015 zSp0|GE3WXq9rZ@pWHPioWBhZ!eI2qONBYB#Z`=FdxgAou=|Ju(8M;#CuLW(i7QQl9 z1QEUmSU8bN1{q3Nf10$5h`(DkN~7_H?`}vekL|9a>AUVKgPE91R8D*C z+57Enc@x63EnW%wNxN)C{(u=rtmUcnYwNc4Rez8tS-D6IqZ)GT^pZH_;LY6>T1RYO zc#LjSg-`f1Jwj~%4L`7X5Iq7E2VMz+Z#wiHJ5o7a$y1B42wqhmylMo3v*{CkCzr8} z{V`ey^qSxc3lGK2qK4@##Tw0cr@s$p2v}Q#N;l?AXLv$^y9at>YV^!7-A_BG3iR6E za0b7%@iyHSd_aMxVb4;jHAvH(kd$Y(C(>Mzv|git{inn-(2d+h01=Qa15)?%RKFw@ zrLeSg2z)aH^wa(RGNiKXVt`Ryn#iuY&t^2$FxoXMKEr147suj1RiQP4jElaES4rA& zk0&tsZ}CT69C&uZG|?-c;LXSr*F_lGSp%;=BrZ^b4QQLW+KR_01a+9K)Ku)i$r7KJ zIJF5D#sp2M`;ho42)j(=;`SO>bC@AwEhHgk(GUSe4q&u^7MlV|MDrwoW0kr%SY y7pY1d`Qsr4iOLU2?`v~f4gmuGFZ{tB+F9;ARjzsQgpm_W{&h|Lt3?`^2mb|%UsJ6B 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 62d13a173bd329c982fdd8b9bc21471c2ec8786c Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 6 Feb 2026 11:08:08 +0100 Subject: [PATCH 03/14] 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 e7c138096e32f0acba13ed138c741a2b00c5ad75 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 12 Feb 2026 13:30:31 +0100 Subject: [PATCH 06/14] 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 f32830a4e29c5472970889f6c1996405b08c17a1 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 13 Feb 2026 14:54:30 +0100 Subject: [PATCH 07/14] 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 f64cd34b5c6f6ddb267789f6f6b41522348214e1 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 13 Feb 2026 16:36:33 +0100 Subject: [PATCH 08/14] 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 883822a6a7c04b5d86df215c94cb4854153c1d65 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 16 Feb 2026 16:53:36 +0100 Subject: [PATCH 09/14] 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 5a86d34e5864afd8cef33efed204934a5dc94bd3 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 16 Feb 2026 17:01:15 +0100 Subject: [PATCH 10/14] 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/14] 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 086c244ea2b2ea4b120876ca35e04a1c69bb56f9 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 17 Feb 2026 14:12:00 +0100 Subject: [PATCH 12/14] 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 91eafaa0fd23790aae4bd19493aa2b97f5c273a1 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 19 Feb 2026 11:58:32 +0100 Subject: [PATCH 13/14] 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 8c43fa63e87c429d6eab295d3e8f9e8709d63e80 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 19 Feb 2026 16:29:13 +0100 Subject: [PATCH 14/14] 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; }