From b43be528e10fbf228d9967c8f7bf2d49de1e32b3 Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 20 Jul 2026 01:23:24 +0800 Subject: [PATCH] feat(sql-library): support nested folders --- .../src/components/layout/SqlLibraryPanel.vue | 38 +++++++++-- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/ja.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + docs/assets/issue-3864-nested-sql-folders.png | Bin 0 -> 19663 bytes packages/app-tests/savedSqlStore.test.ts | 60 ++++++++++++++++++ 10 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 docs/assets/issue-3864-nested-sql-folders.png diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue index 010169ce8..57174ac25 100644 --- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue +++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue @@ -421,13 +421,19 @@ function isFolderExpanded(folderId: string) { async function openNewFolderInput(parentFolderId?: string) { const parent = parentFolderId ? savedSqlStore.allFolders.find((folder) => folder.id === parentFolderId) : undefined; + if (parentFolderId && !parent) return; const connectionId = parent?.connectionId || connectionStore.connections[0]?.id; if (!connectionId) return; if (parent?.id) { collapsedFolders.value = new Set([...collapsedFolders.value].filter((id) => id !== parent.id)); } - const folder = await savedSqlStore.createFolder(connectionId, t("savedSql.newFolderDefault"), parent?.id); - startRenameFolder(folder); + try { + const folder = await savedSqlStore.createFolder(connectionId, t("savedSql.newFolderDefault"), parent?.id); + searchText.value = ""; + startRenameFolder(folder); + } catch (e: any) { + toast(t("savedSql.createFolderFailed", { message: e?.message || String(e) }), 5000); + } } async function openNewQueryInFolder(folder?: SavedSqlFolder) { @@ -892,7 +898,7 @@ const contextMenuItems = computed(() => { ]; } return [ - { label: t("savedSql.newFolder"), action: () => openNewFolderInput(target.id), icon: FolderPlus }, + { label: t("savedSql.newSubfolder"), action: () => openNewFolderInput(target.id), icon: FolderPlus }, { label: t("savedSql.newQuery"), action: () => openNewQueryInFolder(target), icon: FilePlus }, { label: t("sqlLibrary.importIntoFolder"), action: () => importDirectoryIntoLibrary(target), icon: Download }, { label: t("sqlLibrary.exportFolder"), action: () => exportFolderContents(target), icon: Upload }, @@ -1150,7 +1156,7 @@ function showDropInside(targetId: string) { - @@ -1225,6 +1231,18 @@ function showDropInside(targetId: string) { {{ item.item.name }} ({{ folderFileCount(item.item.id) }}) + + +
({{ folderFileCount(row.folder.id) }}) + + +
5&dYtc4|{gc zOiy?1>gww5TB{~fMM)Y3kq{980HDaqNT>k-5ai(U5j-UL^@VPF419xdRg)G2)Jzed zg5OA3>d0CtDgqe6Wq1HYxGezszbfF15PSguPjD+7-H zt4I_0#})t(0mw>-YIs6iZX#q^O1cZ*ytW(vXaN1`9?hfw9wAyNg^h_53>_TsVc3Tw zt|6mgAS$8~89>NIh(qC&U;b@Si)4`Xb}YD95yaYIAH!oor|mr=}7%oP~~ zHwTN4EJ1m4c@tO!i-1Y;tT%dj6LXpbh-MJx#B&3*Re}iY3UdFS_Q+bFNNeKA}O*Xdo8Qf0rB55ZxeG)maBBBv{hjB;|s|7aTz9`B=iaO z12s3cT&D&1`rF;l4Vq?~2y#o%uDTwLejQNTFt>MfXxA8&EBS??-bN;uN3j}rzg=}) zU!9(+I$2wnW>Eao7-TWSsG{DZ$`07r+g@KsY7`a6<;_zqxG!t(j5xe2(}i;J^2US* zEuTNAw66c?F~pPU{5$mx!GQXY0RMt!j|yxcstUJZZBGc09#1hrj3bprANQ6gQ)gHlL?PY@w5Y%u-bbN2Jbfv7+OuU7mXo)b)InDX133V;7Bm zOYl@yr&R{UpsHQ9QLybY&hjVftVRda&%&7xel!3SU6m&MBR*!Gig#6UNR=Uk$f~{r zD718~a5(2<7tLSg6*|c#47*_VmnY>g899Q6fuZ1oj3kX50(w-4lcY06hqu0$#%jyS?9q?9f}0)Y)K$dQ@^xD%jVDI=nPvbNaf(m#;Qho z#;PNDi$JC}3p^2dOEFNSeOSq2Mi-;xtn+L${T6#bhgm^X4I`aIu%;LXA}n8UIB&>k zCCb>(MX{C$nA^|Sp3ix#2-&l6;kRA53PA4n!pz&{+b1~46d`en!KtpX-j&Ly>y^va=+w*FB$Dxla2ulokZ`K(gh z_^BVS``Y(<{z1x%;XZsy0!<1xD_Kghf~9(9A3~Kl?%NeF<45i1OYIrfeyiOKiaR>O zWyT=IcflbEM??R=ncKupy%1`wZ3jt$|5n6}G&O0IL5PxzS+>t*%8l3@4^p)hI)5a? z3sougsAbUhc~Ta6{Yr!av^5t5HhP{Xf7s3S!I3e;e50iwgv;9g_}7x6*y{gr?5~8b zT54b0^)sB&GsIOy{;$Sp^_tJ~xbeqrynj_{NJRcWGOv?-H2?Zfn10E>YxSprz9+j@He{1rm_yv8U;u)G!UTLC?)pu+1 z+NYhZ*jc*T4vTQbYq0CKZQL;Uc8=XA1<|8O=h6au7;JvG(aM4k3s_3LeKO`wFZZRw z?eNdIu7y=LH|7qzICLb;fEW6n_J+uc8bHTkyO{g1mvBWrGk z#SR;a^3OgQke`ra0fGOprRTkx0LAA_onhMd%5@T7H|IZRn}(k_e~|qBZTj;KjU=T~ zSGZurV3-}1J)n)hEQvyAwnEfZ!YN`*dHqI(h|B+u&L8psj>IkD6!SZqXCIQ#^B-lQ z6Hzq8gi4~7ovNexI|_(Gfq9p$U&n&?`Dn$v5Jmdm+YZyGeU8*~Ulu&N|7LUG>;6>R zw@n8|RwY=$pGl}ixw#XS+ytWe>}-ErZp#IN{(EfL;2*x{`W4;}i`wRhVu-NF4Nl^C zS#KYYJ#^(=k6W)~pSNU(cktpAY4VYPK)M{yf?Ldqt-I8gwD28U6hsHgn$g|~oUhue zmHbEJS+XJ{7|h(9VRL@Ia8}A*bIGKyD5RqbT z$)jxk&+@$u?DGvb!|1+;OvZuXpC8wss6vy3B8p5(md58^Sj)?uN8CVB%Ir94M4U)O zuKhyVGlfFZziA_*o=>~s!Apu?H=z##Fud>`M-ZLA2t}1F(xw*ORvNz}a>ywTX!WYd zX&@`p@a%sP^npO@4n#DW*oeTHF@C7fem{{Go|CzCo#KPba}T}^domFI2Z|TG{x$sy zRzNKQIt)vFgOv`0K8=K!QG_$Pe?L}EK+grVU1~vttZrsCT%6F$2|fxR=~a@YrItsS$lyKG?ss#NMCSD4)W97k)og20MpO zy-%~yxiPV6$gSM>)m#QjhuH-!lL#Uz^WBlD8RE4nn}5eI=e62e3)XhSvJ4Bdvp+ls zh;rTz=o(Cr$!Wh`cQ(k*YxlfpMnNNM;H|CZ65J!^E>>zA|2j;0BL2BzYW%i`cY4#1 z`|hx@!iBua$w+IE-=4pl%(&mo(7T|95+`scgQTmnK2cAl&cX9~hXi_ZSfpnvIj>aPnXyE4}%AS?>9$3l~J5#?O`cB8mhjxKIV5 zn(O083!x`JYn#G!2+vulGw}$inu^6NC6lA3Cj=$smUYpt#=5K`EdmM40DluHUBe$y z!C?>ItEn3h&S}FgT}WmKl}j0@;KMGxa|(lJ;B=I3ZRB%11TYD)L;$VXmSz_f-RgyS zl|485l%WV|8IR7C^+fIdcrcAE!g*COQc{!r`h81#4Z z+Hd*kyxhgTU^exvL(?=v#Bo8+xMT5EryRLi3%7<=RAcDksP^NmcEfKED>o0>g+lxX zyb8$5M|Z4lek?>gQ9A<{Mk*2&kNn>+X(pd^40j-oX6fd)150%D__aHiJj3T$D2Y3~PK@;kZBjs_4Jq3U| z-76g#c=E>*$knsf%i_P@(T42gI}F6Iitaz6ukhaVLNzHqUbLUr)Jc?px*>&8v~q?k z(@mZu5EP7lu3A=6QoJY*ug&^_WNDp}uru7gcEzoGyh1|C?a4QL|x*)d}r#wk(~Bm45&h#*h+% z+@9ymo_xQwak1CJ*r<|$VGl9SeWLA%6xvcAC89(OvVKq)G^2dl-)Z68=X# zQ94Z?h<{p6^$f>;kG-p}N;DgxZS;l1qWLERfgU|5U(xy(wKnqKy8=-%_5egLE8-Xk z>|*%&5I zTSG&?#BvVXq|qR3+W+J4L;;6vavhfc`uY~`ie`oWyk(F|GokS3^R%|Dn8nQJ`}XJN z^bKPviWd^6tg*W6^SAA1RBy{&dZZk?663&m+VIWNvc_x`0$Fv|Me+>fI@{Olcv`hQ z6;V4v&IQroj(MeTmzO{MJ96&M>rgrS2BIb)#N$t31X*iQ-7EL+VNmmdlx`x&XNtEc z6Ct#F~~)SdU)C>D!Ah49de90Y&?YxCB%sBhZ+xxrxyR$uRdvt{j-Q`NT`13AK-Ds z<3etYfn^>Oh!o%8fk_K0Z(<19UYn?hC2M{wKy4JcyvLHY* zeMd=@#NQQrHSx&IY6{xf=^4Ffg@((LvX(Ap-TRs;b!az+=vH&BSWQ$%nSi|{ieH%A z)sPfjH%DK4(z7NB;j`v+U<>n=ZvgoDlOf8l4~Yi#(dXqSz5>;xd9t-bU5n3e?7yFajZtNlqNL7*LFwaJWpobOAIPwkikww%%=@W1& zT?PfQWBn-B+M?(54S`!s60jbQ+7OXkj(Uk>kYb3I0+0z2g;QJNb4Gy3Qx29Uj5*{o z#cfGyJd|KVF*JM=3b71EYNa^UwlobH;9dofi)8jpA#%VU84DZsdn6K5iX}g;h^{|k zRUA{cnO72I!`lB&(C+yf86gS^UuI$MFspJFF$S4t)1I=91x7-|qTHa{#Z%R2n6P$n z(_1TR7K~fl3h+Sa0HxLf3x2`+88INd(?Bxr8hr_4*bzPlhG$45|0v9~@ayWd#=sf{ zFfgf%Eg>{*!Ji{J42LMg40M*uRhsU(JVh~_4A>!E37^S&npJOI+HG5(xnP?z{*(#+&Qk9|SY zsa|OhWcr~Km<5ah{e#YMwM5{O%e%rcr${yB;Nyz693hN!5da5d@&plp9T72_h`*8x z1^JE)bRp;E@_Z_dZ;*#|*qzk}JO6N0*SVObid15p)Q^(FC>8_b8WN)9i=PHrPUZ{CS6$^&d*5UO z_GXoL=uuEm;~F&9ad9F>M(!YQu3La-{fS>6OmNlFu0=x{)!0;Eh5 znUg`^fcydv`30hyJX#upuecSZOkB^5C`(;8`RRC_Iq*3_kz z(3cb}eIkgv5?F3u9~PUj8G%AWqf$J&u(!y(h~q~Ld+w zeOu7;!NgQc)lhJfX<^udr;v6M#Y5%Q4|dkxPm{q_m_^_TEEZ`MKkJvxmkLT9EU#94 zbETFy5uz3m7ALZ`G6|^((@bk_Edhv6M$Gy>Ls7RvpSMyvMO8FhWWkhE(;3n+7mqed zN||MuU)`&f0{MZN+h3&}zpXB(pKgfr{U$ze@Z(KUBe}tIhOQ`B9LYfG4ZxDHrl%Ah z!%TBUP#D70%*qF*18`HJ)#x~i4}m$qVd?h1%`bqW;w)j3N(BxXoPzMj2KptWL=@>$ z;wT_r19^<_#jhr|UY1(;r%>S-P-l(|LqC;ITqr?bC}-imhQ-aJ#3p7-9$NQq9Xa$0 zh`;_VWDZQFMz4^eOWsqJ{02XnHUV8{`!___jWJ8S@$M2q0$}C?o1sl~> zpC!&_o(B){Ll)l!-x6imXZ4`&Oux&k3A622E|(<{9rIC4UZL|IK^9;`V~W`nc=?;y)*rMn7J!e=y*9 zlk&(5flu+$l)Sp%Z~w#w8$>{8>N?=L`QAnwA4sUP)&SVLpUjYWEEz2I2qb6cg~q@p zlc@li-AT~^6?+KcF?H?F%#-H8Dm;fkz+i`B8K-Ob*0KZfLrbOP7Y}~1IJSD~Kfa*g zi$6O6%r7`tV(NYJY+V{suu)Ll`u~<(qC}>V3l8?<{~EJ`s>j~M9HnOsFwPbx-4b*4 ziHp{+;O@Y|Z9E~(T97JV4h=gUX5OD+Uu_C}PNEVMTFd#n+&@7uS&$B#rTPqg% zk<(T|1E}97iZE*4PA(?V=odAFF)zTOqxu2O{;fuo|7HmhLm&W>R<;X^vc%bb@I3Lt#tI#1@%zg-=VbCLRKziBt6G`(uw6TI7Cx| zaK=W5GB~s%ji}-G2!&;-*kcv4_A$oF{LQzBr!mvS5vW;;Z6Ujgy^jn!F6ze{ixoa| zFw|BF^CN!`Mp@gAg@mx4CSuZ#7qL1)CeJ~+s5W01 z9G|C0YD`$|f`1ali=mk!mE6!w#hwN))qFsaxjBH0)AxlgSxfG0O4tw^f1OAZQRifm zzwm4o*NOdur(Fi+u(pSAE;cs^#A6Y%6MGVKM)0UA;Zo311sm+@_44uCKt(Ew$u%@Z zV5JsfEHCGqI!WCag}ljknglMEm39;r1${a70MR&+9tQiuZZo=_P==sE;1(3Vp|Uqo zxrRj|jB_0%{xz8#mvBrrnycg!!opBtuk4I@bR;&$3}(T`aMzPai5rk1r{Xsi*r(7y ztP$}`5e=1F7LqD4T0)7@3OW%j0q~q-C}Hg7a%YA<02E~*b8rOfp}YzS7bp4fC}yXS zQz!e!5YQF$am*z?2_@JTY6qASPN+omhEbKq7zV}Oj6tP4Ja=+`z=DGps}UQ*sr!VA zj<5=4ecU3T6!RfM!Hnvw`&|Q_P!)8r%DKhw63doIEOUyMNTkUj7E6QxT=HHdie5;Y z^wuF}2R#t*2VZC`BxCx(jto#bickN7-jmt{gFQ1Zv@&qv;@^ki7tt~E! zK`Vk5S)(vLW6*5wvH;KKyGd#H*&-jIHK*@sfPavwSk%UiIv=Ti$_!h50P8$SnHjre8cNt z?C_)2(I9O|J$?3oVNZkN2v|%T$j;-u_^)@RQLOCsG!leJsXmxSCZ;`>MXH!7OlO6_ z%z(hogP=rNei`d28%g;9>aPrrG3@L-mK6PEXI|KRL1)zRprZGSv4uM zeI-ACubGHY|Mpeh1VT1%)k=zLg)kHAML6&`Ci`9u){KI(1!3jR0-!-kiYSjKksrqe zOZje_Ik1>Au$@NVTz_*RDh`y zgz(v%3nb-9@+OMm2G=4ps6 zuuwWH>H!{0l^6@INM;j zqa{;D2tB1NuCgzbG4#&0DG za5m+ItSV|rRw$L5KIgR_tEOM$?Nu;8$J4h=RQ0vpp8Eak@GX~i%Iu$NxF0mvv9llg z@&m7GjJOH@cIiH|MwF5i(g;O+FI5@06`(DJNh5{s4HolFi`zi~7>iO>8+wz*$~gpD z?esN(1=7%v+05vdeH+ym-3=wTN(`~sBNPEDV!%Q8z{Uu1DozKS*^Be{TpOokv$PDWs(;yPHSKb*$hJ+>cSKitp1Mb}JCb=a#gHO*kku&SKNi=PX!E*b;qyxDzl|R59M)fTK8a-QnqX!pIsao zc~m0n^fl>sC-rJ77z+E+^LEI#eP8nR7NakPRstdJ!(HSI3`?4k79gwKepTVH+FS(v+j@9dt1s|aH)||CxnO+pB5l5|X)*Fu1cDm#cjyca>dvV`?5^>7E^lpc|iBxbKG-t^1BPDm+939=&~p;W3xo@ zKw*Z(XP21v-um0T?e~h0zhMV(KQ818gCn({=09gvnRT z$O^vLM{iB8B>z^v8+MGNZGh2A6J(`Ejy)8Lqbb;u`?_(AUpas#ob}Y|ySx1kKF4hO zK5q6s+>NsxfBZ-y{(ISs45;f%m%+)M30W$+$L-xp<-YfMC}+9NYx5H|$bvF_2J1*ZAY^w`=PJ7W3~qayKxb z>l%;g4SV)juIwAd$LSdXHxWa*MlQ2E9ca7v8Hlv%(Ya5OVRzFT(nI-0YVrsXJ$?Ag z_mb#k1J_u6wH={kHcqCm@e3iUc3~Xc&r=@V18`i;VNg&j>s$YLh&4#duo;H{!?t-9 zq;4~!lTY+t!$WBx^za6p=`(XuFyHMJN})>FmHl7lC*vVauonr#3Ds5Yum(IPhDvxk zyBE8@EoxucaB!89C;d#$#&&$89@XliV^=6%BtGx3Z*35KVjdM8dnw2O^6>r*5 z@iBE}!4yjs?HiM&0v;)(6e#2-Wyj#~2)Grl0c3RYblc%E4Mih?bhfeqVVysev)CLC zCDm171;dki&4CM?{r=~!X z#R#m-9#eS0_So>A^r+pmdKDyX9XD^**-}T2!#!7hg~&MUa6CNG67IT9^?f8}-R@_T zvb~W}?y7Zt9_y}Y@~AC5hlQm#Q!vS{v~*?HXRW9q##d2I)b8BFp^deiU5p>U-x^#F zQoL-+sxdFnyx2;VCbfNd0OO6>QR}apq%Y<~?P?2#BK?6csM5pzcyYJT5o%S4~hT%BP@#ZodU;AsLh6?M-YVQ_PWzi|k*ggae zjo()lSb`m%=KErQ+UBE_m$Y{V^Ab~BbkMM%Z zT*N>pq@!;vXQ2s$gV5s=XI*ua92*M$`@8#|roOIVfBTuc_E&7SpivGKPl7BLoA0T7 ziaQuaA3GegFS##_7kWW5ehRWRN|GdO9MvvRjAJjogY4Pi%_G;MaURtcY;sXlaIp}T zCwB|D3y`ye4=u8V)z$t^hEj`-P*#O)!@xODtXv$jl)jTEyGlg$AardI6XX6f=BL(8 z#THW)JU>6*4dK-lgO?-qzl+Z8nr6XH@}@bqsgfjITPh~Rt2Tb^EuYP6k2SBytAf(E zrgp9Cst3=~n-_b(f1%3$&ry{%trK@vuaHb;&u&f|%pln}mlb#V*| z3k2;FHcBV?yvddZB?&Vru$E_xtl#RbP8gqKfVl<0YyP&lluGuN#qkFc$V$KLBOug2tr)2-1Ii zgO|JP&MvFTy(7rzsRi#gHSK2=|FNm8{iz{g<*+2bOzaS2s)nG4$xATAUeF&)+Bcz5~@NoMS zdFV@*VHXqst4Xs2N;cD((Q$P;4Jw)g$nv`W5DFYu)V20)?Wk=o@!uS)*7}Kdk7ExP zHH9;z*Fo4NetvDlQE#}YkgvIEj=4R3J=V||L z=aS)ho9O}4f9wGCqTYbY+5Ur>6ElffJ*=K58buoJxk50K{TchiXaBzq zhyP2df5GFw+oshII61Lf?3;s7kskCNs4sqwe~Ad$ku+UL*ba)}U$aUwMg=y1^t>x=}o=BH@}Djmzl= z^UUJm-F}6*tTlUUw!Ed)XON~sPj!GKRl6~-V+Jq1v>a~7?Q42$i_9la((r`BUf`C0 ze4V>=jAhaOzU+UkqwSmSflgLGz13t$ri;A5q<=#4Qqu>*AOk+IRK8vn7oOBfaq>03 zn^;M{F3yD{Y#D>O23tSO?B2T%v|+rF^9N_G=4E;=E0R+2zZku)EzdovEAhdgn41xi zDEAl;==Ba?Yw)b*0}XEA5av6@(FB2p{l*1d=*hBJ!UY(#5DE;?A*Ja>f?ifCQX-5r zbLgEyVl&M6>Prb(g-AM(9MK3V@DKC&4LQd`k8MU*!K@i3!xB(1P^^wcCZGjtsF(*{ zE^$1qrgi`K^JC_evS>ZoDTLvU>E1ZbI`7kd^ivzk@cLK|HKc~;A9_&-yE z2KeR91|8do5KK(5>C(Q|Pu?;I(ohAyWYpwnF8aWc%W(mPTVhkJ#u!5x7}Xie0u&y6 zV%@NM^bp9gjY?Z+Ink=z|Gn@`z{*M*X0xI5R{25DKOZE})ZJ67JlbnVX3xPQh#wO0 zvM(agksL}JAHI^*`Z3EMQR9CG z%i4NeOudqA8>U9wmEYMsztq~A+3w3Ffy^v>L7WbU^O&lJixVs@8%roH zIjZnt#z6i7jpl6kyOy`GIi?^Q-eG?Jt|FW0LWP>5{d7Dze(5(BQ%gz&0VNctFQ8;q zh^mcedY0TwhhfG)hvRc8BEEGE0vLciP^h@MCOad!4EWtD<79FFiJyUa!}ER;F_3RJm28&~&}!bmb%2AFOv8<>(cpEUzh zRImTmt79?IL;u?wU(Tp-CtOQBJ6*G@r~h#%QQ&1TiPR;2Q9Q^yj<_;s0AehoWKzE- zg4|h*JMtwv=wJ?nVG8_r&qvS!h_UC+qz=6=iqTdcd2d!ooq;V=6i7#Gi==!0k=iSk zB0>bPA5`J`7NC)D4{5W1m)4s7^bJ9RQ;uL{Eu_hytYG3{3Nwu{a0%jwhQV}$6~=_N z3IiYJh4zN4sq?i&V|6@tnNX~aB^?~%p-cp~R;g!nITTNni{do)F^C(fcVIP<0<8z9 z!^lU#l;|7-lKgJq$d@YW@_eg@gI{u?>>!5!e65x!at>HGsipm6GobKe&*Rvo9*-S6 za%Az1ZHhK+bPKo(E&7p3Q)fam1KZoViG-EF`GGka@=O^lk}U04IR);iO6?$OIvbiX zdJeGzWR@h%ky`$?T_FtGJ@bKt3wjF8_`;!Gx>5RUu`n85AXbshChLoNyd?c2nNoB= zS^>kt`Fwvg;wei>qNep>)gY^5hllj}<{RlVmpZkjg2gSDF4n{&#FTOAy@*cSoH!I-sYGzY)eU&WV4|Si zr8PYqHA?}wBd)a0rNR>u(5sRbL3|76?AOrBEQJd3Q%f(3>J{P&je_=LrDQ>bHWDjxxS~9 zz~2(|=$DaWr^JVOGE&j1In8se{Qcu0^M#rwI>scDH-Y&|bq}59=zK3#q%4F-Dx`lV zDkM}TB>@n72UH8pa&tZfhgX8^d{KC`xr9DTHFW*Fw(BVR*jLUzdlgrXY(L~n z-+xqBTOOHJMNxinxDtCD&+LrG_H>d0vM?t1)mF$vRT1=p#3{~0i1GCw7F2-d&)Utd| zb|k8Zy`LIO1!L2)$lUs1@NO0{d*v~=XJ&}?-7sgyXG`#Z+aeW1*0tTDEmgEiMRyQy zcl71z*pBQJq4=9o4W$*zKkI#!KVUDprjI|6lptqi3uJ$bIFB z*O2NLtnH@$yPTUASoi*22?bO9Ew!^}#Hy~rkFvou!?(?Ulfct)Q2uGQI= zB&Y=F241Jd0{49E_-{c_DnnIh$q9K)<$q;K*iiWZQW2=~T$OIb1p_$QUnfZU+Y;f} zN8?W(sne`3IeMls1SHJZyml&SS6fn<}%B0Fk#pFFZ^fzX()Shlvqso`Zox#vp2DqToj!- z$IZ_&y*>fk%qPUA&=et1zcnfQGAWukbVUXkuO69X1bZ)wt{jjtno}FC4{18sh%rYm z>p(uDM8s{gEvz%HcHT%LobeVJxom&FenZz>V(zAUT37paeNnDddyIxR9(4$2wU2MV zUwM43;D*1wEq)J*CHdRJ%e4XVxZ^QO5zc$M7L!i&@Ys1QNU65~A5-^=31yNN(V$ck zkp~kMBpUtD(1>Cy!x4^+7YXz++`tSd<6V?KQ5hqGh@{76YInxjwHw+6wKS-qC z?)I%WGXzECcr#haDdB5Lh6NsbGOCOpy=!|{EY`2W=TWDl;^FF`Ui?5$Nb7{d^Sxw% zV<0PuqO96EJuEl-Z|5db4!bXw4thi%eR}?yW8-*lk+Hh&vHyVfwUKt*FIe8aKfhhK z2uqtuO4Iq9jlUOs+nCn`TRD>y?SH~D$B!}iYkU#D;M#Q?_!6Bs@WPlu*ea)oito3- zcifl7o?zZC!!PiAy{rz-<};g|8D#cE!Kn<2VsafEhyD?IKx38)UyOMhZh^|pA?mvpw_eFA=V0bK=IRjue1CVi;on_0@?@|T{>RUn-ZX)fOrl!nEIh`9EFA|-0r zb2~OQz#;6EtNDACf1Q>j&)n+yCX#xk3xoP@OfF7se+zhz;pwYtY?Z3(walyxx=YrX zZ7LWE>oBgo5EsESH#2}hFj2uZSp8!8lYB-Yp8PEYM%W-E+j*D(*NT!Z%PA<^OE~Ti zsd*BI6f}WCP^xVaUn%dgFYb^37=#lzyI2Pn5GFnBYvu8Y46$~a`hP7kzm%Cy_h*lOP2DPaB)ka2tLl#$ z((D?0Q2IyL9MQz_wb~M)5d@pLf+yQ57b*v$N_&r_2|O50`YkdJAUa|#K%7X?v2E0; z=JjHE#X_1ruuts`f!2dfyNpHDxK&Sk7w-vJb*SYzZx99{fR1`}At}Igk0q=g?-cR| z{J)r8zw(Rbs-7j8Ua`ibQfOxA|6|DH{0bHZ*$tl@R~4kbM{>*Yj5I*Yw$VTBkn-*D$|N zHS)=uU;(Ny#3`Z>trT+TYGO_Ie(qflR=(_WamHQi$feb##y-0W$ea94>V1P^e#Khv z^b%OMN|yWYXLib91Jh3By-pxcJ{Gm7TCc&A#`&8Yyt`$;M~3R$ zx$nw>tMGubGAIjC&MU!Y8Rg{CPnpQ8|@9KPu0-yHB zAS?;=#pIr;HBe(}aDzV+R1sHuxCiwkS1GDPiiL{9SMnH2?(`J5*mLG-bgr;9%JU#Y zh|ps~=|C~w2aS<+P#2cLoHDxfxm|@iZ=`Tulb6!@LD83sLTE6i6vKmB$<9zM1(-zn z7qxCB@-bs11fx>UVKWz>O}f#~B!UgZxktk0P$}*C9O%Q zWku@F0A-^fs%>gsAm%1!u3M|Sh>XlOOBip0EwL;?iGqL^J$yza7=fL_j9}Foh)@A^ zRf!uWf0uZ-jQ~nbCT>-oAN!;5czg^#o&S}Li7ZGbr}GvcKsBwNDr8h2qbftD>%|I# zXJ4T?{f=W`qC(T2TmFJeBHKIgBJ_VPV~G0jRb{2@}YbK z$5pd&U`VMCnZL8X$xNyz!SEzR>lSpXSx^`kP>YVx@fVIUW2^`$dLsFpC4sH0QyK4w z%&*X>B=L!2IRlGpb zG&qaz4$>5HWO9s7=r&Tj`z~d#t`7mfg;~YzepdQh>Evm|_S@0d{gX1Xw;i~dQFMYR zc>b`m1m4+>$&W$3%NrPJG;LMu!U|{g>+M|d)8lMsy<&YaY=A#IHx%M*y}FL`zJ&7F zI{lmb*{Mh1PE3I)L+Lc6#v#{lE&Qr0?Zg)!`$Qag0+bZ#p*4%yC3ZDV`zE&4R-yOyx`IGx!`NwQr!^jnz}mNy8GI3*_phC z?=m{-+-dwgrY$ncVvKJUj9W{~gNj$M>9-PH!4jEMQ-&QGyfAIIk7C1#9{p;{Yh*rp-J1!o^nUSm+O(|$k zD}S@1P$WXRsmPrvs6**y+M7U$?~#g?g3$~Z z4h84s$ubV`BTfpkJo#WPhw@8*3|`^u^|<4)`6>-x8?QDJf;`FM{1Y4wZKFX%QIFVD=_a$yq{TOUsa~`=V z|8?EFhGQrwK(%@Em+1Gy{et$t1k{@w_`a%vw8QdST5{{ip&$6QOm%VnZ2$l=%YXj` z@Dp#KIM@)~D!bDCuaqm)ZAeYyqUaLD7xW>D|N0%rL-)mJ-K?HhZ(_jQjQBMv^#Ygq z$xyJ&+KO;qMtqUNl4c`SC=A-l$$ng4JgdjQ0#B#6MEk)?6|f7#&E5xl7$5<`USP4S z<+N6|Cc9AhCOO`0RR7i#;5`+5F__qTIC_c`Zt-=F)O@43$PJ=dk?@bmL!lBpC|js)-e<3aL{ zhIPsrJ4{N4r{VXDGX03`E9%1C&w*bj3vS`c-Oo#1mq%&*c(Ujic9xP+R`PmoxwdXR zWVe>GwcSy*{8PEch5NRp06ME=`6R8HWd@t{b~d-0;O_>0_zrH`p17ea|UK$sf~nzQn~ z5!TzMJ4PvMsOm-rxd+dr`tLMkDZd-;HCBi|aeDd9G*+Ia8xY7{`hEX_dYzp!Bf;x- zFgIA*l~(MLW!D;+6eDk0#+T(zN8tc~QaduvnRdE;kVzEAr`3!a+Mk!KwB=_kUJSBN zRhpf;D@b;{VG9x`ey=zaZUe-SqY*O;Q+Jyh8KA)_3Uk*8KYtd)x-J&RY$H zc0_~171s{NqmXn;)PlDBD9o~7fpT?S+s?1D^Yz*7k;}e=L2`D=ln)qYmpgW1$|}c$){${T%m0x&Ctp4I?8#p_{03=w%t>R=Ms($4w zm(AxJt@PH3B>R<*{1vm1F9*$;xq*ELE-rJ+w8TUdla?UNswq&QY49#cV})+2=CIJApssUC+%#*1H?MZ~Ek? z3y)e&XZRP#eYG!;{ulZiGbm^MY99Sc9i z<;Gag$3~FM<_g@zcRZN{^QAcEzjo3VVC58e3H54-lHS zz(HSAQKb|8nfH~tbMo2p|6-EjOyzBAZ3$3|YdTiL0w*{tUgRrwmJyHD)X2yYgqAiH z1&tf*oRQ)@P4KkL)gYup5|Vz*P&my%y=~r5evXGZAwCMW!8cwd+zQ8-Jhtbffc#HS_~%=)zPCw?-`Y zM+qG`8zIGzq~e|dc_NpoQ9r#{2#w(i&T~={^x!K=Vpg^J8FI!{zm7vI0h9QK!`re% zAx3?~$mHx2VPDx;EN3#!p< zYs1~qD0NaiVyp{24}Eg>(agpoU?&jzP_>#QT~2__$w0rR5ZDqDO`Ee>VF|hWamU!6 zzOpxSsq91eGg&~3vx|t;%d-QKwqqKjBMRpnkdi?5xlsT}b17Oya6IA@4rdTKBCb(n z!x4RzC?eT_CWJm%4x>!(EQ)o9-#S}42nZ|JR-!i>zAL0)`CIvfARoW@{pKAkno@b_ z93WJiO0Zi|H9N$)q5MqlpTY=p<%sZqOy)tPBWTduUz$^9tzKU=oR{mAsbvozFNbf(SEu1?@L9og+csZ*i{=>4bm>Aq31*`;iZOx z%3*+HkPy!v&20P-zYoH}2|o2SNo4Rff~%sH1&>>ZKDabq?mS$)&AeR?>(^Pl`y%7| zA$Hr_*6R5}oTe~4#A2THb!Yb5R4}eYfY7w?W1!-GBsCFTwh3I{5tce5Sh86vudB#* zzF7V|^i&A9kDU50*lDaMd2sRC{x-N8cNzG~ z_Wf@Q*ux#`!0)Wo_r^3D*_x@{HHb#J`_PF~EmDYQEhJf&Xv|Y4f8zYT=+$PhrU%@i zRTfa;RzHq;rs&6okjpdaB@OZMJl2?X*gI&Jc;6OS@$3;k825@&BrIupy zg8MpvAjfZLzG9fOJh)h%_sBe3;+0qV7Pw+v)Na16BOS-utA0wy_qH3oiBDBYa;GN& zB#I*He<%W{@C)O%Qq5zUhQ6t}$eeSMYSanv37#Z@^I3*)G9FIbMzr!2g0?>u2(4Ym z22Dj(bc?dt$vef?S5<1X_TmN_KD+EVl3!4W#uG1vlyf4**34b^lP4N7y?nRsq@^0U z@!nM1zuo4;#GA~@ip<)E>zSN#&DY_0qzgZ<#jLhCtT%*j8{ndU-M!_4foK8KXh?y& zLCC6q<}dV+jIZ1Kot7T`v#{@5MB`#04U5$n-drF38#U@{s1tA`P%gm^?|GdS9J#vQ z%p1L$qS;lgCV>7PFF@-bP1eF6Iqzm_*e>dMh|^SF zH-Imp&|i=Kv3}Vc>w8A)zhLza@C9no_R7@e2UCHKu8mYI=Ez@@#Uy$R!Y5u;LsW}> zI%qyCmV|a^_br1Av5ttXaM@M8w}KcXWu)QzGfHmE-d>ZvluYg_mRqJoWej~!4Jj9> z3)N|N-7r4g90i#N#D>)ornqm+z=_sR0(ReraQNTaxE`#)eEifuyL>Y$J*}(u4Uth3 z_qY4a8+y3muktH8`wvV2H#~?uAwr}uhyq}o@hVs$Cxs{rv|X}1x%hjyB6Z8F+G|nL zxA9l#D3E58ltR!?TU}`99$w;MSq3J-vg`P#$K3{??XXe@HpRQ{>;RtNst%RSS@&S4 z8US8WHGDR5pCDfLK#`77neT+;-elEi@`RUKWXkru0E#17&RsdOYMcybDg)Cz8S{rO zK(SW2zEIU1Ck*LeD!6$^!z^3B0Vw*|2cliGn~*Yhm{FW?k4z*f6mYQ`d@&RF9!YXA zm16M~Hux;#0HR~0SSnpU%|$& literal 0 HcmV?d00001 diff --git a/packages/app-tests/savedSqlStore.test.ts b/packages/app-tests/savedSqlStore.test.ts index d90d4c24f..bf9b854cc 100644 --- a/packages/app-tests/savedSqlStore.test.ts +++ b/packages/app-tests/savedSqlStore.test.ts @@ -48,6 +48,66 @@ test("concurrent saved SQL folder creates reuse the same pending folder", async assert.equal(store.folders[0]?.id, firstFolder.id); }); +test("creates a nested SQL folder under the requested parent", async () => { + const root: SavedSqlFolder = { + id: "root", + connectionId: "conn-1", + name: "Root", + orderIndex: 0, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + const sibling: SavedSqlFolder = { + id: "child-1", + connectionId: "conn-1", + parentFolderId: "root", + name: "Existing child", + orderIndex: 0, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + apiMock.loadSavedSqlLibrary.mockResolvedValue({ folders: [root, sibling], files: [] }); + + const store = useSavedSqlStore(); + await store.initFromStorage(); + const child = await store.createFolder("conn-1", "Nested child", "root"); + + assert.equal(child.parentFolderId, "root"); + assert.equal(child.connectionId, "conn-1"); + assert.equal(child.orderIndex, 1); + assert.equal(apiMock.saveSavedSqlFolder.mock.calls.at(-1)?.[0].parentFolderId, "root"); + assert.deepEqual( + store.listChildFolders("conn-1", "root").map((folder) => folder.id), + ["child-1", child.id], + ); +}); + +test("does not move a SQL folder into its own descendant", async () => { + const root: SavedSqlFolder = { + id: "root", + connectionId: "conn-1", + name: "Root", + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + const child: SavedSqlFolder = { + id: "child", + connectionId: "conn-1", + parentFolderId: "root", + name: "Child", + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + apiMock.loadSavedSqlLibrary.mockResolvedValue({ folders: [root, child], files: [] }); + + const store = useSavedSqlStore(); + await store.initFromStorage(); + await store.moveFolderToFolder("root", "child"); + + assert.equal(store.allFolders.find((folder) => folder.id === "root")?.parentFolderId, undefined); + assert.equal(apiMock.saveSavedSqlFolder.mock.calls.length, 0); +}); + test("saved SQL summaries load file content on demand", async () => { const summaryFile: SavedSqlFile = { id: "sql-1",