easy_flow 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/app/assets/builds/easy_flow/canvas.js +1 -1
- data/app/controllers/concerns/easy_flow/manage/draws_canvas.rb +1 -1
- data/app/controllers/easy_flow/manage/canvas_controller.rb +2 -2
- data/app/controllers/easy_flow/manage/flows_controller.rb +4 -1
- data/app/javascript/easy_flow/canvas/Canvas.jsx +2 -0
- data/app/javascript/easy_flow/canvas/Control.jsx +1 -1
- data/app/javascript/easy_flow/canvas/StepCard.jsx +5 -2
- data/app/jobs/easy_flow/advance_parent_job.rb +9 -0
- data/app/models/easy_flow/canvas.rb +25 -4
- data/app/models/easy_flow/chosen_flow.rb +26 -0
- data/app/models/easy_flow/definition.rb +10 -0
- data/app/models/easy_flow/flow_step.rb +27 -0
- data/app/models/easy_flow/progress/kept.rb +17 -0
- data/app/models/easy_flow/progress/loose.rb +4 -0
- data/app/models/easy_flow/run.rb +17 -2
- data/app/models/easy_flow/runner.rb +6 -0
- data/app/models/easy_flow/step_type/declaration.rb +10 -1
- data/app/models/easy_flow/step_type.rb +17 -2
- data/app/models/easy_flow/validator.rb +37 -2
- data/app/views/easy_flow/manage/flows/show.html.erb +4 -0
- data/db/migrate/20261007120000_add_parent_run_to_easy_flow_runs.rb +6 -0
- data/lib/easy_flow/engine.rb +1 -0
- data/lib/easy_flow/version.rb +1 -1
- data/lib/easy_flow.rb +2 -1
- data/the_local/agents/easy_flow-develop.md +146 -10
- data/the_local/agents/easy_flow-info.md +16 -7
- data/the_local/agents/easy_flow-install.md +20 -6
- data/the_local/interface.yml +2 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd76ff73b7535488466a0d9b117baa070987f3cefae1947d0b6b038a794408d0
|
|
4
|
+
data.tar.gz: 8193d30d3f11a20720c01d25d23e1666406d674f57da2e2dbfc0387a4c0c94be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae11c115249f725fd651f83341861470d0885cc2171da51742c9eb5e79215e23bdc86a4fb3ac02bfea83f0c942182050c3f4d6cb66e9711a674f2a4c47249602
|
|
7
|
+
data.tar.gz: 619f909b5d2054f382145510315e0f55a5843b220c92a13f51bf9a19b7d39766a8aaa25985b15c17f19577b8cf252a9cf2f20eafefa02ffbefc15df9756984ec
|
|
@@ -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])
|
|
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}` : ""}
|
|
@@ -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(
|
|
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,26 @@
|
|
|
1
|
+
module EasyFlow
|
|
2
|
+
ChosenFlow = Data.define(:flow_id, :number) do
|
|
3
|
+
def self.of(node, registry: EasyFlow.registry)
|
|
4
|
+
chosen = (registry.fetch(node.type).flow_chosen_by(node) if registry.registered?(node.type))
|
|
5
|
+
return worked_out(chosen) if chosen
|
|
6
|
+
|
|
7
|
+
new(flow_id: node.config["flow"].presence, number: node.config["version"].presence)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.worked_out(chosen)
|
|
11
|
+
new(flow_id: chosen[:flow].presence, number: chosen[:version].presence)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
[ flow&.title, ("version #{number}" if number) ].compact.join(", ").presence
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def flow
|
|
19
|
+
Definition.find_by(id: flow_id) if flow_id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def version
|
|
23
|
+
flow&.definition_versions&.find_by(number: number)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
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" } ] }
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { |node| ChosenFlow.of(node).name }
|
|
17
|
+
|
|
18
|
+
def self.endings_of(node)
|
|
19
|
+
version = ChosenFlow.of(node).version
|
|
20
|
+
Array(version&.definition.to_h["nodes"]).select { |step| step["type"] == "terminal" }.filter_map { |step| step["output"].presence }.uniq
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def route(node, state)
|
|
24
|
+
state[node.id]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
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
|
data/app/models/easy_flow/run.rb
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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)
|
|
@@ -34,6 +38,11 @@ module EasyFlow
|
|
|
34
38
|
digest.output(recorded.transform_keys(&:to_s))
|
|
35
39
|
end
|
|
36
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
|
+
|
|
37
46
|
def walked(state)
|
|
38
47
|
digest.state_on_path(state.transform_keys(&:to_s))
|
|
39
48
|
end
|
|
@@ -50,5 +59,11 @@ module EasyFlow
|
|
|
50
59
|
def pinned_steps
|
|
51
60
|
Array(definition_version.definition.to_h["nodes"]).index_by { |node| node["id"] }
|
|
52
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
|
|
53
68
|
end
|
|
54
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,14 @@ 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
|
+
|
|
73
|
+
def chooses_flow(&chooser)
|
|
74
|
+
@flow_chooser = chooser
|
|
75
|
+
end
|
|
76
|
+
|
|
68
77
|
def names_by(field = nil, &naming)
|
|
69
78
|
@naming_field = field
|
|
70
79
|
@naming = naming
|
|
@@ -111,7 +120,7 @@ module EasyFlow
|
|
|
111
120
|
|
|
112
121
|
def to_step_type
|
|
113
122
|
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,
|
|
123
|
+
ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, flow_chooser: @flow_chooser, behaviour: @behaviour, routing: @routing,
|
|
115
124
|
display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
|
|
116
125
|
outputs: @declared_outputs, answer_check: @answer_check, readiness: @readiness)
|
|
117
126
|
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, flow_chooser: nil, 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,8 @@ 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
|
|
19
|
+
@flow_chooser = flow_chooser
|
|
18
20
|
@behaviour = behaviour
|
|
19
21
|
@routing = routing
|
|
20
22
|
@display = display
|
|
@@ -53,7 +55,7 @@ module EasyFlow
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def name_of(node)
|
|
56
|
-
@naming&.call(node).presence || node.config[naming_field.to_s].presence
|
|
58
|
+
@naming&.call(node).presence || node.config[naming_field.to_s].presence || worked_out_flow_name(node)
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def acts?
|
|
@@ -79,5 +81,18 @@ module EasyFlow
|
|
|
79
81
|
def begins_here?
|
|
80
82
|
@begins_here
|
|
81
83
|
end
|
|
84
|
+
|
|
85
|
+
def starts_a_flow?
|
|
86
|
+
@starts_a_flow
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def flow_chosen_by(node)
|
|
90
|
+
@flow_chooser&.call(node)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def worked_out_flow_name(node)
|
|
94
|
+
chosen = flow_chosen_by(node)
|
|
95
|
+
ChosenFlow.worked_out(chosen).name if chosen
|
|
96
|
+
end
|
|
82
97
|
end
|
|
83
98
|
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? }
|