From 41cca86e6e33363163efb575ae34f4cbe5aea694 Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Sat, 22 Aug 2026 21:48:18 +0700 Subject: [PATCH] feat(workflows): enhance script handling and introduce custom step names - Updated AGENTS.md to include a `name` field for scripts, allowing custom display titles in the editor and graph. - Enhanced script icon resolution to support `.jpeg` file extensions. - Modified workflow parsing to accommodate the new `name` property for steps, improving step identification. - Added new utility functions for handling custom step names and display names. - Introduced tests to validate the functionality of custom step names in workflow YAML parsing and step labeling. Co-authored-by: Nasyarobby Putra --- AGENTS.md | 5 +- packages/server/fs-store.js | 3 +- packages/server/scripts/fetch-binary.png | Bin 0 -> 2917 bytes packages/server/scripts/fetch-html.png | Bin 0 -> 2917 bytes packages/server/scripts/fetch-http.png | Bin 0 -> 2917 bytes packages/server/scripts/fetch-rss-feed.png | Bin 0 -> 3385 bytes packages/server/scripts/jsonata.png | Bin 0 -> 2403 bytes packages/server/scripts/ntfy.png | Bin 0 -> 3312 bytes packages/server/workflow-parse.js | 28 ++++++-- packages/web/package.json | 2 +- packages/web/src/components/ScriptIcon.jsx | 2 +- .../web/src/components/workflow/GraphTab.jsx | 59 ++++++++++------- .../src/components/workflow/ScriptCard.jsx | 61 ++++++++++++++---- .../components/workflow/graph/GraphNodes.jsx | 56 ++++++++++------ packages/web/src/lib/try-session.js | 12 ++-- packages/web/src/lib/try-session.test.js | 15 +++++ packages/web/src/lib/workflow-doc.js | 33 +++++++++- packages/web/src/lib/workflow-doc.test.js | 59 +++++++++++++++++ packages/web/src/lib/workflow-graph.js | 5 +- packages/web/src/lib/workflow-graph.test.js | 13 ++++ 20 files changed, 282 insertions(+), 71 deletions(-) create mode 100644 packages/server/scripts/fetch-binary.png create mode 100644 packages/server/scripts/fetch-html.png create mode 100644 packages/server/scripts/fetch-http.png create mode 100644 packages/server/scripts/fetch-rss-feed.png create mode 100644 packages/server/scripts/jsonata.png create mode 100644 packages/server/scripts/ntfy.png create mode 100644 packages/web/src/lib/workflow-doc.test.js diff --git a/AGENTS.md b/AGENTS.md index 34ab958..5182884 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,13 +138,14 @@ Injected: `log` (pino), `console`, `fetch`, `require`, `$axios`, `$kv`, `$finger ```yaml scripts: - - script: plugin/my-plugin + - name: Notify to channel + script: plugin/my-plugin config: url: http://10.8.0.6:3030/notes token: $SECRET_joplin_api_token ``` -Canonical ref is `plugin/` (`.js` suffix is optional). +Optional `name` is the display title in the editor and graph (falls back to the script filename). Canonical ref is `plugin/` (`.js` suffix is optional). ## Do not diff --git a/packages/server/fs-store.js b/packages/server/fs-store.js index bf96205..2e3e6d5 100644 --- a/packages/server/fs-store.js +++ b/packages/server/fs-store.js @@ -50,7 +50,7 @@ export function readScript(name) { } /** - * Icon next to the script: `fetch-html.js` → `fetch-html.png` or `.jpg`. + * Icon next to the script: `fetch-html.js` → `fetch-html.png`, `.jpg`, or `.jpeg`. * @returns {{ filePath: string, contentType: string } | null} */ export function resolveScriptIcon(name) { @@ -59,6 +59,7 @@ export function resolveScriptIcon(name) { for (const { ext, contentType } of [ { ext: "png", contentType: "image/png" }, { ext: "jpg", contentType: "image/jpeg" }, + { ext: "jpeg", contentType: "image/jpeg" }, ]) { const filePath = path.join(SCRIPTS_DIR, `${base}.${ext}`); if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { diff --git a/packages/server/scripts/fetch-binary.png b/packages/server/scripts/fetch-binary.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab9b42130e24174ae4e1a46cd94f8ad210a857b GIT binary patch literal 2917 zcmd5;XH%1l5`GCS6e$rx4Il;*ks@M2X`z!yQ#c?c^rBRyLktNZDqR7uD4`q>A@oB8 zq)Jl=2#BbH~gf6aWOe{}Hy{;4-h{phTGQ%`lr_pRh>x5N{wdGE((+kbkJB z`yFr9;1J*3O>Id4;H9ID^lhU^8+rcLHuf?dtd$u!9Bu%_xqWrl_kR*!<)KpdXfL4A zv0m`EcRhtNi=7k<_ieb^F1a`{`*}8Rwt8c1B%%`kA)?a8pV`TMtJKaHf3|i!87wbn zlqv+yfY0D|x_NVN^tEvq8Vs>^lSs_BD+gK3ZCdD%&MrpWv+zE{3vUiL^3jO%1`_V_ z=R5&M)ZL7(bDE*#P~z+p#&Hejx+I_XRkNd-5*-{xH`EXvK6TMW$>mTGN)honAdOc&KG1MED~ia>a@$0tuVw6KIQ_>@?E(GI0PAdpF%=7zXQP5Es8sq>bd z_%{mih(iYSG7%k6n&=R8=;cIUtL%AUjNBJxhpKx>$g@2J?y4uX1-t);74^(!OVG{( zmcgy3?cO0z?5e5id9i?{A&5_J78ARawD;W8B#{U2F_jN!@w#kfC|6MH@-|8tT-&dEr?@EcYaB-@XC_-(hltQ?t}HLPmC+G0dh`mDZX&Vj1WKK$t5u%R;+xX5{6GD z(b#G$>S}Mz;i2Cjr+!BriW?-c0)^x#DimMg&S6(y5fqqB?p)=TDL8KCk0y=7{ay&Y z8)cZ;sp&bMI_9<_Oc#>dW?dx&na89CAzKn#kD+yKd$gN*{s-}+_=V3+!5?6sO5BW0 z*oI7ezc=j;VOyJ7er?BBgjiBEw!*4k#P_Gzgp?)ohAXfQ54Tcj^tm257Pqpv8RmN2Y!x7*8rlqE} zE{;et_9PYuk1xrq4ytP z*SCE$`{VGF&NZR*|J(7{N+^- zO+3}wG%Q7@K#Xw20JvoV!0&BE;Oxb~N&6lD9YqC3iRh~mWS~~z+w$#nibYUn(NP$_ z%~|I@1YDp_F#`+w`9)1Iwi~Y40w~Ona8L@NB8M()qJ>or8~M(u zZJ%h+K4}9OI{W2fjjVlGbF~(BrS|ER*Kkfb_-VgpXW6X_Le^|Ec|jS@=8^u50p^;c zE_1QNQ>(VE&QqaWvxgM!J9ElF55x zzpuq6RBe=Iamwi0Lx`-7uH()S0w69n)H%IJq8^VRR*P-;Zg^(*K;j;cT_K;$?zvhH z%fl>K6NTJp1PG{aW5waXl%VagLxcB-8W8;UVC^=jG62X$!aKX+?2ePt3FtkW|=m@ zBN=zqPdZQKvSafDCh{iX2SljrH7#JnuKCTVYwgLt|7B_sKxX|7NW#KQ1Q1a}n{=fI zVTj25Q-)o9pszBVpvHteFhpA1JuN$FJ*M`gemuMATKH4wYwy{QSbk0Us4su_2c+}0 zsFHq)o<`rOrCl0=KUM;J@*gvRy@s2EK8Axwz#BSJmjwdOCBb+iF7-Wqonk@oGy)*N5l2lo{q8TkJyu4^FFIheQT@NlCTx@kqImXBF$Hm9M_dfjX>Q z9;BvX98vLkP@jFOMgsfsXX~b5#}2NQ7XQE@POC6~Qg}+(z*6H0?1%0KPNNKH3y{zK zAX)AZLdXXYDJI+yz_v_*(Mh8nx%KgW9EhmXX_jgZMG^VC%pmMgKHzEx#jw_$!wdnu rK7Jj^W;Ky0)BW6aiz$VD0AO*JmgGhstme!d<2Zmewlu0XaEt#h^s`Mr literal 0 HcmV?d00001 diff --git a/packages/server/scripts/fetch-html.png b/packages/server/scripts/fetch-html.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab9b42130e24174ae4e1a46cd94f8ad210a857b GIT binary patch literal 2917 zcmd5;XH%1l5`GCS6e$rx4Il;*ks@M2X`z!yQ#c?c^rBRyLktNZDqR7uD4`q>A@oB8 zq)Jl=2#BbH~gf6aWOe{}Hy{;4-h{phTGQ%`lr_pRh>x5N{wdGE((+kbkJB z`yFr9;1J*3O>Id4;H9ID^lhU^8+rcLHuf?dtd$u!9Bu%_xqWrl_kR*!<)KpdXfL4A zv0m`EcRhtNi=7k<_ieb^F1a`{`*}8Rwt8c1B%%`kA)?a8pV`TMtJKaHf3|i!87wbn zlqv+yfY0D|x_NVN^tEvq8Vs>^lSs_BD+gK3ZCdD%&MrpWv+zE{3vUiL^3jO%1`_V_ z=R5&M)ZL7(bDE*#P~z+p#&Hejx+I_XRkNd-5*-{xH`EXvK6TMW$>mTGN)honAdOc&KG1MED~ia>a@$0tuVw6KIQ_>@?E(GI0PAdpF%=7zXQP5Es8sq>bd z_%{mih(iYSG7%k6n&=R8=;cIUtL%AUjNBJxhpKx>$g@2J?y4uX1-t);74^(!OVG{( zmcgy3?cO0z?5e5id9i?{A&5_J78ARawD;W8B#{U2F_jN!@w#kfC|6MH@-|8tT-&dEr?@EcYaB-@XC_-(hltQ?t}HLPmC+G0dh`mDZX&Vj1WKK$t5u%R;+xX5{6GD z(b#G$>S}Mz;i2Cjr+!BriW?-c0)^x#DimMg&S6(y5fqqB?p)=TDL8KCk0y=7{ay&Y z8)cZ;sp&bMI_9<_Oc#>dW?dx&na89CAzKn#kD+yKd$gN*{s-}+_=V3+!5?6sO5BW0 z*oI7ezc=j;VOyJ7er?BBgjiBEw!*4k#P_Gzgp?)ohAXfQ54Tcj^tm257Pqpv8RmN2Y!x7*8rlqE} zE{;et_9PYuk1xrq4ytP z*SCE$`{VGF&NZR*|J(7{N+^- zO+3}wG%Q7@K#Xw20JvoV!0&BE;Oxb~N&6lD9YqC3iRh~mWS~~z+w$#nibYUn(NP$_ z%~|I@1YDp_F#`+w`9)1Iwi~Y40w~Ona8L@NB8M()qJ>or8~M(u zZJ%h+K4}9OI{W2fjjVlGbF~(BrS|ER*Kkfb_-VgpXW6X_Le^|Ec|jS@=8^u50p^;c zE_1QNQ>(VE&QqaWvxgM!J9ElF55x zzpuq6RBe=Iamwi0Lx`-7uH()S0w69n)H%IJq8^VRR*P-;Zg^(*K;j;cT_K;$?zvhH z%fl>K6NTJp1PG{aW5waXl%VagLxcB-8W8;UVC^=jG62X$!aKX+?2ePt3FtkW|=m@ zBN=zqPdZQKvSafDCh{iX2SljrH7#JnuKCTVYwgLt|7B_sKxX|7NW#KQ1Q1a}n{=fI zVTj25Q-)o9pszBVpvHteFhpA1JuN$FJ*M`gemuMATKH4wYwy{QSbk0Us4su_2c+}0 zsFHq)o<`rOrCl0=KUM;J@*gvRy@s2EK8Axwz#BSJmjwdOCBb+iF7-Wqonk@oGy)*N5l2lo{q8TkJyu4^FFIheQT@NlCTx@kqImXBF$Hm9M_dfjX>Q z9;BvX98vLkP@jFOMgsfsXX~b5#}2NQ7XQE@POC6~Qg}+(z*6H0?1%0KPNNKH3y{zK zAX)AZLdXXYDJI+yz_v_*(Mh8nx%KgW9EhmXX_jgZMG^VC%pmMgKHzEx#jw_$!wdnu rK7Jj^W;Ky0)BW6aiz$VD0AO*JmgGhstme!d<2Zmewlu0XaEt#h^s`Mr literal 0 HcmV?d00001 diff --git a/packages/server/scripts/fetch-http.png b/packages/server/scripts/fetch-http.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab9b42130e24174ae4e1a46cd94f8ad210a857b GIT binary patch literal 2917 zcmd5;XH%1l5`GCS6e$rx4Il;*ks@M2X`z!yQ#c?c^rBRyLktNZDqR7uD4`q>A@oB8 zq)Jl=2#BbH~gf6aWOe{}Hy{;4-h{phTGQ%`lr_pRh>x5N{wdGE((+kbkJB z`yFr9;1J*3O>Id4;H9ID^lhU^8+rcLHuf?dtd$u!9Bu%_xqWrl_kR*!<)KpdXfL4A zv0m`EcRhtNi=7k<_ieb^F1a`{`*}8Rwt8c1B%%`kA)?a8pV`TMtJKaHf3|i!87wbn zlqv+yfY0D|x_NVN^tEvq8Vs>^lSs_BD+gK3ZCdD%&MrpWv+zE{3vUiL^3jO%1`_V_ z=R5&M)ZL7(bDE*#P~z+p#&Hejx+I_XRkNd-5*-{xH`EXvK6TMW$>mTGN)honAdOc&KG1MED~ia>a@$0tuVw6KIQ_>@?E(GI0PAdpF%=7zXQP5Es8sq>bd z_%{mih(iYSG7%k6n&=R8=;cIUtL%AUjNBJxhpKx>$g@2J?y4uX1-t);74^(!OVG{( zmcgy3?cO0z?5e5id9i?{A&5_J78ARawD;W8B#{U2F_jN!@w#kfC|6MH@-|8tT-&dEr?@EcYaB-@XC_-(hltQ?t}HLPmC+G0dh`mDZX&Vj1WKK$t5u%R;+xX5{6GD z(b#G$>S}Mz;i2Cjr+!BriW?-c0)^x#DimMg&S6(y5fqqB?p)=TDL8KCk0y=7{ay&Y z8)cZ;sp&bMI_9<_Oc#>dW?dx&na89CAzKn#kD+yKd$gN*{s-}+_=V3+!5?6sO5BW0 z*oI7ezc=j;VOyJ7er?BBgjiBEw!*4k#P_Gzgp?)ohAXfQ54Tcj^tm257Pqpv8RmN2Y!x7*8rlqE} zE{;et_9PYuk1xrq4ytP z*SCE$`{VGF&NZR*|J(7{N+^- zO+3}wG%Q7@K#Xw20JvoV!0&BE;Oxb~N&6lD9YqC3iRh~mWS~~z+w$#nibYUn(NP$_ z%~|I@1YDp_F#`+w`9)1Iwi~Y40w~Ona8L@NB8M()qJ>or8~M(u zZJ%h+K4}9OI{W2fjjVlGbF~(BrS|ER*Kkfb_-VgpXW6X_Le^|Ec|jS@=8^u50p^;c zE_1QNQ>(VE&QqaWvxgM!J9ElF55x zzpuq6RBe=Iamwi0Lx`-7uH()S0w69n)H%IJq8^VRR*P-;Zg^(*K;j;cT_K;$?zvhH z%fl>K6NTJp1PG{aW5waXl%VagLxcB-8W8;UVC^=jG62X$!aKX+?2ePt3FtkW|=m@ zBN=zqPdZQKvSafDCh{iX2SljrH7#JnuKCTVYwgLt|7B_sKxX|7NW#KQ1Q1a}n{=fI zVTj25Q-)o9pszBVpvHteFhpA1JuN$FJ*M`gemuMATKH4wYwy{QSbk0Us4su_2c+}0 zsFHq)o<`rOrCl0=KUM;J@*gvRy@s2EK8Axwz#BSJmjwdOCBb+iF7-Wqonk@oGy)*N5l2lo{q8TkJyu4^FFIheQT@NlCTx@kqImXBF$Hm9M_dfjX>Q z9;BvX98vLkP@jFOMgsfsXX~b5#}2NQ7XQE@POC6~Qg}+(z*6H0?1%0KPNNKH3y{zK zAX)AZLdXXYDJI+yz_v_*(Mh8nx%KgW9EhmXX_jgZMG^VC%pmMgKHzEx#jw_$!wdnu rK7Jj^W;Ky0)BW6aiz$VD0AO*JmgGhstme!d<2Zmewlu0XaEt#h^s`Mr literal 0 HcmV?d00001 diff --git a/packages/server/scripts/fetch-rss-feed.png b/packages/server/scripts/fetch-rss-feed.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a4d4063580e86274eafaea03895d196d30490c GIT binary patch literal 3385 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 X!ljC>TQlzJ?E$nA`Wj{GcDVlm5f@Z& literal 0 HcmV?d00001 diff --git a/packages/server/scripts/jsonata.png b/packages/server/scripts/jsonata.png new file mode 100644 index 0000000000000000000000000000000000000000..9715ca3b368e376d3c6c74ee5e575567eefa8898 GIT binary patch literal 2403 zcmZ`*c{tQ<7yiw>wy|a3GMSL5Y(+C(LLy6=X)JBF8T*o$Xl7EF7E@jtlBMisBxQ-x zVj2vFQK87zG|UWz5*b6Z;PZZcf4qOZ-yi2W*SXI**L7d#xzBSwsYj5`a?)DT0079j zx;T1lc+%e{C9!d~+hSjBI7qC^i8ug|Z~xms?3l{n4Nx=Q>3F?)7%Kh> zD|qWrCgmg({Ql@$cc1qI2+pBU&aM0g3mR3w2>G_twNw96PY4|h4pg>w4YlykYKR!T zMyIAqYy&YAT1Qiw7^@g8Ywmi;>yYKu_2&Ai(S5JG&mqE=eKn`FTe}R652t;s9%;wf z1@&u_P-2;~B=IGXqlvaXoRkKfM1)BacjZ^By}E-L>jtaBsu~8wOlqc>2Y_-#_BOZJ zxZZC#vRhQs$I_OIwlbN1jJD~1tEZrqC)$wI-5PaOI&+W1W@fCLkjqC&-47pXPht-P z6l&R51T!c=rZxg#$Sf_w`D}qm_$GFJDbz|lvePw_}*yDt!ktRwbaP` zG-{BklO9K@?)Olh>QA!tRCH<-VB=7h$88%MCHVabIe#dIe1ZwD1I*CeJXE&tFECK_ zvh3PJ8g0lO}4y1}O$R1u|xd9$IeF3+M%VkjKrNgA^KKtBje!)-627 z@)i~K4?+H+6&#$`Hg`~9P+%;N`y8De(>Bf(zt-1hZf0K!{uMnF+KogVvnEeUiFPte zGd!&?89+RNap=GD`v+M_oSBQfMAH^tz!R}UY9_1|+`Sd*%sB9g7t6%|dM_8!D zmSp0cwp51$;!+oF_o}^8S6zWVx_|~P*4MkSG!=K)OQjnbX+)c?K=*Jk%pyX$VYXqs z7fE4p)9~5NEkUlf2;wWS)!< ze!#HM&|;$<`nb6D^WmKEub;IR63!n}Pomc=IoE`Fx$U)S(7}kw>sV9TEN9FJL%TG- zy6k7?RIOwTNB>InLv02&gr0J4?e$pWU|9v@wNOOhd#4hU%nvDX`ID(fk0N!6nsDUYI~ zci-*LtsJwX&pi*-@C5U!`{t(G>G#8gf>dSc@{N=+b7L)u!SDJ>6ZBfY_WT|n+&I@t z+#>?zr3Tv`Bym4!?Gedt6`73RQU&*>PJH%G$%;S?sA7_aPBB&rwwu4?ub*gLM=XGf ze~Dxiafb+2PTruu77nkhes;!Q6E)4pjLcSY(bwsOF;BDGcqb>2EDlGE9LzXEC*I)U zHpD*9Dk)}91>g;{-GAyf1K&kcf_pj-9P))xw(JpTA`v@m;sv*K9`LitQK%PpDM5f* zd>g@=CO`gwE`Jl~jhytmY-$>SHaroW`V8#8TdTg)mmKr4fb5%__#9>G*H)1i&>;w7 ztGcV7)2#sFDp2iuuW|!lB+4k#-hS66Ed4mk@bbAj%;Pv${e*=#1D)9_HtVbhnK#Yb zzdrLXfIhIWD6KeW`UTspgvgI*28xDjzc+C@Cu2+JMwjP3{PxwxC*Q$5oM!IM%S%}v z(%gM(PomZ?Zyz!jH$fhyz@EG&^H$3CXvhm_b!@5PSi=tuypj%RDyY-6L!GC|hU4iLa_n!GGNwON{ z|Kip<2VyuaWyUukKYho!Z>e}$s#=lR$qi6ttk++fp!dip0^4z z<)VE5%$4b^0uevRy97^!GOM_EN~WR(f7lKIPxJi(d&w*~__#E4+55KRA+BtAs1na? zKAM+D64%H)@v8~)#wtzY76fwwD3OIh;*l3QHUJ$oBrz9pktJ_N%tsClCMtFptjlzf Xi;td9*dDU+K>}QzkdCzum`ncwkaA;Y literal 0 HcmV?d00001 diff --git a/packages/server/scripts/ntfy.png b/packages/server/scripts/ntfy.png new file mode 100644 index 0000000000000000000000000000000000000000..c1c653e756e33b9449f15e439cd4b97ace1748b6 GIT binary patch literal 3312 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 u5dky+D8sFBofMNGCS buildGraphElements(doc, positions), [doc, positions]); - - useEffect(() => { - const built = buildGraphElements(doc, positions); - setNodes((prev) => { - const selected = new Set(prev.filter((n) => n.selected).map((n) => n.id)); - return built.nodes.map((n) => ({ - ...n, - selected: selected.has(n.id), - deletable: !disabled, - draggable: !disabled, - })); - }); - }, [doc, disabled, positions]); - - useEffect(() => { - const t = requestAnimationFrame(() => fitView(FIT_VIEW)); - return () => cancelAnimationFrame(t); - }, [owner, file, fitView]); - - const selected = parseGraphNodeId(selectedId); - const scriptsByName = useMemo(() => { const map = new Map(); for (const s of scripts) { @@ -126,6 +118,29 @@ function GraphTabInner({ return map; }, [profiles]); + const { edges } = useMemo(() => buildGraphElements(doc, positions), [doc, positions]); + + useEffect(() => { + const built = buildGraphElements(doc, positions); + setNodes((prev) => { + const selected = new Set(prev.filter((n) => n.selected).map((n) => n.id)); + return built.nodes.map((n) => ({ + ...n, + data: { ...n.data, ...stepIconData(n, scriptsByName, profilesByName) }, + selected: selected.has(n.id), + deletable: !disabled, + draggable: !disabled, + })); + }); + }, [doc, disabled, positions, scriptsByName, profilesByName]); + + useEffect(() => { + const t = requestAnimationFrame(() => fitView(FIT_VIEW)); + return () => cancelAnimationFrame(t); + }, [owner, file, fitView]); + + const selected = parseGraphNodeId(selectedId); + const displayEdges = useMemo( () => edges.map((e) => ({ diff --git a/packages/web/src/components/workflow/ScriptCard.jsx b/packages/web/src/components/workflow/ScriptCard.jsx index c57ae54..69b4caf 100644 --- a/packages/web/src/components/workflow/ScriptCard.jsx +++ b/packages/web/src/components/workflow/ScriptCard.jsx @@ -18,7 +18,7 @@ import { LogViewer } from "../LogViewer.jsx"; import { StatusBadge } from "../../lib/format"; import { prettyJson } from "../../lib/script.js"; import { seedTryDialog, stepTryLabel } from "../../lib/try-session.js"; -import { needsMode } from "../../lib/workflow-doc.js"; +import { needsMode, stepCustomName, stepDisplayName } from "../../lib/workflow-doc.js"; import { stepPredecessors, stepSuccessors, @@ -117,7 +117,9 @@ function ScriptCardView({ step.id && otherSteps.some((s) => s.id === step.id && s.uiId !== step.uiId); const preview = previewConfigValue(mergedConfig, meta?.previewConfigKey); const previewFull = configValueText(mergedConfig, meta?.previewConfigKey); - const baseName = step.kind === "set" ? "set" : scriptName || "untitled"; + const scriptFallback = step.kind === "set" ? "set" : scriptName || "untitled"; + const customName = stepCustomName(step); + const baseName = stepDisplayName(step, scriptFallback); const titleFull = previewFull ? `${baseName} (${previewFull})` : baseName; const setConfig = step.kind === "set" ? { expression: step.expression ?? "" } : null; @@ -180,10 +182,18 @@ function ScriptCardView({ >

