easy_flow 0.27.0 → 0.28.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/easy_flow/manage/canvas_controller.rb +9 -0
- data/app/javascript/easy_flow/canvas/Control.jsx +12 -0
- data/app/models/easy_flow/canvas.rb +1 -1
- data/app/models/easy_flow/definition.rb +4 -0
- data/app/models/easy_flow/digest.rb +5 -1
- data/app/models/easy_flow/flow_step.rb +1 -1
- data/app/models/easy_flow/play_flows.rb +39 -0
- data/app/models/easy_flow/progress/kept.rb +23 -7
- data/app/models/easy_flow/step_type/declaration.rb +11 -2
- data/app/models/easy_flow/step_type.rb +12 -2
- data/app/models/easy_flow/validator.rb +28 -2
- data/lib/easy_flow/engine.rb +1 -0
- data/lib/easy_flow/version.rb +1 -1
- data/lib/easy_flow.rb +1 -1
- data/the_local/agents/easy_flow-develop.md +2 -0
- data/the_local/agents/easy_flow-info.md +2 -1
- data/the_local/agents/easy_flow-install.md +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c3f5410fc5f056129c38c8538226461dd426e932bf267d6116bc6c07ad872d1
|
|
4
|
+
data.tar.gz: 19221f96eb57398266925771014571f69aeb155bbfab9a3bb55b45dc2401175c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9786ee8c6b473f189f062870851305bc16ec2031586ea6eb4833cc792558a0efd3b5aa88e4da23ba913fba30e73dfa7c84e3d25ce6132ead745712e9c1cc1c46
|
|
7
|
+
data.tar.gz: 89d01cd446c018ae52a86932c8235c77f49ca92de1e65aef00570b9b81511e604a7bcb569df50ce618976a66fcb2a624e3209ac397e17e8920980de00be118f2
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{var Dt=globalThis.ReactDOM;var{createPortal:Ne,createRoot:q,flushSync:ze,hydrateRoot:Be,preconnect:Ee,prefetchDNS:Te,preinit:$e,preinitModule:De,preload:Ae,preloadModule:Pe,requestFormReset:_e,unstable_batchedUpdates:Le,useFormState:Ie,useFormStatus:Me,version:Oe}=Dt;var J=globalThis.React,e=J,{Children:Fe,Component:je,Fragment:Ve,PureComponent:We,StrictMode:Ye,Suspense:Ue,cloneElement:Xe,createContext:Ge,createElement:qe,createRef:Je,forwardRef:Ke,isValidElement:Ze,lazy:Qe,memo:Re,startTransition:to,useCallback:eo,useContext:oo,useDebugValue:ro,useDeferredValue:no,useEffect:T,useId:so,useImperativeHandle:ao,useLayoutEffect:K,useMemo:A,useReducer:lo,useRef:F,useState:b,useSyncExternalStore:io,useTransition:co}=J;var Z=new Map,_=new Map,Q=(t,o)=>Z.set(t,o),R=()=>{_.forEach(t=>t.unmount()),_.clear()},tt=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(n=>{if(_.has(n))return;let r=Z.get(n.dataset.reactUi),a=o(n);_.set(n,a),a.render(e.createElement(r,JSON.parse(n.dataset.props)))});var et=(t,o)=>{let n=()=>tt(t,o);t.addEventListener("turbo:load",n),t.addEventListener("turbo:frame-load",n),t.addEventListener("turbo:before-cache",R),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",n):n()};var C=(...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"},_t=({variant:t="primary",size:o="md",className:n,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:C("ks-button",At[t],Pt[o],n)},r)},x=_t;var Lt="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:n,onArm:r,connecting:a})=>e.createElement("button",{onClick:l=>{a||(l.stopPropagation(),r(l))},title:n?"Now choose a step to connect to":"Connect this branch",className:n?Mt:o?It:Lt,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},Ft={padding:"8px 14px",textAlign:"center",fontWeight:600},jt={fontWeight:600,lineHeight:1.3},Vt=(t,o,n)=>{let r=n?"ring-4 ring-accent-600/15":"shadow-sm";return t||n&&!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:n,connecting:r,onSelect:a,onOpen:l,onArm:i,onDragEnd:y,onDragStart:p})=>{let f=t.begins_here||t.ends_here,u=t.begins_here,d=t.ends_here?[]:t.ports,h=r&&!u;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!u,onDragStart:c=>{c.dataTransfer.effectAllowed="move",c.dataTransfer.setData("text/plain",t.id),p()},onDragEnd:y,onClick:t.opens&&!r?()=>l(t.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Vt(h,t.violations.length>0,o)}`,style:{...Ht,padding:f?0:"12px 14px",cursor:u?"default":r?"crosshair":"grab"}},f&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:Ft},t.label),!f&&e.createElement("div",{style:jt},t.label),!f&&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(x,{variant:"secondary",size:"sm",className:"mt-2",onClick:c=>{c.stopPropagation(),a()}},"Settings"),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}`:"")),d.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},d.map(c=>e.createElement(ot,{key:c,name:c,connected:t.connected.includes(c),connecting:r,armed:n===c,onArm:g=>i(c,g)}))))},j=Wt;var rt=(t,o)=>t.includes(o)?t.filter(n=>n!==o):[...t,o],nt=(t,o)=>({...o?.choices||{},...t?.choices||{}});var Yt=({as:t="input",disabled:o,className:n,...r})=>e.createElement(t,{...r,disabled:o,className:C("ks-input",o&&"ks-input-disabled",n)}),S=Yt;var Ut=t=>e.createElement(S,{...t,as:"select"}),st=Ut;var Xt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:C("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:n,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(n).map(l=>e.createElement("option",{key:l.value,value:l.value},l.label)));if(t==="multi_select"){let l=Array.isArray(o)?o:[],i=y=>a(rt(l,y));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(n||[]).map(y=>e.createElement("label",{key:y,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(V,{checked:l.includes(y),onChange:()=>i(y)}),e.createElement("span",null,y))))}return t==="integer"||t==="float"?e.createElement(S,{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(S,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},L=qt;var at=(t,o,n,r)=>t.map((a,l)=>l===o?{...a,[n]:r}:a);var Jt=(t,o,n,r)=>{let a=(t||{})[n],l=(o||{})[n];return l?(a||{})[r[l]]:a},Kt=({holds:t,labels:o,rows:n,choices:r,outputsOf:a,onChange:l,onSettle:i})=>{let y=Array.isArray(n)?n:[],p=(f,u,d,h)=>{let c=at(y,f,u,d);h?i(c):l(c)};return e.createElement("div",{style:{marginBottom:12}},y.map((f,u)=>e.createElement("div",{key:u,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([d,h])=>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(L,{type:h,value:f[d],choices:Jt(r,a,d,f),onChange:c=>p(u,d,c,!1),onSettle:c=>p(u,d,c,!0)}))),e.createElement(x,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>i(y.filter((d,h)=>h!==u))},"Remove"))),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>i([...y,{}])},"Add"))},lt=Kt;var Zt=({as:t="div",className:o,children:n,...r})=>e.createElement(t,{...r,className:C("ks-panel",o)},n),D=Zt;var I=(t,o)=>({className:n,children:r,...a})=>e.createElement(t,{...a,className:C(o,n)},r),P=I("label","ks-label"),er=I("p","ks-hint"),or=I("p","ks-error"),rr=I("span","ks-required");var Qt={width:280,padding:20,overflowY:"auto"},Rt=({node:t,fields:o,holds:n,labels:r,recordLabels:a,recordChoices:l,recordOutputsOf:i,choices:y,onSave:p,onDelete:f,onClose:u})=>{let[d,h]=b(t.config);T(()=>h(t.config),[t.id,t.config]);let c=g=>{h(g),JSON.stringify(g)!==JSON.stringify(t.config)&&p(g)};return e.createElement(D,{as:"aside",style:Qt,"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:u,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,w])=>e.createElement(P,{key:g},e.createElement("span",{style:{display:"block",marginBottom:3}},r[g]||g),w==="list"?e.createElement(lt,{holds:n[g]||{},labels:a[g]||{},rows:d[g],choices:{...(l||{})[g],...(t.record_choices||{})[g]},outputsOf:(i||{})[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}):e.createElement(L,{type:w,value:d[g],choices:y[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}))),e.createElement(x,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},it=Rt;var te={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},ee="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",oe="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",re=({node:t,dragging:o,onFill:n,onDrop:r})=>e.createElement("div",{ref:t.ref,"data-placeholder":t.id,title:`Choose the step \u201C${t.label}\u201D should lead to`,onClick:n,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?oe:ee,style:te},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=re;var ne=({entries:t,at:o,onPick:n,onConnect:r,onDismiss:a})=>e.createElement(D,{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(x,{key:l.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>n(l)},l.label,e.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},l.type))),r&&e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),ct=ne;var se={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},pt=({label:t,title:o,idle:n,enabled:r,onUse:a})=>e.createElement(x,{variant:"secondary",size:"sm",title:r?o:n,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},t),ae=({undoable:t,redoable:o,onAdd:n,onUndo:r,onRedo:a})=>e.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},e.createElement(x,{variant:"secondary",className:"rounded-full",style:se,title:"Add a step",onClick:n},"+"),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})),ut=ae;var mt=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 le={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,n)=>r=>{r.target.value!==(t[o]??"")&&n({[o]:r.target.value})},ie=({flow:t,changes:o,problems:n,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:y,onClose:p})=>e.createElement(D,{as:"aside",style:le,"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),n.length===0&&!r?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):n.map(f=>e.createElement("p",{key:`${f.node}-${f.problem}`,className:"text-amber-700 dark:text-amber-400",style:M,"data-problem":!0},"\u26A0 ",mt(f))),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((f,u)=>e.createElement("p",{key:u,style:M,"data-change":!0},f)),e.createElement("h2",{style:W},"Details"),e.createElement(P,null,e.createElement("span",{style:gt},"Title"),e.createElement(S,{className:"mb-3",defaultValue:t.title??"",onBlur:yt(t,"title",y),"data-flow-title":!0})),e.createElement(P,null,e.createElement("span",{style:gt},"Start label"),e.createElement(S,{className:"mb-3",defaultValue:t.start_label??"",onBlur:yt(t,"start_label",y),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(x,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),ht=ie;var xt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},de=({link:t,onInsert:o,onRemove:n,onDrop:r,dragging:a})=>{let[l,i]=b(!1),y=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:y?1:0,transition:"opacity .12s"}},e.createElement(x,{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(x,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:xt},"\xD7")))},bt=de;var ce=({links:t,extent:o,dragging:n,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:!!n,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),vt=ce;var pe="Your change was not saved because the server could not be reached.",kt=({base:t,token:o,fetch:n,onFlow:r,onError:a,onNotice:l})=>async(i,y,p)=>{let f=await n(t+i,{method:y,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:p&&JSON.stringify(p)}).catch(()=>null);if(!f)return l(null),a(pe);let u=await f.json().catch(()=>({}));if(!f.ok)return l(null),a(u.error||"That change was refused");a(null),l(u.notice||null),r(await(await n(t+".json",{headers:{Accept:"application/json"}})).json())};var ue=(t,o,n)=>{let[r,a]=b(n),[l,i]=b(null),[y,p]=b(null),f=A(()=>kt({base:t,token:o,fetch:(...u)=>window.fetch(...u),onFlow:a,onError:i,onNotice:p}),[t,o]);return{flow:r,error:l,notice:y,send:f}},wt=ue;var O=(t,o,n,r)=>{let a=t.left-n.left+r.scrollLeft,l=t.top-n.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},fe=(t,o,n,r,a)=>{let l={straight:`M ${o.x} ${o.y} L ${n.x} ${n.y}`,turn:`M ${o.x} ${o.y} H ${n.x} V ${n.y}`,lane:`M ${o.x} ${o.y} V ${r} H ${n.x} V ${n.y}`,detour:`M ${o.x} ${o.y} H ${a} V ${n.y} H ${n.x}`}[t.route];return{...t,path:l,midX:t.route==="detour"?a:(o.x+n.x)/2,midY:t.route==="lane"?r:(o.y+n.y)/2}},ge=(t,o,n,r)=>{let[a,l]=b([]),[i,y]=b({width:"100%",height:"100%"});return K(()=>{let p=()=>{let u=o.current?.getBoundingClientRect();u&&(y({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(d=>{let h=n.current[d.source]?.getBoundingClientRect(),c=n.current[d.target]?.getBoundingClientRect();if(!h||!c)return[];let g=O(h,d.leaves,u,o.current),w=me(O(c,d.enters,u,o.current),d.enters),v=(O(h,"bottom",u,o.current).y+O(c,"top",u,o.current).y)/2,B=d.leaves==="left"?Math.min(g.x,w.x)-28:Math.max(g.x,w.x)+28;return[fe(d,g,w,v,B)]})))};p(),window.addEventListener("resize",p);let f=new ResizeObserver(p);return o.current&&f.observe(o.current),()=>{window.removeEventListener("resize",p),f.disconnect()}},r),{links:a,extent:i}},Ct=ge;var St=(t,o)=>{let n=new Set(o),r=t,a=2;for(;n.has(r);)r=`${t}_${a++}`;return r};var xe={display:"flex",height:"100%",minHeight:0,fontSize:13},be={flex:1,overflow:"auto",position:"relative"},ve={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},ke={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},Ce=({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")),Se=({base:t,token:o,initial:n})=>{let{flow:r,error:a,notice:l,send:i}=wt(t,o,n),[y,p]=b(null),[f,u]=b(null),[d,h]=b(null),[c,g]=b(null),[w,v]=b(!1),B=F({}),E=F(null);T(()=>{let s=()=>{p(null),v(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),T(()=>{if(!w)return;let s=m=>{m.target.closest("[data-builder-panel], [data-open-panel]")||v(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[w]),T(()=>{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(s=>[s.id,s])),[r.nodes]),{links:Bt,extent:Et}=Ct(r,E,B,[r,y,zt]),Y=A(()=>{let s={};return r.violations.forEach(m=>{var k;return(s[k=m.node]||(s[k]=[])).push(m)}),s},[r.violations]),H=r.nodes.filter(s=>!s.loose),U=r.nodes.filter(s=>s.loose),Tt=Math.max(0,...H.map(s=>s.row))+1,$t=Math.max(0,...H.map(s=>s.column))+1,X=s=>{if(!d)return;let[m,k]=d;h(null),m!==s&&i("/edges","POST",{from:m,to:s,on:k||null})},N=r.nodes.find(s=>s.id===y),z=r.palette.find(s=>s.type===N?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:xe,onMouseUp:()=>g(null),onKeyDown:s=>{s.key==="Escape"&&(p(null),h(null),u(null))},tabIndex:-1},e.createElement("div",{ref:E,className:"bg-surface-50 dark:bg-surface-950",style:be,onClick:s=>{s.target===E.current&&(p(null),h(null),v(!1))}},d&&e.createElement(Ce,{port:d[1],onCancel:()=>h(null)}),e.createElement(ut,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>u({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:c,onInsert:s=>u({from:s.source,to:s.target,at:{x:s.midX+30,y:s.midY}}),onRemove:s=>{p(null),i("/edges","DELETE",{from:s.source,to:s.target})},onDrop:s=>{let m=c;g(null),i("/steps/"+m+"/move","PATCH",{from:s.source,to:s.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:ve},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(s=>e.createElement("div",{key:s.id,style:{marginBottom:8}},e.createElement(j,{node:{...s,ref:m=>{B.current[s.id]=m},violations:Y[s.id]||[],connected:[]},selected:y===s.id,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d,onSelect:()=>d?X(s.id):p(s.id),onOpen:m=>window.location.assign(m),onArm:(m,k)=>{k.stopPropagation(),h([s.id,m])},onDragStart:()=>g(s.id),onDragEnd:()=>g(null)})))),e.createElement("div",{style:{...ke,gridTemplateColumns:`repeat(${$t+1}, ${280/2}px)`,gridTemplateRows:`repeat(${Tt}, auto)`}},H.map(s=>e.createElement("div",{key:s.id,style:{gridRow:s.row+1,gridColumn:`${s.column+1} / span 2`,zIndex:2}},s.placeholder&&e.createElement(dt,{node:{...s,ref:m=>{B.current[s.id]=m}},dragging:!!c,onFill:()=>u({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let m=c;g(null),m&&i("/edges","POST",{from:s.from,to:m,on:s.on})}}),!s.placeholder&&e.createElement(j,{node:{...s,ref:m=>{B.current[s.id]=m},violations:Y[s.id]||[],connected:r.edges.filter(m=>m.source===s.id).map(m=>m.label||null)},selected:s.id===y,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d&&d[0]!==s.id,onSelect:()=>d?X(s.id):p(s.id),onOpen:m=>window.location.assign(m),onArm:(m,k)=>{let G=E.current.getBoundingClientRect();u({from:s.id,on:m,at:{x:k.clientX-G.left+E.current.scrollLeft+8,y:k.clientY-G.top+E.current.scrollTop+8}})},onDragEnd:()=>g(null),onDragStart:()=>g(s.id)})))),f&&e.createElement(ct,{entries:r.palette,at:f.at,onDismiss:()=>u(null),onConnect:f.on!==void 0?()=>{h([f.from,f.on]),u(null)}:null,onPick:s=>{let m=f;u(null),i("/steps","POST",{id:St(s.type,r.nodes.map(k=>k.id)),type:s.type,from:m.from,to:m.to,on:m.on})}})),w&&!N&&e.createElement(ht,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>v(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:s=>i("/details","PATCH",{flow:s})}),N&&e.createElement(it,{node:N,fields:z?.fields||{},holds:z?.records||{},labels:z?.labels||{},recordLabels:z?.record_labels||{},recordChoices:z?.record_choices||{},recordOutputsOf:z?.record_outputs_of||{},choices:nt(N,z),onClose:()=>p(null),onSave:s=>i(`/steps/${N.id}`,"PATCH",{config:s}),onDelete:()=>{p(null),i(`/steps/${N.id}`,"DELETE")}}))},Nt=Se;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:Ne,createRoot:J,flushSync:ze,hydrateRoot:Be,preconnect:Ee,prefetchDNS:Te,preinit:$e,preinitModule:Ae,preload:De,preloadModule:Pe,requestFormReset:_e,unstable_batchedUpdates:Le,useFormState:Ie,useFormStatus:Me,version:Oe}=Dt;var K=globalThis.React,e=K,{Children:Fe,Component:je,Fragment:Ve,PureComponent:We,StrictMode:Ye,Suspense:Ue,cloneElement:Xe,createContext:Ge,createElement:qe,createRef:Je,forwardRef:Ke,isValidElement:Ze,lazy:Qe,memo:Re,startTransition:to,useCallback:eo,useContext:oo,useDebugValue:ro,useDeferredValue:no,useEffect:T,useId:so,useImperativeHandle:ao,useLayoutEffect:Z,useMemo:D,useReducer:lo,useRef:j,useState:b,useSyncExternalStore:io,useTransition:co}=K;var Q=new Map,_=new Map,R=(t,o)=>Q.set(t,o),tt=()=>{_.forEach(t=>t.unmount()),_.clear()},et=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(n=>{if(_.has(n))return;let r=Q.get(n.dataset.reactUi),a=o(n);_.set(n,a),a.render(e.createElement(r,JSON.parse(n.dataset.props)))});var ot=(t,o)=>{let n=()=>et(t,o);t.addEventListener("turbo:load",n),t.addEventListener("turbo:frame-load",n),t.addEventListener("turbo:before-cache",tt),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",n):n()};var C=(...t)=>t.filter(Boolean).join(" ");var Pt={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},_t={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},Lt=({variant:t="primary",size:o="md",className:n,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:C("ks-button",Pt[t],_t[o],n)},r)},x=Lt;var It="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",Mt="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",Ot="border border-accent-600 bg-accent-600 text-white",Ht=({name:t,connected:o,armed:n,onArm:r,connecting:a})=>e.createElement("button",{onClick:l=>{a||(l.stopPropagation(),r(l))},title:n?"Now choose a step to connect to":"Connect this branch",className:n?Ot:o?Mt:It,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},t||"next"),rt=Ht;var Ft={width:240,boxSizing:"border-box",borderRadius:8},jt={padding:"8px 14px",textAlign:"center",fontWeight:600},Vt={fontWeight:600,lineHeight:1.3},Wt=(t,o,n)=>{let r=n?"ring-4 ring-accent-600/15":"shadow-sm";return t||n&&!o?`border-accent-600 ${r}`:o?`border-red-600 ${r}`:"border-gray-300 shadow-sm dark:border-zinc-700"},Yt=({node:t,selected:o,armed:n,connecting:r,onSelect:a,onOpen:l,onArm:i,onDragEnd:y,onDragStart:p})=>{let f=t.begins_here||t.ends_here,m=t.begins_here,d=t.ends_here?[]:t.ports,h=r&&!m;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!m,onDragStart:c=>{c.dataTransfer.effectAllowed="move",c.dataTransfer.setData("text/plain",t.id),p()},onDragEnd:y,onClick:t.opens&&!r?()=>l(t.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Wt(h,t.violations.length>0,o)}`,style:{...Ft,padding:f?0:"12px 14px",cursor:m?"default":r?"crosshair":"grab"}},f&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:jt},t.label),!f&&e.createElement("div",{style:Vt},t.label),!f&&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(x,{variant:"secondary",size:"sm",className:"mt-2",onClick:c=>{c.stopPropagation(),a()}},"Settings"),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}`:"")),d.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},d.map(c=>e.createElement(rt,{key:c,name:c,connected:t.connected.includes(c),connecting:r,armed:n===c,onArm:g=>i(c,g)}))))},V=Yt;var W=(t,o)=>t.includes(o)?t.filter(n=>n!==o):[...t,o],nt=(t,o)=>({...o?.choices||{},...t?.choices||{}});var Ut=({as:t="input",disabled:o,className:n,...r})=>e.createElement(t,{...r,disabled:o,className:C("ks-input",o&&"ks-input-disabled",n)}),S=Ut;var Xt=t=>e.createElement(S,{...t,as:"select"}),st=Xt;var Gt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:C("ks-checkbox",t)}),L=Gt;var at=t=>(t||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),qt=({type:t,value:o,choices:n,onChange:r,onSettle:a})=>{if(t==="boolean")return e.createElement(L,{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:""}),at(n).map(l=>e.createElement("option",{key:l.value,value:l.value},l.label)));if(t==="multi_select"){let l=Array.isArray(o)?o:[],i=y=>a(W(l,y));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(n||[]).map(y=>e.createElement("label",{key:y,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(L,{checked:l.includes(y),onChange:()=>i(y)}),e.createElement("span",null,y))))}if(t==="flows"){let l=Array.isArray(o)?o:[];return e.createElement("div",{style:{display:"flex",flexDirection:"column",gap:4},className:"mb-3"},at(n).map(i=>e.createElement("label",{key:i.value,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(L,{checked:l.includes(i.value),onChange:()=>a(W(l,i.value))}),e.createElement("span",null,i.label))))}return t==="integer"||t==="float"?e.createElement(S,{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(S,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},I=qt;var lt=(t,o,n,r)=>t.map((a,l)=>l===o?{...a,[n]:r}:a);var Jt=(t,o,n,r)=>{let a=(t||{})[n],l=(o||{})[n];return l?(a||{})[r[l]]:a},Kt=({holds:t,labels:o,rows:n,choices:r,outputsOf:a,onChange:l,onSettle:i})=>{let y=Array.isArray(n)?n:[],p=(f,m,d,h)=>{let c=lt(y,f,m,d);h?i(c):l(c)};return e.createElement("div",{style:{marginBottom:12}},y.map((f,m)=>e.createElement("div",{key:m,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([d,h])=>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(I,{type:h,value:f[d],choices:Jt(r,a,d,f),onChange:c=>p(m,d,c,!1),onSettle:c=>p(m,d,c,!0)}))),e.createElement(x,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>i(y.filter((d,h)=>h!==m))},"Remove"))),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>i([...y,{}])},"Add"))},it=Kt;var Zt=({as:t="div",className:o,children:n,...r})=>e.createElement(t,{...r,className:C("ks-panel",o)},n),A=Zt;var M=(t,o)=>({className:n,children:r,...a})=>e.createElement(t,{...a,className:C(o,n)},r),P=M("label","ks-label"),er=M("p","ks-hint"),or=M("p","ks-error"),rr=M("span","ks-required");var Qt={width:280,padding:20,overflowY:"auto"},Rt=({node:t,fields:o,holds:n,labels:r,recordLabels:a,recordChoices:l,recordOutputsOf:i,choices:y,onSave:p,onDelete:f,onClose:m})=>{let[d,h]=b(t.config);T(()=>h(t.config),[t.id,t.config]);let c=g=>{h(g),JSON.stringify(g)!==JSON.stringify(t.config)&&p(g)};return e.createElement(A,{as:"aside",style:Qt,"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:m,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,w])=>e.createElement(P,{key:g},e.createElement("span",{style:{display:"block",marginBottom:3}},r[g]||g),w==="list"?e.createElement(it,{holds:n[g]||{},labels:a[g]||{},rows:d[g],choices:{...(l||{})[g],...(t.record_choices||{})[g]},outputsOf:(i||{})[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}):e.createElement(I,{type:w,value:d[g],choices:y[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}))),e.createElement(x,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},dt=Rt;var te={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},ee="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",oe="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",re=({node:t,dragging:o,onFill:n,onDrop:r})=>e.createElement("div",{ref:t.ref,"data-placeholder":t.id,title:`Choose the step \u201C${t.label}\u201D should lead to`,onClick:n,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?oe:ee,style:te},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")),ct=re;var ne=({entries:t,at:o,onPick:n,onConnect:r,onDismiss:a})=>e.createElement(A,{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(x,{key:l.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>n(l)},l.label,e.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},l.type))),r&&e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),pt=ne;var se={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},mt=({label:t,title:o,idle:n,enabled:r,onUse:a})=>e.createElement(x,{variant:"secondary",size:"sm",title:r?o:n,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},t),ae=({undoable:t,redoable:o,onAdd:n,onUndo:r,onRedo:a})=>e.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},e.createElement(x,{variant:"secondary",className:"rounded-full",style:se,title:"Add a step",onClick:n},"+"),e.createElement(mt,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(mt,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),ut=ae;var ft=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var gt=({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 le={width:280,padding:20,overflowY:"auto"},Y={fontWeight:600,marginBottom:6,marginTop:14},O={fontSize:12,marginBottom:4,lineHeight:1.4},yt={display:"block",marginBottom:3},ht=(t,o,n)=>r=>{r.target.value!==(t[o]??"")&&n({[o]:r.target.value})},ie=({flow:t,changes:o,problems:n,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:y,onClose:p})=>e.createElement(A,{as:"aside",style:le,"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},gt(t)),e.createElement("h2",{style:Y},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...O,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...O,fontWeight:600},"data-notice":!0},a),n.length===0&&!r?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):n.map(f=>e.createElement("p",{key:`${f.node}-${f.problem}`,className:"text-amber-700 dark:text-amber-400",style:O,"data-problem":!0},"\u26A0 ",ft(f))),e.createElement("h2",{style:Y},"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((f,m)=>e.createElement("p",{key:m,style:O,"data-change":!0},f)),e.createElement("h2",{style:Y},"Details"),e.createElement(P,null,e.createElement("span",{style:yt},"Title"),e.createElement(S,{className:"mb-3",defaultValue:t.title??"",onBlur:ht(t,"title",y),"data-flow-title":!0})),e.createElement(P,null,e.createElement("span",{style:yt},"Start label"),e.createElement(S,{className:"mb-3",defaultValue:t.start_label??"",onBlur:ht(t,"start_label",y),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(x,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),xt=ie;var bt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},de=({link:t,onInsert:o,onRemove:n,onDrop:r,dragging:a})=>{let[l,i]=b(!1),y=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:y?1:0,transition:"opacity .12s"}},e.createElement(x,{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:bt},"+"),!a&&e.createElement(x,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:bt},"\xD7")))},vt=de;var ce=({links:t,extent:o,dragging:n,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(vt,{key:i.id,link:i,dragging:!!n,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),kt=ce;var pe="Your change was not saved because the server could not be reached.",wt=({base:t,token:o,fetch:n,onFlow:r,onError:a,onNotice:l})=>async(i,y,p)=>{let f=await n(t+i,{method:y,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:p&&JSON.stringify(p)}).catch(()=>null);if(!f)return l(null),a(pe);let m=await f.json().catch(()=>({}));if(!f.ok)return l(null),a(m.error||"That change was refused");a(null),l(m.notice||null),r(await(await n(t+".json",{headers:{Accept:"application/json"}})).json())};var me=(t,o,n)=>{let[r,a]=b(n),[l,i]=b(null),[y,p]=b(null),f=D(()=>wt({base:t,token:o,fetch:(...m)=>window.fetch(...m),onFlow:a,onError:i,onNotice:p}),[t,o]);return{flow:r,error:l,notice:y,send:f}},Ct=me;var H=(t,o,n,r)=>{let a=t.left-n.left+r.scrollLeft,l=t.top-n.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}},ue=(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},fe=(t,o,n,r,a)=>{let l={straight:`M ${o.x} ${o.y} L ${n.x} ${n.y}`,turn:`M ${o.x} ${o.y} H ${n.x} V ${n.y}`,lane:`M ${o.x} ${o.y} V ${r} H ${n.x} V ${n.y}`,detour:`M ${o.x} ${o.y} H ${a} V ${n.y} H ${n.x}`}[t.route];return{...t,path:l,midX:t.route==="detour"?a:(o.x+n.x)/2,midY:t.route==="lane"?r:(o.y+n.y)/2}},ge=(t,o,n,r)=>{let[a,l]=b([]),[i,y]=b({width:"100%",height:"100%"});return Z(()=>{let p=()=>{let m=o.current?.getBoundingClientRect();m&&(y({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(d=>{let h=n.current[d.source]?.getBoundingClientRect(),c=n.current[d.target]?.getBoundingClientRect();if(!h||!c)return[];let g=H(h,d.leaves,m,o.current),w=ue(H(c,d.enters,m,o.current),d.enters),v=(H(h,"bottom",m,o.current).y+H(c,"top",m,o.current).y)/2,B=d.leaves==="left"?Math.min(g.x,w.x)-28:Math.max(g.x,w.x)+28;return[fe(d,g,w,v,B)]})))};p(),window.addEventListener("resize",p);let f=new ResizeObserver(p);return o.current&&f.observe(o.current),()=>{window.removeEventListener("resize",p),f.disconnect()}},r),{links:a,extent:i}},St=ge;var Nt=(t,o)=>{let n=new Set(o),r=t,a=2;for(;n.has(r);)r=`${t}_${a++}`;return r};var xe={display:"flex",height:"100%",minHeight:0,fontSize:13},be={flex:1,overflow:"auto",position:"relative"},ve={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},ke={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},Ce=({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")),Se=({base:t,token:o,initial:n})=>{let{flow:r,error:a,notice:l,send:i}=Ct(t,o,n),[y,p]=b(null),[f,m]=b(null),[d,h]=b(null),[c,g]=b(null),[w,v]=b(!1),B=j({}),E=j(null);T(()=>{let s=()=>{p(null),v(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),T(()=>{if(!w)return;let s=u=>{u.target.closest("[data-builder-panel], [data-open-panel]")||v(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[w]),T(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let Bt=D(()=>Object.fromEntries(r.nodes.map(s=>[s.id,s])),[r.nodes]),{links:Et,extent:Tt}=St(r,E,B,[r,y,Bt]),U=D(()=>{let s={};return r.violations.forEach(u=>{var k;return(s[k=u.node]||(s[k]=[])).push(u)}),s},[r.violations]),F=r.nodes.filter(s=>!s.loose),X=r.nodes.filter(s=>s.loose),$t=Math.max(0,...F.map(s=>s.row))+1,At=Math.max(0,...F.map(s=>s.column))+1,G=s=>{if(!d)return;let[u,k]=d;h(null),u!==s&&i("/edges","POST",{from:u,to:s,on:k||null})},N=r.nodes.find(s=>s.id===y),z=r.palette.find(s=>s.type===N?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:xe,onMouseUp:()=>g(null),onKeyDown:s=>{s.key==="Escape"&&(p(null),h(null),m(null))},tabIndex:-1},e.createElement("div",{ref:E,className:"bg-surface-50 dark:bg-surface-950",style:be,onClick:s=>{s.target===E.current&&(p(null),h(null),v(!1))}},d&&e.createElement(Ce,{port:d[1],onCancel:()=>h(null)}),e.createElement(ut,{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(kt,{links:Et,extent:Tt,dragging:c,onInsert:s=>m({from:s.source,to:s.target,at:{x:s.midX+30,y:s.midY}}),onRemove:s=>{p(null),i("/edges","DELETE",{from:s.source,to:s.target})},onDrop:s=>{let u=c;g(null),i("/steps/"+u+"/move","PATCH",{from:s.source,to:s.target})}}),X.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:ve},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"),X.map(s=>e.createElement("div",{key:s.id,style:{marginBottom:8}},e.createElement(V,{node:{...s,ref:u=>{B.current[s.id]=u},violations:U[s.id]||[],connected:[]},selected:y===s.id,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d,onSelect:()=>d?G(s.id):p(s.id),onOpen:u=>window.location.assign(u),onArm:(u,k)=>{k.stopPropagation(),h([s.id,u])},onDragStart:()=>g(s.id),onDragEnd:()=>g(null)})))),e.createElement("div",{style:{...ke,gridTemplateColumns:`repeat(${At+1}, ${280/2}px)`,gridTemplateRows:`repeat(${$t}, auto)`}},F.map(s=>e.createElement("div",{key:s.id,style:{gridRow:s.row+1,gridColumn:`${s.column+1} / span 2`,zIndex:2}},s.placeholder&&e.createElement(ct,{node:{...s,ref:u=>{B.current[s.id]=u}},dragging:!!c,onFill:()=>m({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let u=c;g(null),u&&i("/edges","POST",{from:s.from,to:u,on:s.on})}}),!s.placeholder&&e.createElement(V,{node:{...s,ref:u=>{B.current[s.id]=u},violations:U[s.id]||[],connected:r.edges.filter(u=>u.source===s.id).map(u=>u.label||null)},selected:s.id===y,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d&&d[0]!==s.id,onSelect:()=>d?G(s.id):p(s.id),onOpen:u=>window.location.assign(u),onArm:(u,k)=>{let q=E.current.getBoundingClientRect();m({from:s.id,on:u,at:{x:k.clientX-q.left+E.current.scrollLeft+8,y:k.clientY-q.top+E.current.scrollTop+8}})},onDragEnd:()=>g(null),onDragStart:()=>g(s.id)})))),f&&e.createElement(pt,{entries:r.palette,at:f.at,onDismiss:()=>m(null),onConnect:f.on!==void 0?()=>{h([f.from,f.on]),m(null)}:null,onPick:s=>{let u=f;m(null),i("/steps","POST",{id:Nt(s.type,r.nodes.map(k=>k.id)),type:s.type,from:u.from,to:u.to,on:u.on})}})),w&&!N&&e.createElement(xt,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>v(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:s=>i("/details","PATCH",{flow:s})}),N&&e.createElement(dt,{node:N,fields:z?.fields||{},holds:z?.records||{},labels:z?.labels||{},recordLabels:z?.record_labels||{},recordChoices:z?.record_choices||{},recordOutputsOf:z?.record_outputs_of||{},choices:nt(N,z),onClose:()=>p(null),onSave:s=>i(`/steps/${N.id}`,"PATCH",{config:s}),onDelete:()=>{p(null),i(`/steps/${N.id}`,"DELETE")}}))},zt=Se;R("easy_flow/flow-editor",zt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});ot(document,J);})();
|
|
@@ -177,10 +177,19 @@ module EasyFlow
|
|
|
177
177
|
objections = step_type.settings.objections(values)
|
|
178
178
|
raise InvalidEdit, objections.join(", ") if objections.any?
|
|
179
179
|
|
|
180
|
+
foreign = foreign_flows(step_type, values)
|
|
181
|
+
raise InvalidEdit, foreign.join(", ") if foreign.any?
|
|
182
|
+
|
|
180
183
|
refused = step_type.settings.keep(values)
|
|
181
184
|
raise InvalidEdit, refused.join(", ") if refused.any?
|
|
182
185
|
end.then { |values| step_type.settings.unkept(values) }
|
|
183
186
|
end
|
|
187
|
+
|
|
188
|
+
def foreign_flows(step_type, values)
|
|
189
|
+
step_type.settings.fields.select { |name, type| %i[flow flows].include?(type) && values[name.to_s].present? }
|
|
190
|
+
.reject { |name, _type| Array(values[name.to_s]).all? { |id| flow.reaches?(Definition.find_by(id: id)) } }
|
|
191
|
+
.map { |name, _type| "#{step_type.settings.labels[name]} names another owner's flow" }
|
|
192
|
+
end
|
|
184
193
|
end
|
|
185
194
|
end
|
|
186
195
|
end
|
|
@@ -30,6 +30,18 @@ const Control = ({ type, value, choices, onChange, onSettle }) => {
|
|
|
30
30
|
</div>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
if (type === "flows") {
|
|
34
|
+
const chosen = Array.isArray(value) ? value : []
|
|
35
|
+
return <div style={{ display: "flex", flexDirection: "column", gap: 4 }} className="mb-3">
|
|
36
|
+
{offered(choices).map((choice) => (
|
|
37
|
+
<label key={choice.value} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
38
|
+
<Checkbox checked={chosen.includes(choice.value)} onChange={() => onSettle(toggled(chosen, choice.value))} />
|
|
39
|
+
<span>{choice.label}</span>
|
|
40
|
+
</label>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
if (type === "integer" || type === "float") {
|
|
34
46
|
return <Input className="mb-3" type="number" step={type === "integer" ? "1" : "any"} value={value ?? ""}
|
|
35
47
|
onChange={(e) => onChange(e.target.value)} onBlur={(e) => onSettle(e.target.value)} />
|
|
@@ -137,7 +137,7 @@ module EasyFlow
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def naming_flows(node)
|
|
140
|
-
step_type_for(node)&.settings&.fields.to_h.select { |_name, type| type
|
|
140
|
+
step_type_for(node)&.settings&.fields.to_h.select { |_name, type| %i[flow flows].include?(type) }.keys.map(&:to_s)
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def offered_flows
|
|
@@ -93,7 +93,9 @@ module EasyFlow
|
|
|
93
93
|
def questions_left(state)
|
|
94
94
|
cursor = next_step(state) && step(next_step(state).id)
|
|
95
95
|
left = 0
|
|
96
|
-
|
|
96
|
+
visits = Hash.new(0)
|
|
97
|
+
while cursor && decided?(cursor, state) && visits[cursor.id].zero?
|
|
98
|
+
visits[cursor.id] += 1
|
|
97
99
|
left += 1 if step_type(cursor)&.awaits_input?
|
|
98
100
|
cursor = successor(cursor, state)
|
|
99
101
|
end
|
|
@@ -162,6 +164,8 @@ module EasyFlow
|
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
def successor(node, state, times = {})
|
|
167
|
+
return node if step_type(node)&.comes_back?(node, state)
|
|
168
|
+
|
|
165
169
|
leaving = @document.edges_from(node.id)
|
|
166
170
|
port = step_type(node)&.route(node, state, times)
|
|
167
171
|
taken = port.nil? ? leaving.first : leaving.find { |edge| edge.on.to_s == port.to_s }
|
|
@@ -17,7 +17,7 @@ module EasyFlow
|
|
|
17
17
|
|
|
18
18
|
def self.endings_of(node)
|
|
19
19
|
version = ChosenFlow.of(node).version
|
|
20
|
-
Array(version&.definition.to_h["nodes"]).select { |step| step["type"] == "terminal" }.
|
|
20
|
+
Array(version&.definition.to_h["nodes"]).select { |step| step["type"] == "terminal" }.map { |step| step["output"].presence || "ended" }.uniq
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def route(node, state)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module EasyFlow
|
|
2
|
+
class PlayFlows
|
|
3
|
+
include Step
|
|
4
|
+
|
|
5
|
+
step_name "Play flows"
|
|
6
|
+
|
|
7
|
+
setting :question, type: :string
|
|
8
|
+
setting :flows, type: :flows, required: true
|
|
9
|
+
|
|
10
|
+
names_by :question
|
|
11
|
+
awaits_input
|
|
12
|
+
|
|
13
|
+
plays_the_picked_flow
|
|
14
|
+
|
|
15
|
+
comes_back_while { |node, state| PlayFlows.visits(node, state) < PlayFlows.playable(node).size }
|
|
16
|
+
|
|
17
|
+
displays_by { |node, run| Asked.new(id: node.id.to_sym, text: node.config["question"], choices: PlayFlows.offered(node, run)) }
|
|
18
|
+
|
|
19
|
+
def self.visits(node, state)
|
|
20
|
+
state.keys.count { |key| key.to_s == node.id || key.to_s.start_with?("#{node.id}@") }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.playable(node)
|
|
24
|
+
Definition.where(id: Array(node.config["flows"])).select(&:live_version)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.offered(node, run)
|
|
28
|
+
return [] unless run
|
|
29
|
+
|
|
30
|
+
listed = Array(node.config["flows"]) - played(node, run)
|
|
31
|
+
playable(node).select { |flow| listed.include?(flow.id.to_s) && run.flow.reaches?(flow) }.sort_by { |flow| listed.index(flow.id.to_s) }.map { |flow| Choice.new(value: flow.id.to_s, label: flow.title) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.played(node, run)
|
|
35
|
+
step = node.id.to_s.split("@").first
|
|
36
|
+
run.inner_runs.where(parent_step: step).or(run.inner_runs.where("parent_step LIKE ?", "#{Run.sanitize_sql_like(step)}@%")).pluck(:flow_id).map(&:to_s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -18,6 +18,8 @@ module EasyFlow
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def record(id, value)
|
|
21
|
+
return play(id, value) if plays_the_picked_flow?(id)
|
|
22
|
+
|
|
21
23
|
@run.record(id.to_sym, value)
|
|
22
24
|
end
|
|
23
25
|
|
|
@@ -27,6 +29,7 @@ module EasyFlow
|
|
|
27
29
|
chosen = ChosenFlow.of(node, run: @run)
|
|
28
30
|
raise InnerFlowError, "The step #{node.id} chose no flow to run" if chosen.flow.nil? && EasyFlow.registry.fetch(node.type).chooses_flow_from_run?
|
|
29
31
|
raise InnerFlowError, "The Flow step #{node.id} names a version that cannot run" if chosen.version&.out_of_service?
|
|
32
|
+
raise InnerFlowError, "The Flow step #{node.id} names another owner's flow" if chosen.flow && !@run.flow.reaches?(chosen.flow)
|
|
30
33
|
Run.start(chosen.flow || raise(ActiveRecord::RecordNotFound), version: chosen.version)
|
|
31
34
|
.tap { |inner| inner.update!(parent_run: @run, parent_step: node.id, owner: @run.owner) }.advance
|
|
32
35
|
end
|
|
@@ -36,16 +39,10 @@ module EasyFlow
|
|
|
36
39
|
return if @run.stopped_on_loop
|
|
37
40
|
return if parent.recorded.key?(@run.parent_step.to_sym)
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
parent.record(@run.parent_step.to_sym, ending) if ending
|
|
42
|
+
parent.record(@run.parent_step.to_sym, @run.output || "ended")
|
|
41
43
|
AdvanceParentJob.perform_later(parent, @run.parent_step)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def chose_from_run?(parent)
|
|
45
|
-
type = parent.pinned_steps.dig(@run.parent_step, "type")
|
|
46
|
-
EasyFlow.registry.registered?(type) && EasyFlow.registry.fetch(type).chooses_flow_from_run?
|
|
47
|
-
end
|
|
48
|
-
|
|
49
46
|
def finish(_state)
|
|
50
47
|
@run
|
|
51
48
|
end
|
|
@@ -53,6 +50,25 @@ module EasyFlow
|
|
|
53
50
|
def discard_last
|
|
54
51
|
@run.discard_last
|
|
55
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def plays_the_picked_flow?(id)
|
|
57
|
+
node = @run.digest.step(id)
|
|
58
|
+
node && EasyFlow.registry.registered?(node.type) && EasyFlow.registry.fetch(node.type).plays_the_picked_flow?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def play(id, flow_id)
|
|
62
|
+
return if @run.inner_runs.exists?(parent_step: id.to_s)
|
|
63
|
+
return unless offered?(id, flow_id)
|
|
64
|
+
|
|
65
|
+
Run.start(Definition.find(flow_id)).tap { |inner| inner.update!(parent_run: @run, parent_step: id.to_s, owner: @run.owner) }.advance
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def offered?(id, flow_id)
|
|
69
|
+
node = @run.digest.step(id)
|
|
70
|
+
EasyFlow.registry.fetch(node.type).display_of(node.with(id: id.to_s), @run).choices.map(&:value).include?(flow_id.to_s)
|
|
71
|
+
end
|
|
56
72
|
end
|
|
57
73
|
end
|
|
58
74
|
end
|
|
@@ -24,6 +24,7 @@ module EasyFlow
|
|
|
24
24
|
@ends_here = false
|
|
25
25
|
@begins_here = false
|
|
26
26
|
@starts_a_flow = false
|
|
27
|
+
@plays_the_picked_flow = false
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def output(name, type: :string, label: nil, values: nil, from: nil)
|
|
@@ -58,6 +59,10 @@ module EasyFlow
|
|
|
58
59
|
@answer_labelling = labelling
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
def comes_back_while(&return_rule)
|
|
63
|
+
@return_rule = return_rule
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
def waits_until(&readiness)
|
|
62
67
|
@readiness = readiness
|
|
63
68
|
end
|
|
@@ -82,6 +87,10 @@ module EasyFlow
|
|
|
82
87
|
@starts_a_flow = true
|
|
83
88
|
end
|
|
84
89
|
|
|
90
|
+
def plays_the_picked_flow
|
|
91
|
+
@plays_the_picked_flow = true
|
|
92
|
+
end
|
|
93
|
+
|
|
85
94
|
def chooses_flow_from_run(&chooser)
|
|
86
95
|
@run_chooser = chooser
|
|
87
96
|
end
|
|
@@ -140,9 +149,9 @@ module EasyFlow
|
|
|
140
149
|
|
|
141
150
|
def to_step_type
|
|
142
151
|
StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
|
|
143
|
-
ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, flow_chooser: @flow_chooser, run_chooser: @run_chooser, behaviour: @behaviour, routing: @routing,
|
|
152
|
+
ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, plays_the_picked_flow: @plays_the_picked_flow, flow_chooser: @flow_chooser, run_chooser: @run_chooser, behaviour: @behaviour, routing: @routing,
|
|
144
153
|
display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
|
|
145
|
-
outputs: @declared_outputs, outputs_named_by: @outputs_named_by, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness)
|
|
154
|
+
outputs: @declared_outputs, outputs_named_by: @outputs_named_by, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness, return_rule: @return_rule)
|
|
146
155
|
end
|
|
147
156
|
|
|
148
157
|
def settings
|
|
@@ -7,8 +7,8 @@ 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, starts_a_flow: false, flow_chooser: nil, run_chooser: nil, display: nil, drawn_by: nil,
|
|
11
|
-
naming_field: nil, naming: nil, outputs: [], outputs_named_by: nil, answer_check: nil, answer_labelling: nil, readiness: nil)
|
|
10
|
+
ends_here: false, begins_here: false, starts_a_flow: false, plays_the_picked_flow: false, flow_chooser: nil, run_chooser: nil, display: nil, drawn_by: nil,
|
|
11
|
+
naming_field: nil, naming: nil, outputs: [], outputs_named_by: nil, answer_check: nil, answer_labelling: nil, readiness: nil, return_rule: nil)
|
|
12
12
|
@id = id
|
|
13
13
|
@step_name = step_name
|
|
14
14
|
@settings = settings
|
|
@@ -16,6 +16,7 @@ module EasyFlow
|
|
|
16
16
|
@ends_here = ends_here
|
|
17
17
|
@begins_here = begins_here
|
|
18
18
|
@starts_a_flow = starts_a_flow
|
|
19
|
+
@plays_the_picked_flow = plays_the_picked_flow
|
|
19
20
|
@flow_chooser = flow_chooser
|
|
20
21
|
@run_chooser = run_chooser
|
|
21
22
|
@behaviour = behaviour
|
|
@@ -29,6 +30,7 @@ module EasyFlow
|
|
|
29
30
|
@answer_check = answer_check
|
|
30
31
|
@answer_labelling = answer_labelling
|
|
31
32
|
@readiness = readiness
|
|
33
|
+
@return_rule = return_rule
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def display_of(node, run = nil)
|
|
@@ -61,6 +63,10 @@ module EasyFlow
|
|
|
61
63
|
@readiness.call(node, state, times)
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def comes_back?(node, state)
|
|
67
|
+
@return_rule&.call(node, state) || false
|
|
68
|
+
end
|
|
69
|
+
|
|
64
70
|
def name_of(node)
|
|
65
71
|
@naming&.call(node).presence || node.config[naming_field.to_s].presence || worked_out_flow_name(node)
|
|
66
72
|
end
|
|
@@ -97,6 +103,10 @@ module EasyFlow
|
|
|
97
103
|
@starts_a_flow
|
|
98
104
|
end
|
|
99
105
|
|
|
106
|
+
def plays_the_picked_flow?
|
|
107
|
+
@plays_the_picked_flow
|
|
108
|
+
end
|
|
109
|
+
|
|
100
110
|
def chooses_flow_from_run?
|
|
101
111
|
!@run_chooser.nil?
|
|
102
112
|
end
|
|
@@ -10,7 +10,7 @@ module EasyFlow
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def violations
|
|
13
|
-
structural_violations + unmet_requirements + missing_settings + missing_values + missing_flows + unrunnable_versions + circular_flows + beginnings + asked_for
|
|
13
|
+
structural_violations + unmet_requirements + missing_settings + missing_values + missing_flows + unrunnable_versions + foreign_flows + circular_flows + beginnings + asked_for
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def structural_violations
|
|
@@ -140,7 +140,7 @@ module EasyFlow
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def missing_flows
|
|
143
|
-
starting_a_flow.reject { |node| ChosenFlow.of(node).flow }
|
|
143
|
+
(starting_a_flow.reject { |node| ChosenFlow.of(node).flow } + listing_flows.select { |node| listed_ids(node).size > listed_flows(node).size })
|
|
144
144
|
.map { |node| Violation.new(node: node.id, problem: :missing_flow) }
|
|
145
145
|
end
|
|
146
146
|
|
|
@@ -153,6 +153,32 @@ module EasyFlow
|
|
|
153
153
|
version&.live? || version&.superseded? || false
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
def foreign_flows
|
|
157
|
+
return [] unless @flow
|
|
158
|
+
|
|
159
|
+
(starting_a_flow.select { |node| ChosenFlow.of(node).flow }.reject { |node| @flow.reaches?(ChosenFlow.of(node).flow) } +
|
|
160
|
+
listing_flows.reject { |node| listed_flows(node).all? { |listed| @flow.reaches?(listed) } })
|
|
161
|
+
.map { |node| Violation.new(node: node.id, problem: :owned_by_another_owner) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def listing_flows
|
|
165
|
+
@document.nodes.select { |node| flow_lists(node).any? }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def listed_flows(node)
|
|
169
|
+
Definition.where(id: listed_ids(node))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def listed_ids(node)
|
|
173
|
+
flow_lists(node).flat_map { |name| Array(node.config[name.to_s]) }.uniq
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def flow_lists(node)
|
|
177
|
+
return [] unless @registry.registered?(node.type)
|
|
178
|
+
|
|
179
|
+
@registry.fetch(node.type).settings.fields.select { |_name, type| type == :flows }.keys
|
|
180
|
+
end
|
|
181
|
+
|
|
156
182
|
def circular_flows
|
|
157
183
|
return [] unless @flow
|
|
158
184
|
|
data/lib/easy_flow/engine.rb
CHANGED
data/lib/easy_flow/version.rb
CHANGED
data/lib/easy_flow.rb
CHANGED
|
@@ -7,7 +7,7 @@ require "easy_flow/host_routes"
|
|
|
7
7
|
require "easy_flow/engine"
|
|
8
8
|
|
|
9
9
|
module EasyFlow
|
|
10
|
-
FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list flow].freeze
|
|
10
|
+
FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list flow flows].freeze
|
|
11
11
|
|
|
12
12
|
OUTPUT_TYPES = %i[string integer float boolean].freeze
|
|
13
13
|
OPTIONAL_CHECKS = %i[unrouted_value unfollowed_path dead_end].freeze
|
|
@@ -158,6 +158,8 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
158
158
|
- When the block returns `nil`, a blank `flow:`, or the id of no existing flow, `EasyFlow::InnerFlowError` is raised with a message naming the step, such as `The step pick chose no flow to run`, and no run is started. A retired or withdrawn version raises `EasyFlow::InnerFlowError` as for a Flow step, and a blank version number, one naming no version of the flow, or one naming a draft raises `ActiveRecord::RecordInvalid`.
|
|
159
159
|
- When the started run ends with an output, that output is recorded under the step's id as for a Flow step. When it ends with no output, `"ended"` is recorded instead, and the parent run moves on. A type that routes on what is recorded declares an `output` whose `values:` include `"ended"` beside the outputs the chosen flows can end with. Ask the developer what `"ended"` should lead to.
|
|
160
160
|
- When a loop stops the started run, nothing is recorded, not even `"ended"`, no job is queued, and the parent run stays waiting at the step. See step 5 of "Follow a run into the flow a Flow step started".
|
|
161
|
+
- `plays_the_picked_flow` — for an `awaits_input` type, the visitor's answer at a stored run is the id of a flow to play rather than a value to record. The engine starts a run of that flow on its live version as an inner run of the visitor's run, linked to the visit the answer was given at, and records nothing at that visit until the inner run ends. It plays a flow only when it is among the choices the step's `displays_by` block offers the run, so a pick of a flow the step no longer offers plays nothing, and it plays nothing at a visit that already has an inner run. Before writing such a type, ask the developer whether the built-in Play flows step is enough.
|
|
162
|
+
- `comes_back_while { |node, state| ... }` — after a visit to the step is recorded, the run comes back to the same step under its next visit key while the block returns a truthy value, and follows the step's connection out once it does not. The block is given the base node and the answers recorded on the path so far, every visit under its own key. Use it for a step a visitor repeats a set number of times; ask the developer what decides that number.
|
|
161
163
|
- A flow set to save on finish or to save nothing, and an admin's preview, keep no stored run, so the block is not called and no run is started.
|
|
162
164
|
|
|
163
165
|
```ruby
|
|
@@ -11,7 +11,7 @@ This local explains easy_flow and the words it uses. It makes no changes and giv
|
|
|
11
11
|
|
|
12
12
|
easy_flow is a Rails engine for guided, branching flows. An admin builds a flow on a canvas by placing steps and connecting them, previews it, and publishes it. A visitor then walks the published flow one step at a time, and each answer decides which step comes next.
|
|
13
13
|
|
|
14
|
-
Reach for it when an app needs a questionnaire, an intake form, or any decision path that an admin should be able to change without a deploy. The engine ships a start step, an end step, a question step, a checklist step, a multiple select step, a fill in the blank step, a file upload step, five steps that pick a branch from earlier answers, a Count step that counts earlier answers, a Wait step that holds a run until a time,
|
|
14
|
+
Reach for it when an app needs a questionnaire, an intake form, or any decision path that an admin should be able to change without a deploy. The engine ships a start step, an end step, a question step, a checklist step, a multiple select step, a fill in the blank step, a file upload step, five steps that pick a branch from earlier answers, a Count step that counts earlier answers, a Wait step that holds a run until a time, a Flow step that names another flow, and a Play flows step that lets a visitor play several flows in the order they choose. Anything else a flow needs to ask, do, or wait for is a step type the app declares itself.
|
|
15
15
|
|
|
16
16
|
## Interface
|
|
17
17
|
|
|
@@ -80,6 +80,7 @@ easy_flow declares no commands of its own for this local. Its surface is split b
|
|
|
80
80
|
- **Wait step name** — what a Wait step is called on its canvas card and in the "Waiting for" line a visitor sees, following the same order as the rule that holds the run. A date set names it by that date, such as "2026-12-01". Otherwise it is named by the time of day, or by the minutes followed by "minutes", then "after" and the earlier step, such as "09:00 after signup" or "30 minutes after signup", with "minute" in place of "minutes" when the number is 1, such as "1 minute after signup". A Wait step with no date, time of day or minutes has no name of its own, so its canvas card shows the step's id and the visitor sees "Waiting for Wait".
|
|
81
81
|
- **Count** — a built-in step that acts on its own and records a whole number: how many visits to an earlier step the run has recorded. When the admin picks an answer, only visits that gave that answer are counted, and when the admin picks none, every visit is counted. The admin picks which of the earlier step's outputs the answer is read from, as described under chosen output. On the canvas the step is named "Count" followed by the earlier step and the answer it counts, such as "Count tests failed". Each visit to a Count step in a loop records a new count under its own visit key, so a Compare step after it can end the loop once the count reaches an amount.
|
|
82
82
|
- **Flow step** — a built-in step that names another flow and the number of one of its versions. The admin picks the flow from the same host's flows, listed by title as the admin's list of flows shows them. The step has one connection for each different output the End steps of that version write, and none when no flow or version is picked or no End step there has an output. On the canvas its card is named after the flow's title and the version, such as "Onboarding, version 2", and is marked "Runs a flow". Once a flow is picked, clicking the card opens that flow's canvas, and the card's Settings button opens the step's own settings instead. A canvas opened from a Flow step's card shows a "Back to" button named after the title of the flow it was opened from, or its slug when it has no title, which returns to that flow's canvas. The button only appears when that flow belongs to the same host, and to the same owner when the host has one. While the admin is connecting steps, clicking the card picks it as the connection's target. A run that reaches a Flow step stops there. When the run is a stored run, it starts one inner run of the named flow on the picked version, and a visitor on the run's page is taken to that inner run's first unanswered step. A run that carries its answers in the page, and an admin's preview, stop at the Flow step, start no run, and never move past it.
|
|
83
|
+
- **Play flows step** — a built-in step (`play_flows`) that lists several flows and lets a visitor play each one once, in the order they choose. The admin ticks the flows from the same host's flows, and only that owner's flows when the host has an owner, and fills in the question asked above them. A visitor whose stored run reaches the step picks one of the listed flows not yet played, and it plays as an inner run of the visitor's run, linked to that visit of the step, the same way a Flow step's inner run is. When that inner run ends, the step records its output, or `ended`, under the visit's key, and the visitor is back at the step under its next visit with only the flows not yet played offered. A pick of a flow already played plays nothing. Once every listed flow has been played the run follows the step's one connection out, and a listed flow that no longer exists or has no live version is neither offered nor waited for. Its card shows "missing flow" while a listed flow no longer exists, and in a host that names an owner, saving the step or publishing its flow is refused while it lists another owner's flow. A run that carries its answers in the page, and an admin's preview, offer no flows at the step.
|
|
83
84
|
- **Unrunnable Flow step** — a Flow step whose picked flow or version cannot be run. Its card shows "missing flow" when no flow is picked or the picked flow no longer exists, and "unrunnable version" when the picked flow has no version with that number or that version is a draft, retired or withdrawn. These are always on, unlike the optional checks below, and the flow holding such a step cannot be saved as a new version or published until it is fixed.
|
|
84
85
|
- **Circular Flow step** — a Flow step that leads back to the flow it sits in. That is either a Flow step naming its own flow, or one naming a flow whose picked version holds a Flow step that leads back the same way, through any number of flows. Each flow along the way is read at the version its Flow step picked. Its card shows "circular", and like an unrunnable Flow step this is always on and keeps the flow from being saved as a new version or published until it is fixed.
|
|
85
86
|
- **Flow-starting step type** — a step type of the app's own that starts another flow the way the Flow step does. By default it runs the flow and version named in its settings called flow and version. It may instead carry a rule that works out the flow and version from its other settings, and that choice is then used everywhere a Flow step's choice is: starting the inner run, the unrunnable and circular checks, and the canvas link to that flow. When the step type gives the step no name of its own, the step is named after the flow and version that rule works out, the same way a Flow step is, such as "Onboarding, version 2", on its canvas card and in the "Waiting for" line a visitor sees. Everything said below about a Flow step's inner run and handing back applies to it too, except where a run-chosen flow says otherwise.
|
|
@@ -55,7 +55,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
55
55
|
- `visitor_authorization_method` — a method on the base controller, called with the flow, returning true when the visitor may run it. With none set, every visitor is refused.
|
|
56
56
|
- `refusal_method` — a method on the base controller, called with the refusal error when a visitor is refused or a flow is unpublished or withdrawn. With none set, the response is `404 Not Found`.
|
|
57
57
|
- `owner_method` — a method on the base controller, called with no arguments, returning the record whose flows the current request works with, such as the signed-in account. When set, the admin pages list, create and edit only that record's flows, a visitor runs only that record's flows, and two records may each hold a flow with the same slug. With none set, every flow in the host is shared by everyone who reaches it. Ask the developer whether each host's flows belong to one record or are shared.
|
|
58
|
-
- `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `checklist`, `multiple_select`, `fill_in_the_blank`, `file_upload`, `condition`, `switch`, `compare`, `compare_two_values`, `all_or_any`, `count`, `wait` and `
|
|
58
|
+
- `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `checklist`, `multiple_select`, `fill_in_the_blank`, `file_upload`, `condition`, `switch`, `compare`, `compare_two_values`, `all_or_any`, `count`, `wait`, `flow_step` and `play_flows`, `file_upload` being the File upload step, which needs the file store from step 10, `compare_two_values` being the Compare two values step, which compares the outputs of two earlier steps, `all_or_any` being the All of or Any of step, which decides whether all or any of several such comparisons hold, `count` being the Count step, which counts how often an earlier step was answered, `wait` being the Wait step, which holds a run until a time counted from an earlier answer or until a date, `flow_step` being the Flow step, which names one of the same host's flows, and `play_flows` being the Play flows step, which lists several of the same host's flows for a visitor to play in the order they choose. A step type the app declares is named by the id it was declared with, or after its class when it is a step class, so `Steps::Notify` is `notify`. The End step is offered whether it is listed or not, and the Start step is never offered. Ask the developer which step types each host should offer.
|
|
59
59
|
|
|
60
60
|
Each method named here must exist on the base controller. Ask the developer to point at it or write it. Do not invent its logic.
|
|
61
61
|
8. Mount the engine in `config/routes.rb`, once per host. The host name in `defaults` must match a name declared in step 7. When there is more than one mount, give each an `as:` name:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_flow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -123,6 +123,7 @@ files:
|
|
|
123
123
|
- app/models/easy_flow/name.rb
|
|
124
124
|
- app/models/easy_flow/node.rb
|
|
125
125
|
- app/models/easy_flow/output.rb
|
|
126
|
+
- app/models/easy_flow/play_flows.rb
|
|
126
127
|
- app/models/easy_flow/progress.rb
|
|
127
128
|
- app/models/easy_flow/progress/kept.rb
|
|
128
129
|
- app/models/easy_flow/progress/loose.rb
|