easy_flow 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_flow/canvas.js +1 -1
  3. data/app/controllers/concerns/easy_flow/manage/draws_canvas.rb +1 -1
  4. data/app/controllers/easy_flow/manage/canvas_controller.rb +2 -2
  5. data/app/controllers/easy_flow/manage/flows_controller.rb +4 -1
  6. data/app/javascript/easy_flow/canvas/Canvas.jsx +2 -0
  7. data/app/javascript/easy_flow/canvas/Control.jsx +1 -1
  8. data/app/javascript/easy_flow/canvas/StepCard.jsx +5 -2
  9. data/app/jobs/easy_flow/advance_parent_job.rb +9 -0
  10. data/app/models/easy_flow/canvas.rb +25 -4
  11. data/app/models/easy_flow/chosen_flow.rb +15 -0
  12. data/app/models/easy_flow/definition.rb +10 -0
  13. data/app/models/easy_flow/digest.rb +8 -0
  14. data/app/models/easy_flow/flow_step.rb +30 -0
  15. data/app/models/easy_flow/progress/kept.rb +17 -0
  16. data/app/models/easy_flow/progress/loose.rb +4 -0
  17. data/app/models/easy_flow/run.rb +21 -2
  18. data/app/models/easy_flow/runner.rb +6 -0
  19. data/app/models/easy_flow/step_type/declaration.rb +6 -1
  20. data/app/models/easy_flow/step_type.rb +6 -1
  21. data/app/models/easy_flow/terminal.rb +2 -0
  22. data/app/models/easy_flow/validator.rb +37 -2
  23. data/app/views/easy_flow/manage/flows/show.html.erb +4 -0
  24. data/db/migrate/20261007120000_add_parent_run_to_easy_flow_runs.rb +6 -0
  25. data/lib/easy_flow/engine.rb +1 -0
  26. data/lib/easy_flow/version.rb +1 -1
  27. data/lib/easy_flow.rb +2 -1
  28. data/the_local/agents/easy_flow-develop.md +117 -11
  29. data/the_local/agents/easy_flow-info.md +18 -8
  30. data/the_local/agents/easy_flow-install.md +21 -7
  31. data/the_local/interface.yml +3 -0
  32. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf8a8dbfd774f223e6a43af475f4815a3fa9d94c9ce36d58ac546c803e299232
4
- data.tar.gz: b773f634332cb61f248bbfb03879a322be6baaaacec6d7cf0220a70eb743b72e
3
+ metadata.gz: 62d908e1b9ce4b3b8845a80302af79e28801fa9ec737cf3b09c0032f123d6137
4
+ data.tar.gz: 5118f5f7f8a333facae18c4a61e725cdc099d1e97e69c0c26e0065aa427e621b
5
5
  SHA512:
6
- metadata.gz: 6b5de21e4e9c7074abe01b61fab6c3305f34d407dc19584e54c649e046bb138a92067cebaa1774cc15d3f55e2aaa67eb1a6fa73ed0dcec219881f7df31807e4b
7
- data.tar.gz: e833348051900594e25dc04ffc7a90214544deac6b9f5965e9f3ee79cd1152672253f0679122f3503239552fb8a696285d6c51ff66fefaf955d3e958db91d8c8
6
+ metadata.gz: 6cd630296d7d273e0992399e0678dcc655872f95cd7555bb8064f2bb37bf516e2dfcdddbe114551601dc085a0d3c97499f71a20ee1a7b7e993adc1d0aa3c4596
7
+ data.tar.gz: e796ff7990e86671ef6c49f17ee36b45c97fc064c9efdf335c07dfa2e1b63a8c07ee126d41881636171a019c5f77a1213069a1ccef310783331ed14ed19f4bdf
@@ -1 +1 @@
1
- (()=>{var Dt=globalThis.ReactDOM;var{createPortal:Se,createRoot:q,flushSync:Ne,hydrateRoot:ze,preconnect:Be,prefetchDNS:Ee,preinit:Te,preinitModule:$e,preload:De,preloadModule:Ae,requestFormReset:Pe,unstable_batchedUpdates:Le,useFormState:_e,useFormStatus:Ie,version:Me}=Dt;var J=globalThis.React,e=J,{Children:He,Component:je,Fragment:Fe,PureComponent:Ve,StrictMode:We,Suspense:Ye,cloneElement:Ue,createContext:Xe,createElement:Ge,createRef:qe,forwardRef:Je,isValidElement:Ke,lazy:Ze,memo:Qe,startTransition:Re,useCallback:to,useContext:eo,useDebugValue:oo,useDeferredValue:ro,useEffect:E,useId:no,useImperativeHandle:so,useLayoutEffect:K,useMemo:A,useReducer:ao,useRef:j,useState:x,useSyncExternalStore:lo,useTransition:io}=J;var Z=new Map,L=new Map,Q=(t,o)=>Z.set(t,o),R=()=>{L.forEach(t=>t.unmount()),L.clear()},tt=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(s=>{if(L.has(s))return;let r=Z.get(s.dataset.reactUi),a=o(s);L.set(s,a),a.render(e.createElement(r,JSON.parse(s.dataset.props)))});var et=(t,o)=>{let s=()=>tt(t,o);t.addEventListener("turbo:load",s),t.addEventListener("turbo:frame-load",s),t.addEventListener("turbo:before-cache",R),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",s):s()};var At="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",Pt="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",Lt="border border-accent-600 bg-accent-600 text-white",_t=({name:t,connected:o,armed:s,onArm:r,connecting:a})=>e.createElement("button",{onClick:l=>{a||(l.stopPropagation(),r(l))},title:s?"Now choose a step to connect to":"Connect this branch",className:s?Lt:o?Pt:At,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},t||"next"),ot=_t;var It={width:240,boxSizing:"border-box",borderRadius:8},Mt={padding:"8px 14px",textAlign:"center",fontWeight:600},Ot={fontWeight:600,lineHeight:1.3},Ht=(t,o,s)=>{let r=s?"ring-4 ring-accent-600/15":"shadow-sm";return t||s&&!o?`border-accent-600 ${r}`:o?`border-red-600 ${r}`:"border-gray-300 shadow-sm dark:border-zinc-700"},jt=({node:t,selected:o,armed:s,connecting:r,onSelect:a,onArm:l,onDragEnd:i,onDragStart:f})=>{let p=t.begins_here||t.ends_here,d=t.begins_here,m=t.ends_here?[]:t.ports,u=r&&!d;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!d,onDragStart:c=>{c.dataTransfer.effectAllowed="move",c.dataTransfer.setData("text/plain",t.id),f()},onDragEnd:i,onClick:a,className:`bg-white border-2 dark:bg-zinc-900 ${Ht(u,t.violations.length>0,o)}`,style:{...It,padding:p?0:"12px 14px",cursor:d?"default":r?"crosshair":"grab"}},p&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:Mt},t.label),!p&&e.createElement("div",{style:Ot},t.label),!p&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginTop:2}},t.type),t.violations.map(c=>e.createElement("div",{key:c.problem+c.detail,className:"text-red-600 dark:text-red-400",style:{fontSize:11,padding:"0 14px 6px"}},c.problem.replace(/_/g," "),c.detail?`: ${c.detail}`:"")),m.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},m.map(c=>e.createElement(ot,{key:c,name:c,connected:t.connected.includes(c),connecting:r,armed:s===c,onArm:b=>l(c,b)}))))},F=jt;var rt=(t,o)=>t.includes(o)?t.filter(s=>s!==o):[...t,o],nt=(t,o)=>({...o?.choices||{},...t?.choices||{}});var k=(...t)=>t.filter(Boolean).join(" ");var Ft=({as:t="input",disabled:o,className:s,...r})=>e.createElement(t,{...r,disabled:o,className:k("ks-input",o&&"ks-input-disabled",s)}),w=Ft;var Vt=t=>e.createElement(w,{...t,as:"select"}),st=Vt;var Wt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:k("ks-checkbox",t)}),V=Wt;var Yt=t=>(t||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),Ut=({type:t,value:o,choices:s,onChange:r,onSettle:a})=>{if(t==="boolean")return e.createElement(V,{checked:!!o,onChange:l=>a(l.target.checked)});if(t==="select"||t==="previous_step"||t==="from_step")return e.createElement(st,{className:"mb-3",value:o??"",onChange:l=>a(l.target.value)},e.createElement("option",{value:""}),Yt(s).map(l=>e.createElement("option",{key:l.value,value:l.value},l.label)));if(t==="multi_select"){let l=Array.isArray(o)?o:[],i=f=>a(rt(l,f));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(s||[]).map(f=>e.createElement("label",{key:f,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(V,{checked:l.includes(f),onChange:()=>i(f)}),e.createElement("span",null,f))))}return t==="integer"||t==="float"?e.createElement(w,{className:"mb-3",type:"number",step:t==="integer"?"1":"any",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)}):e.createElement(w,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},_=Ut;var at=(t,o,s,r)=>t.map((a,l)=>l===o?{...a,[s]:r}:a);var Xt={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},Gt={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},qt=({variant:t="primary",size:o="md",className:s,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:k("ks-button",Xt[t],Gt[o],s)},r)},y=qt;var Jt=({holds:t,labels:o,rows:s,onChange:r,onSettle:a})=>{let l=Array.isArray(s)?s:[],i=(f,p,d,m)=>{let u=at(l,f,p,d);m?a(u):r(u)};return e.createElement("div",{style:{marginBottom:12}},l.map((f,p)=>e.createElement("div",{key:p,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([d,m])=>e.createElement("label",{key:d,style:{display:"block"}},e.createElement("span",{className:"text-gray-500 dark:text-gray-400",style:{display:"block",marginBottom:2,fontSize:11}},(o||{})[d]||d),e.createElement(_,{type:m,value:f[d],onChange:u=>i(p,d,u,!1),onSettle:u=>i(p,d,u,!0)}))),e.createElement(y,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>a(l.filter((d,m)=>m!==p))},"Remove"))),e.createElement(y,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>a([...l,{}])},"Add"))},lt=Jt;var Kt=({as:t="div",className:o,children:s,...r})=>e.createElement(t,{...r,className:k("ks-panel",o)},s),$=Kt;var I=(t,o)=>({className:s,children:r,...a})=>e.createElement(t,{...a,className:k(o,s)},r),P=I("label","ks-label"),Ro=I("p","ks-hint"),tr=I("p","ks-error"),er=I("span","ks-required");var Zt={width:280,padding:20,overflowY:"auto"},Qt=({node:t,fields:o,holds:s,labels:r,recordLabels:a,choices:l,onSave:i,onDelete:f,onClose:p})=>{let[d,m]=x(t.config);E(()=>m(t.config),[t.id,t.config]);let u=c=>{m(c),JSON.stringify(c)!==JSON.stringify(t.config)&&i(c)};return e.createElement($,{as:"aside",style:Zt,"data-inspector":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600,marginBottom:2}},t.label),e.createElement("button",{title:"Close",onClick:p,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:16}},t.id," \xB7 ",t.type),Object.entries(o).map(([c,b])=>e.createElement(P,{key:c},e.createElement("span",{style:{display:"block",marginBottom:3}},r[c]||c),b==="list"?e.createElement(lt,{holds:s[c]||{},labels:a[c]||{},rows:d[c],onChange:h=>m({...d,[c]:h}),onSettle:h=>u({...d,[c]:h})}):e.createElement(_,{type:b,value:d[c],choices:l[c],onChange:h=>m({...d,[c]:h}),onSettle:h=>u({...d,[c]:h})}))),e.createElement(y,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},it=Qt;var Rt={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},te="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",ee="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",oe=({node:t,dragging:o,onFill:s,onDrop:r})=>e.createElement("div",{ref:t.ref,"data-placeholder":t.id,title:`Choose the step \u201C${t.label}\u201D should lead to`,onClick:s,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?ee:te,style:Rt},e.createElement("div",{style:{fontWeight:600}},t.label),e.createElement("div",{style:{fontSize:11}},o?"drop a step here":"leads nowhere yet \u2014 click to choose")),dt=oe;var re=({entries:t,at:o,onPick:s,onConnect:r,onDismiss:a})=>e.createElement($,{className:"shadow-lg",style:{position:"absolute",zIndex:9,top:o.y,left:o.x,width:210,padding:8,borderRadius:8}},e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{margin:"0 0 6px",fontSize:11}},"Add a step"),t.map(l=>e.createElement(y,{key:l.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>s(l)},l.label,e.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},l.type))),r&&e.createElement(y,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(y,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),ct=re;var ne={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},pt=({label:t,title:o,idle:s,enabled:r,onUse:a})=>e.createElement(y,{variant:"secondary",size:"sm",title:r?o:s,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},t),se=({undoable:t,redoable:o,onAdd:s,onUndo:r,onRedo:a})=>e.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},e.createElement(y,{variant:"secondary",className:"rounded-full",style:ne,title:"Add a step",onClick:s},"+"),e.createElement(pt,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(pt,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),mt=se;var ut=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var ft=({version:t,published:o})=>t?o?o===t?`Version ${t} \xB7 live`:`Version ${t} \xB7 visitors still on ${o}`:`Version ${t} \xB7 never published`:"No version yet";var ae={width:280,padding:20,overflowY:"auto"},W={fontWeight:600,marginBottom:6,marginTop:14},M={fontSize:12,marginBottom:4,lineHeight:1.4},gt={display:"block",marginBottom:3},yt=(t,o,s)=>r=>{r.target.value!==(t[o]??"")&&s({[o]:r.target.value})},le=({flow:t,changes:o,problems:s,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:f,onClose:p})=>e.createElement($,{as:"aside",style:ae,"data-builder-panel":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600},"data-flow-name":!0},t.title||t.slug),e.createElement("button",{title:"Close",onClick:p,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("a",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:12,display:"block"},href:t.history_url,"data-history":!0},ft(t)),e.createElement("h2",{style:W},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...M,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...M,fontWeight:600},"data-notice":!0},a),s.length===0&&!r?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):s.map(d=>e.createElement("p",{key:`${d.node}-${d.problem}`,className:"text-amber-700 dark:text-amber-400",style:M,"data-problem":!0},"\u26A0 ",ut(d))),e.createElement("h2",{style:W},"Changes since the last version"),o.length===0?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing has changed."):o.map((d,m)=>e.createElement("p",{key:m,style:M,"data-change":!0},d)),e.createElement("h2",{style:W},"Details"),e.createElement(P,null,e.createElement("span",{style:gt},"Title"),e.createElement(w,{className:"mb-3",defaultValue:t.title??"",onBlur:yt(t,"title",f),"data-flow-title":!0})),e.createElement(P,null,e.createElement("span",{style:gt},"Start label"),e.createElement(w,{className:"mb-3",defaultValue:t.start_label??"",onBlur:yt(t,"start_label",f),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(y,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(y,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(y,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(y,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),ht=le;var xt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ie=({link:t,onInsert:o,onRemove:s,onDrop:r,dragging:a})=>{let[l,i]=x(!1),f=l||a;return e.createElement("div",{"data-connector":`${t.source}-${t.target}`,style:{position:"absolute",left:t.midX-34,top:t.midY-20,width:68,height:40,zIndex:4},onMouseEnter:()=>i(!0),onMouseLeave:()=>i(!1),onDragEnter:()=>i(!0),onDragLeave:()=>i(!1),onDragOver:p=>{p.preventDefault(),p.dataTransfer.dropEffect="move"},onDrop:p=>{p.preventDefault(),i(!1),r()}},e.createElement("div",{style:{display:"flex",gap:4,justifyContent:"center",alignItems:"center",height:"100%",opacity:f?1:0,transition:"opacity .12s"}},e.createElement(y,{variant:"secondary",title:a?"Move the step here":"Insert a step here",onClick:o,className:"rounded-full data-[over=true]:ring-2 data-[over=true]:ring-accent-600","data-over":!!(a&&l),style:xt},"+"),!a&&e.createElement(y,{variant:"danger",title:"Remove this connection",onClick:s,className:"rounded-full",style:xt},"\xD7")))},bt=ie;var de=({links:t,extent:o,dragging:s,onInsert:r,onRemove:a,onDrop:l})=>e.createElement(e.Fragment,null,e.createElement("svg",{width:o.width,height:o.height,style:{position:"absolute",top:0,left:0,pointerEvents:"none",zIndex:1}},e.createElement("defs",null,e.createElement("marker",{id:"easy_flow-arrow",viewBox:"0 0 10 10",refX:"8",refY:"5",markerWidth:"5",markerHeight:"5",orient:"auto"},e.createElement("path",{d:"M 0 0 L 10 5 L 0 10 z",className:"fill-gray-400 dark:fill-zinc-500"}))),t.map(i=>e.createElement("path",{key:i.id,"data-link":i.id,className:"stroke-gray-400 dark:stroke-zinc-500",fill:"none",strokeWidth:"1.5",markerEnd:"url(#easy_flow-arrow)",d:i.path}))),t.filter(i=>i.label).map(i=>e.createElement("div",{key:`${i.id}-label`,"data-link-label":i.id,className:"bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400",style:{position:"absolute",left:i.midX-10,top:i.midY-18,zIndex:3,fontSize:11,padding:"0 3px"}},i.label)),t.filter(i=>!i.placeholder).map(i=>e.createElement(bt,{key:i.id,link:i,dragging:!!s,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),vt=de;var ce="Your change was not saved because the server could not be reached.",kt=({base:t,token:o,fetch:s,onFlow:r,onError:a,onNotice:l})=>async(i,f,p)=>{let d=await s(t+i,{method:f,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:p&&JSON.stringify(p)}).catch(()=>null);if(!d)return l(null),a(ce);let m=await d.json().catch(()=>({}));if(!d.ok)return l(null),a(m.error||"That change was refused");a(null),l(m.notice||null),r(await(await s(t+".json",{headers:{Accept:"application/json"}})).json())};var pe=(t,o,s)=>{let[r,a]=x(s),[l,i]=x(null),[f,p]=x(null),d=A(()=>kt({base:t,token:o,fetch:(...m)=>window.fetch(...m),onFlow:a,onError:i,onNotice:p}),[t,o]);return{flow:r,error:l,notice:f,send:d}},wt=pe;var O=(t,o,s,r)=>{let a=t.left-s.left+r.scrollLeft,l=t.top-s.top+r.scrollTop;return o==="left"?{x:a,y:l+t.height/2}:o==="right"?{x:a+t.width,y:l+t.height/2}:o==="top"?{x:a+t.width/2,y:l}:{x:a+t.width/2,y:l+t.height}},me=(t,o)=>o==="top"?{x:t.x,y:t.y-3}:o==="left"?{x:t.x-3,y:t.y}:o==="right"?{x:t.x+3,y:t.y}:{x:t.x,y:t.y+3},ue=(t,o,s,r,a)=>{let l={straight:`M ${o.x} ${o.y} L ${s.x} ${s.y}`,turn:`M ${o.x} ${o.y} H ${s.x} V ${s.y}`,lane:`M ${o.x} ${o.y} V ${r} H ${s.x} V ${s.y}`,detour:`M ${o.x} ${o.y} H ${a} V ${s.y} H ${s.x}`}[t.route];return{...t,path:l,midX:t.route==="detour"?a:(o.x+s.x)/2,midY:t.route==="lane"?r:(o.y+s.y)/2}},fe=(t,o,s,r)=>{let[a,l]=x([]),[i,f]=x({width:"100%",height:"100%"});return K(()=>{let p=()=>{let m=o.current?.getBoundingClientRect();m&&(f({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(u=>{let c=s.current[u.source]?.getBoundingClientRect(),b=s.current[u.target]?.getBoundingClientRect();if(!c||!b)return[];let h=O(c,u.leaves,m,o.current),C=me(O(b,u.enters,m,o.current),u.enters),N=(O(c,"bottom",m,o.current).y+O(b,"top",m,o.current).y)/2,z=u.leaves==="left"?Math.min(h.x,C.x)-28:Math.max(h.x,C.x)+28;return[ue(u,h,C,N,z)]})))};p(),window.addEventListener("resize",p);let d=new ResizeObserver(p);return o.current&&d.observe(o.current),()=>{window.removeEventListener("resize",p),d.disconnect()}},r),{links:a,extent:i}},Ct=fe;var St=(t,o)=>{let s=new Set(o),r=t,a=2;for(;s.has(r);)r=`${t}_${a++}`;return r};var he={display:"flex",height:"100%",minHeight:0,fontSize:13},xe={flex:1,overflow:"auto",position:"relative"},be={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},ve={display:"grid",rowGap:64,columnGap:0,padding:40,justifyContent:"center",position:"relative"},ke={position:"sticky",zIndex:8,top:8,margin:"8px auto 0",width:"fit-content",padding:"6px 12px",borderRadius:6,fontSize:12},we=({port:t,onCancel:o})=>e.createElement("div",{className:"bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100",style:ke},"Choose the step \u201C",t||"next","\u201D should lead to \u2014 ",e.createElement("button",{onClick:o,className:"text-accent-800 underline dark:text-accent-100",style:{border:"none",background:"none",cursor:"pointer",fontSize:12,padding:0}},"cancel")),Ce=({base:t,token:o,initial:s})=>{let{flow:r,error:a,notice:l,send:i}=wt(t,o,s),[f,p]=x(null),[d,m]=x(null),[u,c]=x(null),[b,h]=x(null),[C,N]=x(!1),z=j({}),B=j(null);E(()=>{let n=()=>{p(null),N(!0)};return document.addEventListener("easy_flow:open-flow",n),()=>document.removeEventListener("easy_flow:open-flow",n)},[]),E(()=>{if(!C)return;let n=g=>{g.target.closest("[data-builder-panel], [data-open-panel]")||N(!1)};return document.addEventListener("click",n),()=>document.removeEventListener("click",n)},[C]),E(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let zt=A(()=>Object.fromEntries(r.nodes.map(n=>[n.id,n])),[r.nodes]),{links:Bt,extent:Et}=Ct(r,B,z,[r,f,zt]),Y=A(()=>{let n={};return r.violations.forEach(g=>{var v;return(n[v=g.node]||(n[v]=[])).push(g)}),n},[r.violations]),H=r.nodes.filter(n=>!n.loose),U=r.nodes.filter(n=>n.loose),Tt=Math.max(0,...H.map(n=>n.row))+1,$t=Math.max(0,...H.map(n=>n.column))+1,X=n=>{if(!u)return;let[g,v]=u;c(null),g!==n&&i("/edges","POST",{from:g,to:n,on:v||null})},S=r.nodes.find(n=>n.id===f),D=r.palette.find(n=>n.type===S?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:he,onMouseUp:()=>h(null),onKeyDown:n=>{n.key==="Escape"&&(p(null),c(null),m(null))},tabIndex:-1},e.createElement("div",{ref:B,className:"bg-surface-50 dark:bg-surface-950",style:xe,onClick:n=>{n.target===B.current&&(p(null),c(null),N(!1))}},u&&e.createElement(we,{port:u[1],onCancel:()=>c(null)}),e.createElement(mt,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>m({at:{x:16,y:52}}),onUndo:()=>{p(null),i("/undo","POST")},onRedo:()=>{p(null),i("/redo","POST")}}),e.createElement(vt,{links:Bt,extent:Et,dragging:b,onInsert:n=>m({from:n.source,to:n.target,at:{x:n.midX+30,y:n.midY}}),onRemove:n=>{p(null),i("/edges","DELETE",{from:n.source,to:n.target})},onDrop:n=>{let g=b;h(null),i("/steps/"+g+"/move","PATCH",{from:n.source,to:n.target})}}),U.length>0&&e.createElement("div",{"data-loose":!0,className:"border border-dashed border-gray-300 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-900",style:be},e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:6}},"Not in the flow yet \u2014 drag one onto a connection"),U.map(n=>e.createElement("div",{key:n.id,style:{marginBottom:8}},e.createElement(F,{node:{...n,ref:g=>{z.current[n.id]=g},violations:Y[n.id]||[],connected:[]},selected:f===n.id,armed:u&&u[0]===n.id?u[1]:null,connecting:!!u,onSelect:()=>u?X(n.id):p(n.id),onArm:(g,v)=>{v.stopPropagation(),c([n.id,g])},onDragStart:()=>h(n.id),onDragEnd:()=>h(null)})))),e.createElement("div",{style:{...ve,gridTemplateColumns:`repeat(${$t+1}, ${280/2}px)`,gridTemplateRows:`repeat(${Tt}, auto)`}},H.map(n=>e.createElement("div",{key:n.id,style:{gridRow:n.row+1,gridColumn:`${n.column+1} / span 2`,zIndex:2}},n.placeholder&&e.createElement(dt,{node:{...n,ref:g=>{z.current[n.id]=g}},dragging:!!b,onFill:()=>m({from:n.from,on:n.on,at:{x:16,y:52}}),onDrop:()=>{let g=b;h(null),g&&i("/edges","POST",{from:n.from,to:g,on:n.on})}}),!n.placeholder&&e.createElement(F,{node:{...n,ref:g=>{z.current[n.id]=g},violations:Y[n.id]||[],connected:r.edges.filter(g=>g.source===n.id).map(g=>g.label||null)},selected:n.id===f,armed:u&&u[0]===n.id?u[1]:null,connecting:!!u&&u[0]!==n.id,onSelect:()=>u?X(n.id):p(n.id),onArm:(g,v)=>{let G=B.current.getBoundingClientRect();m({from:n.id,on:g,at:{x:v.clientX-G.left+B.current.scrollLeft+8,y:v.clientY-G.top+B.current.scrollTop+8}})},onDragEnd:()=>h(null),onDragStart:()=>h(n.id)})))),d&&e.createElement(ct,{entries:r.palette,at:d.at,onDismiss:()=>m(null),onConnect:d.on!==void 0?()=>{c([d.from,d.on]),m(null)}:null,onPick:n=>{let g=d;m(null),i("/steps","POST",{id:St(n.type,r.nodes.map(v=>v.id)),type:n.type,from:g.from,to:g.to,on:g.on})}})),C&&!S&&e.createElement(ht,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>N(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:n=>i("/details","PATCH",{flow:n})}),S&&e.createElement(it,{node:S,fields:D?.fields||{},holds:D?.records||{},labels:D?.labels||{},recordLabels:D?.record_labels||{},choices:nt(S,D),onClose:()=>p(null),onSave:n=>i(`/steps/${S.id}`,"PATCH",{config:n}),onDelete:()=>{p(null),i(`/steps/${S.id}`,"DELETE")}}))},Nt=Ce;Q("easy_flow/flow-editor",Nt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});et(document,q);})();
1
+ (()=>{var Dt=globalThis.ReactDOM;var{createPortal:Se,createRoot:q,flushSync:Ne,hydrateRoot:ze,preconnect:Be,prefetchDNS:Ee,preinit:Te,preinitModule:$e,preload:De,preloadModule:Ae,requestFormReset:Pe,unstable_batchedUpdates:Le,useFormState:_e,useFormStatus:Ie,version:Me}=Dt;var J=globalThis.React,e=J,{Children:He,Component:je,Fragment:Fe,PureComponent:Ve,StrictMode:We,Suspense:Ye,cloneElement:Ue,createContext:Xe,createElement:Ge,createRef:qe,forwardRef:Je,isValidElement:Ke,lazy:Ze,memo:Qe,startTransition:Re,useCallback:to,useContext:eo,useDebugValue:oo,useDeferredValue:ro,useEffect:E,useId:no,useImperativeHandle:so,useLayoutEffect:K,useMemo:A,useReducer:ao,useRef:j,useState:b,useSyncExternalStore:lo,useTransition:io}=J;var Z=new Map,L=new Map,Q=(t,o)=>Z.set(t,o),R=()=>{L.forEach(t=>t.unmount()),L.clear()},tt=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(s=>{if(L.has(s))return;let r=Z.get(s.dataset.reactUi),a=o(s);L.set(s,a),a.render(e.createElement(r,JSON.parse(s.dataset.props)))});var et=(t,o)=>{let s=()=>tt(t,o);t.addEventListener("turbo:load",s),t.addEventListener("turbo:frame-load",s),t.addEventListener("turbo:before-cache",R),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",s):s()};var w=(...t)=>t.filter(Boolean).join(" ");var At={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},Pt={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},Lt=({variant:t="primary",size:o="md",className:s,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:w("ks-button",At[t],Pt[o],s)},r)},h=Lt;var _t="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",It="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",Mt="border border-accent-600 bg-accent-600 text-white",Ot=({name:t,connected:o,armed:s,onArm:r,connecting:a})=>e.createElement("button",{onClick:l=>{a||(l.stopPropagation(),r(l))},title:s?"Now choose a step to connect to":"Connect this branch",className:s?Mt:o?It:_t,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},t||"next"),ot=Ot;var Ht={width:240,boxSizing:"border-box",borderRadius:8},jt={padding:"8px 14px",textAlign:"center",fontWeight:600},Ft={fontWeight:600,lineHeight:1.3},Vt=(t,o,s)=>{let r=s?"ring-4 ring-accent-600/15":"shadow-sm";return t||s&&!o?`border-accent-600 ${r}`:o?`border-red-600 ${r}`:"border-gray-300 shadow-sm dark:border-zinc-700"},Wt=({node:t,selected:o,armed:s,connecting:r,onSelect:a,onOpen:l,onArm:i,onDragEnd:f,onDragStart:c})=>{let d=t.begins_here||t.ends_here,p=t.begins_here,m=t.ends_here?[]:t.ports,g=r&&!p;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!p,onDragStart:y=>{y.dataTransfer.effectAllowed="move",y.dataTransfer.setData("text/plain",t.id),c()},onDragEnd:f,onClick:t.opens&&!r?()=>l(t.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Vt(g,t.violations.length>0,o)}`,style:{...Ht,padding:d?0:"12px 14px",cursor:p?"default":r?"crosshair":"grab"}},d&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:jt},t.label),!d&&e.createElement("div",{style:Ft},t.label),!d&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginTop:2}},t.type),t.starts_a_flow&&e.createElement("div",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:11,marginTop:2}},"Runs a flow"),t.opens&&e.createElement(h,{variant:"secondary",size:"sm",className:"mt-2",onClick:y=>{y.stopPropagation(),a()}},"Settings"),t.violations.map(y=>e.createElement("div",{key:y.problem+y.detail,className:"text-red-600 dark:text-red-400",style:{fontSize:11,padding:"0 14px 6px"}},y.problem.replace(/_/g," "),y.detail?`: ${y.detail}`:"")),m.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},m.map(y=>e.createElement(ot,{key:y,name:y,connected:t.connected.includes(y),connecting:r,armed:s===y,onArm:x=>i(y,x)}))))},F=Wt;var rt=(t,o)=>t.includes(o)?t.filter(s=>s!==o):[...t,o],nt=(t,o)=>({...o?.choices||{},...t?.choices||{}});var Yt=({as:t="input",disabled:o,className:s,...r})=>e.createElement(t,{...r,disabled:o,className:w("ks-input",o&&"ks-input-disabled",s)}),k=Yt;var Ut=t=>e.createElement(k,{...t,as:"select"}),st=Ut;var Xt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:w("ks-checkbox",t)}),V=Xt;var Gt=t=>(t||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),qt=({type:t,value:o,choices:s,onChange:r,onSettle:a})=>{if(t==="boolean")return e.createElement(V,{checked:!!o,onChange:l=>a(l.target.checked)});if(t==="select"||t==="previous_step"||t==="from_step"||t==="flow")return e.createElement(st,{className:"mb-3",value:o??"",onChange:l=>a(l.target.value)},e.createElement("option",{value:""}),Gt(s).map(l=>e.createElement("option",{key:l.value,value:l.value},l.label)));if(t==="multi_select"){let l=Array.isArray(o)?o:[],i=f=>a(rt(l,f));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(s||[]).map(f=>e.createElement("label",{key:f,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(V,{checked:l.includes(f),onChange:()=>i(f)}),e.createElement("span",null,f))))}return t==="integer"||t==="float"?e.createElement(k,{className:"mb-3",type:"number",step:t==="integer"?"1":"any",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)}):e.createElement(k,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},_=qt;var at=(t,o,s,r)=>t.map((a,l)=>l===o?{...a,[s]:r}:a);var Jt=({holds:t,labels:o,rows:s,onChange:r,onSettle:a})=>{let l=Array.isArray(s)?s:[],i=(f,c,d,p)=>{let m=at(l,f,c,d);p?a(m):r(m)};return e.createElement("div",{style:{marginBottom:12}},l.map((f,c)=>e.createElement("div",{key:c,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([d,p])=>e.createElement("label",{key:d,style:{display:"block"}},e.createElement("span",{className:"text-gray-500 dark:text-gray-400",style:{display:"block",marginBottom:2,fontSize:11}},(o||{})[d]||d),e.createElement(_,{type:p,value:f[d],onChange:m=>i(c,d,m,!1),onSettle:m=>i(c,d,m,!0)}))),e.createElement(h,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>a(l.filter((d,p)=>p!==c))},"Remove"))),e.createElement(h,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>a([...l,{}])},"Add"))},lt=Jt;var Kt=({as:t="div",className:o,children:s,...r})=>e.createElement(t,{...r,className:w("ks-panel",o)},s),$=Kt;var I=(t,o)=>({className:s,children:r,...a})=>e.createElement(t,{...a,className:w(o,s)},r),P=I("label","ks-label"),tr=I("p","ks-hint"),er=I("p","ks-error"),or=I("span","ks-required");var Zt={width:280,padding:20,overflowY:"auto"},Qt=({node:t,fields:o,holds:s,labels:r,recordLabels:a,choices:l,onSave:i,onDelete:f,onClose:c})=>{let[d,p]=b(t.config);E(()=>p(t.config),[t.id,t.config]);let m=g=>{p(g),JSON.stringify(g)!==JSON.stringify(t.config)&&i(g)};return e.createElement($,{as:"aside",style:Zt,"data-inspector":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600,marginBottom:2}},t.label),e.createElement("button",{title:"Close",onClick:c,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:16}},t.id," \xB7 ",t.type),Object.entries(o).map(([g,y])=>e.createElement(P,{key:g},e.createElement("span",{style:{display:"block",marginBottom:3}},r[g]||g),y==="list"?e.createElement(lt,{holds:s[g]||{},labels:a[g]||{},rows:d[g],onChange:x=>p({...d,[g]:x}),onSettle:x=>m({...d,[g]:x})}):e.createElement(_,{type:y,value:d[g],choices:l[g],onChange:x=>p({...d,[g]:x}),onSettle:x=>m({...d,[g]:x})}))),e.createElement(h,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},it=Qt;var Rt={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},te="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",ee="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",oe=({node:t,dragging:o,onFill:s,onDrop:r})=>e.createElement("div",{ref:t.ref,"data-placeholder":t.id,title:`Choose the step \u201C${t.label}\u201D should lead to`,onClick:s,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?ee:te,style:Rt},e.createElement("div",{style:{fontWeight:600}},t.label),e.createElement("div",{style:{fontSize:11}},o?"drop a step here":"leads nowhere yet \u2014 click to choose")),dt=oe;var re=({entries:t,at:o,onPick:s,onConnect:r,onDismiss:a})=>e.createElement($,{className:"shadow-lg",style:{position:"absolute",zIndex:9,top:o.y,left:o.x,width:210,padding:8,borderRadius:8}},e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{margin:"0 0 6px",fontSize:11}},"Add a step"),t.map(l=>e.createElement(h,{key:l.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>s(l)},l.label,e.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},l.type))),r&&e.createElement(h,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(h,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),ct=re;var ne={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},pt=({label:t,title:o,idle:s,enabled:r,onUse:a})=>e.createElement(h,{variant:"secondary",size:"sm",title:r?o:s,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},t),se=({undoable:t,redoable:o,onAdd:s,onUndo:r,onRedo:a})=>e.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},e.createElement(h,{variant:"secondary",className:"rounded-full",style:ne,title:"Add a step",onClick:s},"+"),e.createElement(pt,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(pt,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),mt=se;var ut=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var ft=({version:t,published:o})=>t?o?o===t?`Version ${t} \xB7 live`:`Version ${t} \xB7 visitors still on ${o}`:`Version ${t} \xB7 never published`:"No version yet";var ae={width:280,padding:20,overflowY:"auto"},W={fontWeight:600,marginBottom:6,marginTop:14},M={fontSize:12,marginBottom:4,lineHeight:1.4},gt={display:"block",marginBottom:3},yt=(t,o,s)=>r=>{r.target.value!==(t[o]??"")&&s({[o]:r.target.value})},le=({flow:t,changes:o,problems:s,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:f,onClose:c})=>e.createElement($,{as:"aside",style:ae,"data-builder-panel":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600},"data-flow-name":!0},t.title||t.slug),e.createElement("button",{title:"Close",onClick:c,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("a",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:12,display:"block"},href:t.history_url,"data-history":!0},ft(t)),e.createElement("h2",{style:W},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...M,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...M,fontWeight:600},"data-notice":!0},a),s.length===0&&!r?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):s.map(d=>e.createElement("p",{key:`${d.node}-${d.problem}`,className:"text-amber-700 dark:text-amber-400",style:M,"data-problem":!0},"\u26A0 ",ut(d))),e.createElement("h2",{style:W},"Changes since the last version"),o.length===0?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing has changed."):o.map((d,p)=>e.createElement("p",{key:p,style:M,"data-change":!0},d)),e.createElement("h2",{style:W},"Details"),e.createElement(P,null,e.createElement("span",{style:gt},"Title"),e.createElement(k,{className:"mb-3",defaultValue:t.title??"",onBlur:yt(t,"title",f),"data-flow-title":!0})),e.createElement(P,null,e.createElement("span",{style:gt},"Start label"),e.createElement(k,{className:"mb-3",defaultValue:t.start_label??"",onBlur:yt(t,"start_label",f),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(h,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(h,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(h,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(h,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),ht=le;var xt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ie=({link:t,onInsert:o,onRemove:s,onDrop:r,dragging:a})=>{let[l,i]=b(!1),f=l||a;return e.createElement("div",{"data-connector":`${t.source}-${t.target}`,style:{position:"absolute",left:t.midX-34,top:t.midY-20,width:68,height:40,zIndex:4},onMouseEnter:()=>i(!0),onMouseLeave:()=>i(!1),onDragEnter:()=>i(!0),onDragLeave:()=>i(!1),onDragOver:c=>{c.preventDefault(),c.dataTransfer.dropEffect="move"},onDrop:c=>{c.preventDefault(),i(!1),r()}},e.createElement("div",{style:{display:"flex",gap:4,justifyContent:"center",alignItems:"center",height:"100%",opacity:f?1:0,transition:"opacity .12s"}},e.createElement(h,{variant:"secondary",title:a?"Move the step here":"Insert a step here",onClick:o,className:"rounded-full data-[over=true]:ring-2 data-[over=true]:ring-accent-600","data-over":!!(a&&l),style:xt},"+"),!a&&e.createElement(h,{variant:"danger",title:"Remove this connection",onClick:s,className:"rounded-full",style:xt},"\xD7")))},bt=ie;var de=({links:t,extent:o,dragging:s,onInsert:r,onRemove:a,onDrop:l})=>e.createElement(e.Fragment,null,e.createElement("svg",{width:o.width,height:o.height,style:{position:"absolute",top:0,left:0,pointerEvents:"none",zIndex:1}},e.createElement("defs",null,e.createElement("marker",{id:"easy_flow-arrow",viewBox:"0 0 10 10",refX:"8",refY:"5",markerWidth:"5",markerHeight:"5",orient:"auto"},e.createElement("path",{d:"M 0 0 L 10 5 L 0 10 z",className:"fill-gray-400 dark:fill-zinc-500"}))),t.map(i=>e.createElement("path",{key:i.id,"data-link":i.id,className:"stroke-gray-400 dark:stroke-zinc-500",fill:"none",strokeWidth:"1.5",markerEnd:"url(#easy_flow-arrow)",d:i.path}))),t.filter(i=>i.label).map(i=>e.createElement("div",{key:`${i.id}-label`,"data-link-label":i.id,className:"bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400",style:{position:"absolute",left:i.midX-10,top:i.midY-18,zIndex:3,fontSize:11,padding:"0 3px"}},i.label)),t.filter(i=>!i.placeholder).map(i=>e.createElement(bt,{key:i.id,link:i,dragging:!!s,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),vt=de;var ce="Your change was not saved because the server could not be reached.",wt=({base:t,token:o,fetch:s,onFlow:r,onError:a,onNotice:l})=>async(i,f,c)=>{let d=await s(t+i,{method:f,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:c&&JSON.stringify(c)}).catch(()=>null);if(!d)return l(null),a(ce);let p=await d.json().catch(()=>({}));if(!d.ok)return l(null),a(p.error||"That change was refused");a(null),l(p.notice||null),r(await(await s(t+".json",{headers:{Accept:"application/json"}})).json())};var pe=(t,o,s)=>{let[r,a]=b(s),[l,i]=b(null),[f,c]=b(null),d=A(()=>wt({base:t,token:o,fetch:(...p)=>window.fetch(...p),onFlow:a,onError:i,onNotice:c}),[t,o]);return{flow:r,error:l,notice:f,send:d}},kt=pe;var O=(t,o,s,r)=>{let a=t.left-s.left+r.scrollLeft,l=t.top-s.top+r.scrollTop;return o==="left"?{x:a,y:l+t.height/2}:o==="right"?{x:a+t.width,y:l+t.height/2}:o==="top"?{x:a+t.width/2,y:l}:{x:a+t.width/2,y:l+t.height}},me=(t,o)=>o==="top"?{x:t.x,y:t.y-3}:o==="left"?{x:t.x-3,y:t.y}:o==="right"?{x:t.x+3,y:t.y}:{x:t.x,y:t.y+3},ue=(t,o,s,r,a)=>{let l={straight:`M ${o.x} ${o.y} L ${s.x} ${s.y}`,turn:`M ${o.x} ${o.y} H ${s.x} V ${s.y}`,lane:`M ${o.x} ${o.y} V ${r} H ${s.x} V ${s.y}`,detour:`M ${o.x} ${o.y} H ${a} V ${s.y} H ${s.x}`}[t.route];return{...t,path:l,midX:t.route==="detour"?a:(o.x+s.x)/2,midY:t.route==="lane"?r:(o.y+s.y)/2}},fe=(t,o,s,r)=>{let[a,l]=b([]),[i,f]=b({width:"100%",height:"100%"});return K(()=>{let c=()=>{let p=o.current?.getBoundingClientRect();p&&(f({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(m=>{let g=s.current[m.source]?.getBoundingClientRect(),y=s.current[m.target]?.getBoundingClientRect();if(!g||!y)return[];let x=O(g,m.leaves,p,o.current),C=me(O(y,m.enters,p,o.current),m.enters),N=(O(g,"bottom",p,o.current).y+O(y,"top",p,o.current).y)/2,z=m.leaves==="left"?Math.min(x.x,C.x)-28:Math.max(x.x,C.x)+28;return[ue(m,x,C,N,z)]})))};c(),window.addEventListener("resize",c);let d=new ResizeObserver(c);return o.current&&d.observe(o.current),()=>{window.removeEventListener("resize",c),d.disconnect()}},r),{links:a,extent:i}},Ct=fe;var St=(t,o)=>{let s=new Set(o),r=t,a=2;for(;s.has(r);)r=`${t}_${a++}`;return r};var he={display:"flex",height:"100%",minHeight:0,fontSize:13},xe={flex:1,overflow:"auto",position:"relative"},be={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},ve={display:"grid",rowGap:64,columnGap:0,padding:40,justifyContent:"center",position:"relative"},we={position:"sticky",zIndex:8,top:8,margin:"8px auto 0",width:"fit-content",padding:"6px 12px",borderRadius:6,fontSize:12},ke=({port:t,onCancel:o})=>e.createElement("div",{className:"bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100",style:we},"Choose the step \u201C",t||"next","\u201D should lead to \u2014 ",e.createElement("button",{onClick:o,className:"text-accent-800 underline dark:text-accent-100",style:{border:"none",background:"none",cursor:"pointer",fontSize:12,padding:0}},"cancel")),Ce=({base:t,token:o,initial:s})=>{let{flow:r,error:a,notice:l,send:i}=kt(t,o,s),[f,c]=b(null),[d,p]=b(null),[m,g]=b(null),[y,x]=b(null),[C,N]=b(!1),z=j({}),B=j(null);E(()=>{let n=()=>{c(null),N(!0)};return document.addEventListener("easy_flow:open-flow",n),()=>document.removeEventListener("easy_flow:open-flow",n)},[]),E(()=>{if(!C)return;let n=u=>{u.target.closest("[data-builder-panel], [data-open-panel]")||N(!1)};return document.addEventListener("click",n),()=>document.removeEventListener("click",n)},[C]),E(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let zt=A(()=>Object.fromEntries(r.nodes.map(n=>[n.id,n])),[r.nodes]),{links:Bt,extent:Et}=Ct(r,B,z,[r,f,zt]),Y=A(()=>{let n={};return r.violations.forEach(u=>{var v;return(n[v=u.node]||(n[v]=[])).push(u)}),n},[r.violations]),H=r.nodes.filter(n=>!n.loose),U=r.nodes.filter(n=>n.loose),Tt=Math.max(0,...H.map(n=>n.row))+1,$t=Math.max(0,...H.map(n=>n.column))+1,X=n=>{if(!m)return;let[u,v]=m;g(null),u!==n&&i("/edges","POST",{from:u,to:n,on:v||null})},S=r.nodes.find(n=>n.id===f),D=r.palette.find(n=>n.type===S?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:he,onMouseUp:()=>x(null),onKeyDown:n=>{n.key==="Escape"&&(c(null),g(null),p(null))},tabIndex:-1},e.createElement("div",{ref:B,className:"bg-surface-50 dark:bg-surface-950",style:xe,onClick:n=>{n.target===B.current&&(c(null),g(null),N(!1))}},m&&e.createElement(ke,{port:m[1],onCancel:()=>g(null)}),e.createElement(mt,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>p({at:{x:16,y:52}}),onUndo:()=>{c(null),i("/undo","POST")},onRedo:()=>{c(null),i("/redo","POST")}}),e.createElement(vt,{links:Bt,extent:Et,dragging:y,onInsert:n=>p({from:n.source,to:n.target,at:{x:n.midX+30,y:n.midY}}),onRemove:n=>{c(null),i("/edges","DELETE",{from:n.source,to:n.target})},onDrop:n=>{let u=y;x(null),i("/steps/"+u+"/move","PATCH",{from:n.source,to:n.target})}}),U.length>0&&e.createElement("div",{"data-loose":!0,className:"border border-dashed border-gray-300 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-900",style:be},e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:6}},"Not in the flow yet \u2014 drag one onto a connection"),U.map(n=>e.createElement("div",{key:n.id,style:{marginBottom:8}},e.createElement(F,{node:{...n,ref:u=>{z.current[n.id]=u},violations:Y[n.id]||[],connected:[]},selected:f===n.id,armed:m&&m[0]===n.id?m[1]:null,connecting:!!m,onSelect:()=>m?X(n.id):c(n.id),onOpen:u=>window.location.assign(u),onArm:(u,v)=>{v.stopPropagation(),g([n.id,u])},onDragStart:()=>x(n.id),onDragEnd:()=>x(null)})))),e.createElement("div",{style:{...ve,gridTemplateColumns:`repeat(${$t+1}, ${280/2}px)`,gridTemplateRows:`repeat(${Tt}, auto)`}},H.map(n=>e.createElement("div",{key:n.id,style:{gridRow:n.row+1,gridColumn:`${n.column+1} / span 2`,zIndex:2}},n.placeholder&&e.createElement(dt,{node:{...n,ref:u=>{z.current[n.id]=u}},dragging:!!y,onFill:()=>p({from:n.from,on:n.on,at:{x:16,y:52}}),onDrop:()=>{let u=y;x(null),u&&i("/edges","POST",{from:n.from,to:u,on:n.on})}}),!n.placeholder&&e.createElement(F,{node:{...n,ref:u=>{z.current[n.id]=u},violations:Y[n.id]||[],connected:r.edges.filter(u=>u.source===n.id).map(u=>u.label||null)},selected:n.id===f,armed:m&&m[0]===n.id?m[1]:null,connecting:!!m&&m[0]!==n.id,onSelect:()=>m?X(n.id):c(n.id),onOpen:u=>window.location.assign(u),onArm:(u,v)=>{let G=B.current.getBoundingClientRect();p({from:n.id,on:u,at:{x:v.clientX-G.left+B.current.scrollLeft+8,y:v.clientY-G.top+B.current.scrollTop+8}})},onDragEnd:()=>x(null),onDragStart:()=>x(n.id)})))),d&&e.createElement(ct,{entries:r.palette,at:d.at,onDismiss:()=>p(null),onConnect:d.on!==void 0?()=>{g([d.from,d.on]),p(null)}:null,onPick:n=>{let u=d;p(null),i("/steps","POST",{id:St(n.type,r.nodes.map(v=>v.id)),type:n.type,from:u.from,to:u.to,on:u.on})}})),C&&!S&&e.createElement(ht,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>N(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:n=>i("/details","PATCH",{flow:n})}),S&&e.createElement(it,{node:S,fields:D?.fields||{},holds:D?.records||{},labels:D?.labels||{},recordLabels:D?.record_labels||{},choices:nt(S,D),onClose:()=>c(null),onSave:n=>i(`/steps/${S.id}`,"PATCH",{config:n}),onDelete:()=>{c(null),i(`/steps/${S.id}`,"DELETE")}}))},Nt=Ce;Q("easy_flow/flow-editor",Nt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});et(document,q);})();
@@ -6,7 +6,7 @@ module EasyFlow
6
6
  private
7
7
 
8
8
  def canvas_payload(flow)
9
- Canvas.new(canvas_document(flow), host: flow_host).to_h
9
+ Canvas.new(canvas_document(flow), host: flow_host, flows: hosted_flows.order(:title), flow_path: ->(id) { manage_flow_path(id, from: flow.id) }, flow: flow).to_h
10
10
  .merge("undoable" => flow.edit_history.undoable?, "redoable" => flow.edit_history.redoable?,
11
11
  "changes" => listed_changes(flow), "flow" => flow_details(flow))
12
12
  end
@@ -7,7 +7,7 @@ module EasyFlow
7
7
  end
8
8
 
9
9
  def create
10
- objections = Validator.new(document).violations
10
+ objections = Validator.new(document, flow: flow).violations
11
11
  return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
12
12
 
13
13
  stood_at = flow.current_definition_version&.number
@@ -17,7 +17,7 @@ module EasyFlow
17
17
  end
18
18
 
19
19
  def publish
20
- objections = Validator.new(document).violations
20
+ objections = Validator.new(document, flow: flow).violations
21
21
  return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
22
22
 
23
23
  ran = flow.live_version&.number
@@ -22,6 +22,7 @@ module EasyFlow
22
22
 
23
23
  def show
24
24
  @flow = hosted_flows.find(params[:id])
25
+ @parent = hosted_flows.find_by(id: params[:from])
25
26
  @canvas = canvas_payload(@flow)
26
27
  end
27
28
 
@@ -36,7 +37,9 @@ module EasyFlow
36
37
  end
37
38
 
38
39
  def destroy
39
- hosted_flows.find(params[:id]).destroy!
40
+ flow = hosted_flows.find(params[:id])
41
+ return redirect_to manage_flows_path, alert: flow.errors.full_messages.to_sentence unless flow.destroy
42
+
40
43
  redirect_to manage_flows_path, notice: "Flow removed."
41
44
  end
42
45
 
@@ -115,6 +115,7 @@ const Canvas = ({ base, token, initial }) => {
115
115
  armed={armed && armed[0] === node.id ? armed[1] : null}
116
116
  connecting={Boolean(armed)}
117
117
  onSelect={() => (armed ? connectTo(node.id) : setSelected(node.id))}
118
+ onOpen={(path) => window.location.assign(path)}
118
119
  onArm={(port, event) => { event.stopPropagation(); setArmed([ node.id, port ]) }}
119
120
  onDragStart={() => setDragging(node.id)}
120
121
  onDragEnd={() => setDragging(null)} />
@@ -147,6 +148,7 @@ const Canvas = ({ base, token, initial }) => {
147
148
  armed={armed && armed[0] === node.id ? armed[1] : null}
148
149
  connecting={Boolean(armed) && armed[0] !== node.id}
149
150
  onSelect={() => (armed ? connectTo(node.id) : setSelected(node.id))}
151
+ onOpen={(path) => window.location.assign(path)}
150
152
  onArm={(port, event) => {
151
153
  const frame = surface.current.getBoundingClientRect()
152
154
  setAdding({
@@ -10,7 +10,7 @@ const offered = (choices) =>
10
10
  const Control = ({ type, value, choices, onChange, onSettle }) => {
11
11
  if (type === "boolean") return <Checkbox checked={Boolean(value)} onChange={(e) => onSettle(e.target.checked)} />
12
12
 
13
- if (type === "select" || type === "previous_step" || type === "from_step") {
13
+ if (type === "select" || type === "previous_step" || type === "from_step" || type === "flow") {
14
14
  return <Select className="mb-3" value={value ?? ""} onChange={(e) => onSettle(e.target.value)}>
15
15
  <option value=""></option>
16
16
  {offered(choices).map((choice) => <option key={choice.value} value={choice.value}>{choice.label}</option>)}
@@ -1,4 +1,5 @@
1
1
  import React from "react"
2
+ import Button from "keystone_ui-react/src/Button.jsx"
2
3
  import Port from "./Port"
3
4
  import { CARD } from "./styles"
4
5
 
@@ -15,7 +16,7 @@ const edge = (target, troubled, selected) => {
15
16
  return "border-gray-300 shadow-sm dark:border-zinc-700"
16
17
  }
17
18
 
18
- const StepCard = ({ node, selected, armed, connecting, onSelect, onArm, onDragEnd, onDragStart }) => {
19
+ const StepCard = ({ node, selected, armed, connecting, onSelect, onOpen, onArm, onDragEnd, onDragStart }) => {
19
20
  const bookend = node.begins_here || node.ends_here
20
21
  const pinned = node.begins_here
21
22
  const ports = node.ends_here ? [] : node.ports
@@ -27,7 +28,7 @@ const StepCard = ({ node, selected, armed, connecting, onSelect, onArm, onDragEn
27
28
  draggable={!pinned}
28
29
  onDragStart={(event) => { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", node.id); onDragStart() }}
29
30
  onDragEnd={onDragEnd}
30
- onClick={onSelect}
31
+ onClick={node.opens && !connecting ? () => onOpen(node.opens) : onSelect}
31
32
  className={`bg-white border-2 dark:bg-zinc-900 ${edge(target, node.violations.length > 0, selected)}`}
32
33
  style={{
33
34
  ...card,
@@ -37,6 +38,8 @@ const StepCard = ({ node, selected, armed, connecting, onSelect, onArm, onDragEn
37
38
  {bookend && <div className="text-gray-500 dark:text-gray-400" style={bookendCard}>{node.label}</div>}
38
39
  {!bookend && <div style={named}>{node.label}</div>}
39
40
  {!bookend && <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginTop: 2 }}>{node.type}</div>}
41
+ {node.starts_a_flow && <div className="text-accent-600 dark:text-accent-400" style={{ fontSize: 11, marginTop: 2 }}>Runs a flow</div>}
42
+ {node.opens && <Button variant="secondary" size="sm" className="mt-2" onClick={(event) => { event.stopPropagation(); onSelect() }}>Settings</Button>}
40
43
  {node.violations.map((violation) => (
41
44
  <div key={violation.problem + violation.detail} className="text-red-600 dark:text-red-400" style={{ fontSize: 11, padding: "0 14px 6px" }}>
42
45
  {violation.problem.replace(/_/g, " ")}{violation.detail ? `: ${violation.detail}` : ""}
@@ -0,0 +1,9 @@
1
+ module EasyFlow
2
+ class AdvanceParentJob < ActiveJob::Base
3
+ def perform(run, step)
4
+ raise InnerFlowError, "Nothing was recorded at the Flow step #{step}, so its run cannot move on" unless run.recorded.key?(step.to_sym)
5
+
6
+ run.advance
7
+ end
8
+ end
9
+ end
@@ -1,9 +1,12 @@
1
1
  module EasyFlow
2
2
  class Canvas
3
- def initialize(document, registry: EasyFlow.registry, host: nil)
3
+ def initialize(document, registry: EasyFlow.registry, host: nil, flows: [], flow_path: nil, flow: nil)
4
4
  @document = document
5
+ @flow = flow
5
6
  @registry = registry
6
7
  @host = host
8
+ @flows = flows
9
+ @flow_path = flow_path
7
10
  end
8
11
 
9
12
  def to_h
@@ -19,7 +22,7 @@ module EasyFlow
19
22
  def drawn_step(node)
20
23
  { "id" => node.id, "type" => node.type, "label" => label_for(node),
21
24
  "config" => node.config.merge(kept_values(node)), "ports" => [], "ends_here" => ends_here?(node), "begins_here" => begins_here?(node),
22
- "loose" => loose?(node), "choices" => choices_for(node), "placeholder" => false, **placed[node.id] }
25
+ "starts_a_flow" => starts_a_flow?(node), "opens" => opens(node), "loose" => loose?(node), "choices" => choices_for(node), "placeholder" => false, **placed[node.id] }
23
26
  end
24
27
 
25
28
  def drawn_placeholder(gap)
@@ -100,7 +103,7 @@ module EasyFlow
100
103
  end
101
104
 
102
105
  def violations
103
- Validator.new(@document, registry: @registry).violations.map do |violation|
106
+ Validator.new(@document, registry: @registry, flow: @flow).violations.map do |violation|
104
107
  { "node" => violation.node, "problem" => violation.problem.to_s, "detail" => violation.detail }
105
108
  end
106
109
  end
@@ -114,7 +117,16 @@ module EasyFlow
114
117
  end
115
118
 
116
119
  def choices_for(node)
117
- naming_steps(node).index_with { earlier_than(node) }.merge(drawn_by(node)).merge(named_outputs_by(node))
120
+ naming_steps(node).index_with { earlier_than(node) }.merge(naming_flows(node).index_with { offered_flows })
121
+ .merge(drawn_by(node)).merge(named_outputs_by(node))
122
+ end
123
+
124
+ def naming_flows(node)
125
+ step_type_for(node)&.settings&.fields.to_h.select { |_name, type| type == :flow }.keys.map(&:to_s)
126
+ end
127
+
128
+ def offered_flows
129
+ @flows.map { |flow| { "value" => flow.id.to_s, "label" => flow.title } }
118
130
  end
119
131
 
120
132
  def named_outputs_by(node)
@@ -159,6 +171,15 @@ module EasyFlow
159
171
  step_type_for(node)&.begins_here? || false
160
172
  end
161
173
 
174
+ def starts_a_flow?(node)
175
+ step_type_for(node)&.starts_a_flow? || false
176
+ end
177
+
178
+ def opens(node)
179
+ named = ChosenFlow.of(node).flow_id
180
+ @flow_path.call(named) if @flow_path && starts_a_flow?(node) && named
181
+ end
182
+
162
183
  def ports_for(node)
163
184
  digest.routing_values(node.id)
164
185
  end
@@ -0,0 +1,15 @@
1
+ module EasyFlow
2
+ ChosenFlow = Data.define(:flow_id, :number) do
3
+ def self.of(node)
4
+ new(flow_id: node.config["flow"].presence, number: node.config["version"].presence)
5
+ end
6
+
7
+ def flow
8
+ Definition.find_by(id: flow_id) if flow_id
9
+ end
10
+
11
+ def version
12
+ flow&.definition_versions&.find_by(number: number)
13
+ end
14
+ end
15
+ end
@@ -11,6 +11,7 @@ module EasyFlow
11
11
  validates :host, :slug, presence: true
12
12
 
13
13
  after_initialize :begin_the_flow, if: :new_record?
14
+ before_destroy :kept_while_a_parent_waits, prepend: true
14
15
 
15
16
  scope :listable, -> { active }
16
17
 
@@ -107,6 +108,15 @@ module EasyFlow
107
108
 
108
109
  private
109
110
 
111
+ def kept_while_a_parent_waits
112
+ waiting = runs.where.not(parent_run_id: nil).select { |run| run.parent_run.waiting_on == run }
113
+ return if waiting.empty?
114
+
115
+ names = waiting.map { |run| run.parent_run.flow.title.presence || run.parent_run.flow.slug }.uniq.to_sentence
116
+ errors.add(:base, "This flow cannot be removed while #{names} waits on one of its runs")
117
+ throw :abort
118
+ end
119
+
110
120
  def begin_the_flow
111
121
  self.document ||= { "nodes" => [ { "id" => "start", "type" => "start" }, { "id" => "end", "type" => "terminal" } ],
112
122
  "edges" => [ { "from" => "start", "to" => "end" } ] }
@@ -67,6 +67,13 @@ module EasyFlow
67
67
  walk(state).last
68
68
  end
69
69
 
70
+ def output(state)
71
+ ended = nil
72
+ walk(state) { |cursor| ended = cursor }
73
+
74
+ ended.config["output"].presence if step_type(ended)&.ends_here?
75
+ end
76
+
70
77
  def state_on_path(state)
71
78
  state.slice(*walk(state).first)
72
79
  end
@@ -94,6 +101,7 @@ module EasyFlow
94
101
  while cursor
95
102
  return [ recorded, nil ] if visits[cursor.id].positive? && answers_at_visit[cursor.id] == answered
96
103
 
104
+ yield cursor if block_given?
97
105
  visits[cursor.id] += 1
98
106
  answers_at_visit[cursor.id] = answered
99
107
  key = visit_key(cursor.id, visits[cursor.id])
@@ -0,0 +1,30 @@
1
+ module EasyFlow
2
+ class FlowStep
3
+ include Step
4
+
5
+ step_name "Flow"
6
+
7
+ starts_a_flow
8
+
9
+ setting :flow, type: :flow
10
+ setting :version, type: :integer
11
+
12
+ output :result, values: ->(node) { FlowStep.endings_of(node) }
13
+
14
+ waits_until { |_node, _state| false }
15
+
16
+ names_by do |node|
17
+ chosen = ChosenFlow.of(node)
18
+ [ chosen.flow&.title, ("version #{chosen.number}" if chosen.number) ].compact.join(", ").presence
19
+ end
20
+
21
+ def self.endings_of(node)
22
+ version = ChosenFlow.of(node).version
23
+ Array(version&.definition.to_h["nodes"]).select { |step| step["type"] == "terminal" }.filter_map { |step| step["output"].presence }.uniq
24
+ end
25
+
26
+ def route(node, state)
27
+ state[node.id]
28
+ end
29
+ end
30
+ end
@@ -17,6 +17,23 @@ module EasyFlow
17
17
  @run.record(id.to_sym, value)
18
18
  end
19
19
 
20
+ def start_inner(node)
21
+ return if @run.inner_runs.exists?(parent_step: node.id)
22
+
23
+ chosen = ChosenFlow.of(node)
24
+ raise InnerFlowError, "The Flow step #{node.id} names a version that cannot run" if chosen.version&.out_of_service?
25
+ Run.start(chosen.flow || raise(ActiveRecord::RecordNotFound), version: chosen.version)
26
+ .tap { |inner| inner.update!(parent_run: @run, parent_step: node.id, owner: @run.owner) }.advance
27
+ end
28
+
29
+ def ended
30
+ parent = @run.parent_run or return
31
+ return if parent.recorded.key?(@run.parent_step.to_sym)
32
+
33
+ parent.record(@run.parent_step.to_sym, @run.output) if @run.output
34
+ AdvanceParentJob.perform_later(parent, @run.parent_step)
35
+ end
36
+
20
37
  def finish(_state)
21
38
  @run
22
39
  end
@@ -19,6 +19,10 @@ module EasyFlow
19
19
  @answers = @answers.merge(id.to_sym => value)
20
20
  end
21
21
 
22
+ def start_inner(_node); end
23
+
24
+ def ended; end
25
+
22
26
  def finish(state)
23
27
  return unless @flow.on_finish?
24
28
 
@@ -5,9 +5,13 @@ module EasyFlow
5
5
  belongs_to :flow, class_name: "EasyFlow::Definition"
6
6
  belongs_to :definition_version, class_name: "EasyFlow::Version"
7
7
  belongs_to :owner, polymorphic: true, optional: true
8
+ belongs_to :parent_run, class_name: "EasyFlow::Run", optional: true
9
+ has_many :inner_runs, class_name: "EasyFlow::Run", foreign_key: :parent_run_id, inverse_of: :parent_run, dependent: :nullify
8
10
 
9
- def self.start(flow)
10
- create!(flow: flow, definition_version: flow.live_version)
11
+ validate :started_on_a_published_version, on: :create
12
+
13
+ def self.start(flow, version: flow.live_version)
14
+ create!(flow: flow, definition_version: version)
11
15
  end
12
16
 
13
17
  def record(step_id, value)
@@ -30,6 +34,15 @@ module EasyFlow
30
34
  digest.next_step(state.transform_keys(&:to_s))
31
35
  end
32
36
 
37
+ def output
38
+ digest.output(recorded.transform_keys(&:to_s))
39
+ end
40
+
41
+ def waiting_on
42
+ stopped_at = next_step(recorded)
43
+ inner_runs.find_by(parent_step: stopped_at.id) if stopped_at
44
+ end
45
+
33
46
  def walked(state)
34
47
  digest.state_on_path(state.transform_keys(&:to_s))
35
48
  end
@@ -46,5 +59,11 @@ module EasyFlow
46
59
  def pinned_steps
47
60
  Array(definition_version.definition.to_h["nodes"]).index_by { |node| node["id"] }
48
61
  end
62
+
63
+ private
64
+
65
+ def started_on_a_published_version
66
+ errors.add(:definition_version, "must be a published version of the flow") unless definition_version&.flow_id == flow_id && (definition_version.live? || definition_version.superseded?)
67
+ end
49
68
  end
50
69
  end
@@ -30,6 +30,8 @@ module EasyFlow
30
30
  while (node = @digest.next_step(named(progress.recorded))) && goes_on?(node, named(progress.recorded))
31
31
  progress.record(node.id, result_of(@digest.step(node.id), named(progress.recorded)))
32
32
  end
33
+ progress.start_inner(node) if node && starts_a_flow?(node)
34
+ progress.ended unless node
33
35
  end
34
36
 
35
37
  def drawing_at(state)
@@ -50,6 +52,10 @@ module EasyFlow
50
52
 
51
53
  private
52
54
 
55
+ def starts_a_flow?(node)
56
+ @registry.registered?(node.type) && @registry.fetch(node.type).starts_a_flow?
57
+ end
58
+
53
59
  def acts?(node)
54
60
  @registry.registered?(node.type) && @registry.fetch(node.type).acts?
55
61
  end
@@ -19,6 +19,7 @@ module EasyFlow
19
19
  @awaits_input = false
20
20
  @ends_here = false
21
21
  @begins_here = false
22
+ @starts_a_flow = false
22
23
  end
23
24
 
24
25
  def output(name, type: :string, label: nil, values: nil, from: nil)
@@ -65,6 +66,10 @@ module EasyFlow
65
66
  @begins_here = true
66
67
  end
67
68
 
69
+ def starts_a_flow
70
+ @starts_a_flow = true
71
+ end
72
+
68
73
  def names_by(field = nil, &naming)
69
74
  @naming_field = field
70
75
  @naming = naming
@@ -111,7 +116,7 @@ module EasyFlow
111
116
 
112
117
  def to_step_type
113
118
  StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
114
- ends_here: @ends_here, begins_here: @begins_here, behaviour: @behaviour, routing: @routing,
119
+ ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, behaviour: @behaviour, routing: @routing,
115
120
  display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
116
121
  outputs: @declared_outputs, answer_check: @answer_check, readiness: @readiness)
117
122
  end
@@ -7,7 +7,7 @@ module EasyFlow
7
7
  attr_reader :id, :step_name, :settings, :naming_field, :naming, :outputs, :drawn_by
8
8
 
9
9
  def initialize(id:, step_name:, settings:, awaits_input:, behaviour:, routing:,
10
- ends_here: false, begins_here: false, display: nil, drawn_by: nil,
10
+ ends_here: false, begins_here: false, starts_a_flow: false, display: nil, drawn_by: nil,
11
11
  naming_field: nil, naming: nil, outputs: [], answer_check: nil, readiness: nil)
12
12
  @id = id
13
13
  @step_name = step_name
@@ -15,6 +15,7 @@ module EasyFlow
15
15
  @awaits_input = awaits_input
16
16
  @ends_here = ends_here
17
17
  @begins_here = begins_here
18
+ @starts_a_flow = starts_a_flow
18
19
  @behaviour = behaviour
19
20
  @routing = routing
20
21
  @display = display
@@ -79,5 +80,9 @@ module EasyFlow
79
80
  def begins_here?
80
81
  @begins_here
81
82
  end
83
+
84
+ def starts_a_flow?
85
+ @starts_a_flow
86
+ end
82
87
  end
83
88
  end
@@ -6,6 +6,8 @@ module EasyFlow
6
6
 
7
7
  ends_here
8
8
 
9
+ setting :output, type: :string
10
+
9
11
  names_by { |_node| "End" }
10
12
  end
11
13
  end
@@ -2,14 +2,15 @@ module EasyFlow
2
2
  class Validator
3
3
  OPTIONAL = { unrouted_value: :unrouted_values, unfollowed_path: :unfollowed_paths, dead_end: :dead_ends }.freeze
4
4
 
5
- def initialize(document, registry: EasyFlow.registry, checks: EasyFlow.checks)
5
+ def initialize(document, registry: EasyFlow.registry, checks: EasyFlow.checks, flow: nil)
6
6
  @document = document
7
7
  @registry = registry
8
8
  @checks = checks
9
+ @flow = flow
9
10
  end
10
11
 
11
12
  def violations
12
- structural_violations + unmet_requirements + missing_settings + missing_values + beginnings + asked_for
13
+ structural_violations + unmet_requirements + missing_settings + missing_values + missing_flows + unrunnable_versions + circular_flows + beginnings + asked_for
13
14
  end
14
15
 
15
16
  def structural_violations
@@ -138,6 +139,40 @@ module EasyFlow
138
139
  @registry.fetch(node.type).settings.drawn_from
139
140
  end
140
141
 
142
+ def missing_flows
143
+ starting_a_flow.reject { |node| ChosenFlow.of(node).flow }
144
+ .map { |node| Violation.new(node: node.id, problem: :missing_flow) }
145
+ end
146
+
147
+ def unrunnable_versions
148
+ starting_a_flow.select { |node| ChosenFlow.of(node).flow }.reject { |node| runnable?(ChosenFlow.of(node).version) }
149
+ .map { |node| Violation.new(node: node.id, problem: :unrunnable_version) }
150
+ end
151
+
152
+ def runnable?(version)
153
+ version&.live? || version&.superseded? || false
154
+ end
155
+
156
+ def circular_flows
157
+ return [] unless @flow
158
+
159
+ starting_a_flow.select { |node| leads_back?(node, []) }
160
+ .map { |node| Violation.new(node: node.id, problem: :circular) }
161
+ end
162
+
163
+ def leads_back?(node, seen)
164
+ chosen = ChosenFlow.of(node)
165
+ return true if chosen.flow_id.to_s == @flow.id.to_s
166
+ return false if chosen.flow_id.nil? || seen.include?(chosen.flow_id.to_s)
167
+
168
+ starting_a_flow(Document.new(chosen.version&.definition.to_h, registry: @registry))
169
+ .any? { |inner| leads_back?(inner, seen + [ chosen.flow_id.to_s ]) }
170
+ end
171
+
172
+ def starting_a_flow(document = @document)
173
+ document.nodes.select { |node| @registry.registered?(node.type) && @registry.fetch(node.type).starts_a_flow? }
174
+ end
175
+
141
176
  def missing_settings
142
177
  @document.nodes.flat_map do |node|
143
178
  required_of(node).reject { |name| node.config[name.to_s].present? }