{baseName} + {customName && scriptFallback && scriptFallback !== customName ? ( + + {scriptFallback} + + ) : null} {!expanded && preview ? ( + {step.kind === "set" ? (
s.id && s.uiId !== step.uiId) - .map((s) => s.id); + const others = otherSteps.filter((s) => s.id && s.uiId !== step.uiId); + const ids = others.map((s) => s.id); + const nameById = new Map( + others.map((s) => [s.id, stepCustomName(s) || null]), + ); function setMode(next) { if (next === "none") onChange({ ...step, needs: null }); @@ -479,6 +500,11 @@ function NeedsEditor({ step, otherSteps, disabled, onChange }) { else onChange({ ...step, needs: {}, when: "" }); } + function idLabel(id) { + const named = nameById.get(id); + return named ? `${id} · ${named}` : id; + } + return (
needs @@ -520,7 +546,7 @@ function NeedsEditor({ step, otherSteps, disabled, onChange }) { }); }} /> - {id} + {idLabel(id)} ); }) @@ -531,6 +557,7 @@ function NeedsEditor({ step, otherSteps, disabled, onChange }) { onChange({ ...step, needs })} /> @@ -539,7 +566,7 @@ function NeedsEditor({ step, otherSteps, disabled, onChange }) { ); } -function NeedsMap({ needs, ids, disabled, onChange }) { +function NeedsMap({ needs, ids, idLabel, disabled, onChange }) { const entries = Object.entries(needs ?? {}); return (
@@ -564,11 +591,11 @@ function NeedsMap({ needs, ids, disabled, onChange }) { onChange={(e) => onChange({ ...needs, [alias]: e.target.value })} > {from && !ids.includes(from) ? ( - + ) : null} {ids.map((id) => ( ))} @@ -874,8 +901,20 @@ function ScriptTryDialog({ } >
+ {!isSet && script ? ( + + ) : null}
-

{isSet ? "set" : script}

+

+ {stepDisplayName(step, isSet ? "set" : script)} +

+ {stepCustomName(step) && script ? ( +

{isSet ? "set" : script}

+ ) : null}

{isSet ? "Dry-run this set with editable data, context, and expression. Does not create an event. Use Apply to card to write the expression back to the step." diff --git a/packages/web/src/components/workflow/graph/GraphNodes.jsx b/packages/web/src/components/workflow/graph/GraphNodes.jsx index e0aa990..9b0df5e 100644 --- a/packages/web/src/components/workflow/graph/GraphNodes.jsx +++ b/packages/web/src/components/workflow/graph/GraphNodes.jsx @@ -1,4 +1,5 @@ import { Handle, Position } from "@xyflow/react"; +import { ScriptIcon } from "../../ScriptIcon.jsx"; export function TriggerGraphNode({ data, selected }) { return ( @@ -19,6 +20,7 @@ export function TriggerGraphNode({ data, selected }) { export function StepGraphNode({ data, selected }) { const kindLabel = data.kind === "set" ? "set" : data.profile ? "profile" : "script"; + const iconScript = data.iconScript || ""; return (

-
-

{kindLabel}

- {data.stepId ? ( - {data.stepId} - ) : null} - {data.when ? ( - - when - - ) : null} - {data.needsMode === "map" ? ( - - map - +
+ {data.kind !== "set" && iconScript ? ( + ) : null} +
+
+

{kindLabel}

+ {data.stepId ? ( + {data.stepId} + ) : null} + {data.when ? ( + + when + + ) : null} + {data.needsMode === "map" ? ( + + map + + ) : null} +
+

+ {data.label} +

+ {data.missingNeeds?.length ? ( +

Unknown needs: {data.missingNeeds.join(", ")}

+ ) : null} +
-

- {data.label} -

- {data.missingNeeds?.length ? ( -

Unknown needs: {data.missingNeeds.join(", ")}

- ) : null} { assert.equal(pruned.byStep.b, undefined); assert.equal(pruned.lastTriedUiId, null); }); + +test("stepTryLabel uses custom name", () => { + assert.equal( + stepTryLabel({ kind: "script", script: "ntfy.js", name: "Notify to channel" }, 0), + "step 1 / Notify to channel", + ); + assert.equal( + stepTryLabel( + { kind: "script", script: "ntfy.js", id: "notify", name: "Notify to channel" }, + 2, + ), + "step 3 / notify (Notify to channel)", + ); +}); diff --git a/packages/web/src/lib/workflow-doc.js b/packages/web/src/lib/workflow-doc.js index 652065e..7dba978 100644 --- a/packages/web/src/lib/workflow-doc.js +++ b/packages/web/src/lib/workflow-doc.js @@ -105,6 +105,19 @@ export function isDagDoc(doc) { return (doc?.scripts ?? []).some((s) => !isEmptyNeeds(s.needs)); } +export function stepCustomName(step) { + return typeof step?.name === "string" ? step.name.trim() : ""; +} + +export function stepDisplayName(step, fallback) { + const custom = stepCustomName(step); + if (custom) return custom; + if (fallback) return fallback; + if (step?.kind === "set") return "set"; + if (step?.profile) return `profile ${step.profile}`; + return step?.script || "untitled"; +} + export function newScriptStep(script, config = {}) { return { uiId: nextUiId("step"), @@ -112,6 +125,7 @@ export function newScriptStep(script, config = {}) { script, profile: "", config: config && typeof config === "object" && !Array.isArray(config) ? { ...config } : {}, + name: "", id: "", when: "", needs: null, @@ -125,6 +139,7 @@ export function newProfileStep(profileName, script = "") { script, profile: profileName, config: {}, + name: "", id: "", when: "", needs: null, @@ -137,6 +152,7 @@ export function newSetStep() { kind: "set", script: "set", expression: "", + name: "", id: "", when: "", needs: null, @@ -200,6 +216,10 @@ function readOnFailureWorkflow(raw) { return typeof raw?.onFailureWorkflow === "string" ? raw.onFailureWorkflow : ""; } +function readStepName(raw) { + return typeof raw === "string" ? raw : ""; +} + function normalizeStep(step) { const uiId = nextUiId("step"); if (typeof step === "string") { @@ -209,6 +229,7 @@ function normalizeStep(step) { script: step, profile: "", config: {}, + name: "", id: "", when: "", needs: null, @@ -221,6 +242,7 @@ function normalizeStep(step) { script: "", profile: "", config: {}, + name: "", id: "", when: "", needs: null, @@ -233,12 +255,13 @@ function normalizeStep(step) { kind: "set", script: "set", expression: typeof spec.expression === "string" ? spec.expression : "", + name: readStepName(step.name), id: typeof step.id === "string" ? step.id : "", when: typeof step.when === "string" ? step.when : "", needs: step.needs ?? null, }; } - const known = new Set(["script", "profile", "config", "id", "when", "needs", "set"]); + const known = new Set(["script", "profile", "config", "name", "id", "when", "needs", "set"]); /** @type {Record} */ const extra = {}; for (const [key, value] of Object.entries(step)) { @@ -254,6 +277,7 @@ function normalizeStep(step) { script: typeof step.script === "string" ? step.script : "", profile: typeof step.profile === "string" ? step.profile : "", config, + name: readStepName(step.name), id: typeof step.id === "string" ? step.id : "", when: typeof step.when === "string" ? step.when : "", needs: step.needs ?? null, @@ -306,10 +330,16 @@ function normalizeTrigger(raw) { }; } +function dumpStepName(step, out) { + const name = stepCustomName(step); + if (name) out.name = name; +} + function dumpStep(step) { if (step.kind === "set") { /** @type {Record} */ const out = {}; + dumpStepName(step, out); if (step.id) out.id = step.id; out.set = { expression: step.expression ?? "", @@ -320,6 +350,7 @@ function dumpStep(step) { } /** @type {Record} */ const out = {}; + dumpStepName(step, out); if (step.id) out.id = step.id; if (step.profile) { out.profile = step.profile; diff --git a/packages/web/src/lib/workflow-doc.test.js b/packages/web/src/lib/workflow-doc.test.js new file mode 100644 index 0000000..e8c213c --- /dev/null +++ b/packages/web/src/lib/workflow-doc.test.js @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + parseWorkflowYaml, + stringifyWorkflowDoc, + stepCustomName, + stepDisplayName, +} from "./workflow-doc.js"; + +test("step name round-trips in yaml", () => { + const { doc, parseError } = parseWorkflowYaml(` +name: demo +scripts: + - name: Notify to channel + script: ntfy.js + config: + url: https://ntfy.sh/demo + - name: Transform + set: + expression: data +`); + assert.equal(parseError, null); + assert.equal(doc.scripts[0].name, "Notify to channel"); + assert.equal(doc.scripts[0].script, "ntfy.js"); + assert.equal(doc.scripts[0].extra, undefined); + assert.equal(doc.scripts[1].name, "Transform"); + assert.equal(doc.scripts[1].kind, "set"); + + const yaml = stringifyWorkflowDoc(doc); + assert.match(yaml, /name: Notify to channel/); + assert.match(yaml, /script: ntfy.js/); + assert.match(yaml, /name: Transform/); +}); + +test("empty step name is omitted from yaml", () => { + const { doc } = parseWorkflowYaml(` +scripts: + - name: " " + script: ntfy.js +`); + assert.equal(doc.scripts[0].name, " "); + assert.equal(stepCustomName(doc.scripts[0]), ""); + const yaml = stringifyWorkflowDoc(doc); + assert.doesNotMatch(yaml, /^\s*name:/m); + assert.match(yaml, /script: ntfy.js/); +}); + +test("stepDisplayName prefers custom name", () => { + assert.equal( + stepDisplayName({ kind: "script", script: "ntfy.js", name: "Notify to channel" }), + "Notify to channel", + ); + assert.equal(stepDisplayName({ kind: "script", script: "ntfy.js" }), "ntfy.js"); + assert.equal(stepDisplayName({ kind: "set" }), "set"); + assert.equal( + stepDisplayName({ kind: "script", script: "ntfy.js" }, "fallback.js"), + "fallback.js", + ); +}); diff --git a/packages/web/src/lib/workflow-graph.js b/packages/web/src/lib/workflow-graph.js index d8542ff..7627375 100644 --- a/packages/web/src/lib/workflow-graph.js +++ b/packages/web/src/lib/workflow-graph.js @@ -1,4 +1,4 @@ -import { isDagDoc, isEmptyNeeds, needsMode, nextStepId } from "./workflow-doc.js"; +import { isDagDoc, isEmptyNeeds, needsMode, nextStepId, stepCustomName } from "./workflow-doc.js"; export const STEP_NODE_PREFIX = "step:"; export const TRIGGER_NODE_PREFIX = "trig:"; @@ -44,6 +44,8 @@ export function triggerSummary(trigger, owner) { } export function stepLabel(step) { + const custom = stepCustomName(step); + if (custom) return custom; if (step?.kind === "set") return step.id ? `${step.id}: set` : "set"; const target = step?.profile ? `profile ${step.profile}` : step?.script || "untitled"; return step?.id ? `${step.id}: ${target}` : target; @@ -254,6 +256,7 @@ export function buildGraphElements(doc, positions = {}) { data: { layoutKey, label: stepLabel(s), + named: Boolean(stepCustomName(s)), kind: s.kind, stepId: s.id || "", when: s.when || "", diff --git a/packages/web/src/lib/workflow-graph.test.js b/packages/web/src/lib/workflow-graph.test.js index dd78ba4..411fd81 100644 --- a/packages/web/src/lib/workflow-graph.test.js +++ b/packages/web/src/lib/workflow-graph.test.js @@ -6,6 +6,7 @@ import { canConnectSteps, enteringDagWouldStripWhen, removeStepEdge, + stepLabel, stepPredecessors, stepSuccessors, wouldCreateCycle, @@ -113,3 +114,15 @@ test("stepSuccessors / stepPredecessors DAG multi", () => { assert.deepEqual(stepPredecessors(scripts, "d"), ["a"]); assert.deepEqual(stepPredecessors(scripts, "a"), []); }); + +test("stepLabel uses custom name", () => { + assert.equal( + stepLabel({ kind: "script", script: "ntfy.js", name: "Notify to channel" }), + "Notify to channel", + ); + assert.equal(stepLabel({ kind: "script", script: "ntfy.js", id: "n" }), "n: ntfy.js"); + assert.equal( + stepLabel({ kind: "set", id: "xform", name: "Shape payload" }), + "Shape payload", + ); +});