easy_flow 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_flow/canvas.js +1 -1
  3. data/app/controllers/easy_flow/flows_controller.rb +18 -4
  4. data/app/javascript/easy_flow/canvas/Canvas.jsx +1 -0
  5. data/app/javascript/easy_flow/canvas/Inspector.jsx +3 -1
  6. data/app/javascript/easy_flow/canvas/Records.jsx +8 -2
  7. data/app/models/easy_flow/all_or_any.rb +33 -0
  8. data/app/models/easy_flow/canvas.rb +17 -2
  9. data/app/models/easy_flow/compare.rb +2 -4
  10. data/app/models/easy_flow/compare_two_values.rb +32 -0
  11. data/app/models/easy_flow/completed_at.rb +34 -0
  12. data/app/models/easy_flow/condition.rb +3 -1
  13. data/app/models/easy_flow/digest.rb +19 -11
  14. data/app/models/easy_flow/progress/kept.rb +4 -0
  15. data/app/models/easy_flow/progress/loose.rb +12 -4
  16. data/app/models/easy_flow/progress.rb +2 -2
  17. data/app/models/easy_flow/run.rb +19 -6
  18. data/app/models/easy_flow/runner.rb +11 -11
  19. data/app/models/easy_flow/settings.rb +5 -2
  20. data/app/models/easy_flow/step.rb +6 -2
  21. data/app/models/easy_flow/step_type/declaration.rb +13 -3
  22. data/app/models/easy_flow/step_type.rb +18 -5
  23. data/app/models/easy_flow/validator.rb +12 -1
  24. data/app/models/easy_flow/wait.rb +38 -0
  25. data/app/views/easy_flow/flows/step.html.erb +6 -0
  26. data/db/migrate/20261011130000_keep_step_visits_whole_in_easy_flow_runs.rb +23 -0
  27. data/lib/easy_flow/engine.rb +3 -0
  28. data/lib/easy_flow/version.rb +1 -1
  29. data/the_local/agents/easy_flow-develop.md +100 -20
  30. data/the_local/agents/easy_flow-info.md +22 -10
  31. data/the_local/agents/easy_flow-install.md +18 -5
  32. data/the_local/interface.yml +5 -0
  33. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '093d6e31f420e50db193c425d8d523e75f242fc86f82b069962cc09e3639f79b'
4
- data.tar.gz: 4d87fc9a7f19b7be067a01502653aa8e271dff52df51de6971317b830ba6a370
3
+ metadata.gz: 7edfa93c9b14713a7ff4a65d05d6a2a1db3c5f5c53ae915496b0df374ee7b3d5
4
+ data.tar.gz: 9be1fef6ffb30e8e8063521dbc3ea880fc2957534a9be8fb54694c08316e86f7
5
5
  SHA512:
6
- metadata.gz: d4b69c37a43cfb0b9b4de6696b97063da84086ba23154a78bcc013577a6a6beff5a4ed6bf65647580ad88336b18486244662e4eb5792e39c4225616ca8a578bd
7
- data.tar.gz: 9ede6727e7d129813dd1ec212b6c18e0edf9846b17a20feba45756ff5986ad4f70af897df70d5e462ec2eb775f9725c7545a510c306dc09c4c02ec1ae828256e
6
+ metadata.gz: d1b72663067985bc5298a8f3ef1cf753b99efe7bc6df10cfda381808a918ebf50bb43286addbe3151b1120c3bc99349717598588d2481d7e35416338371fd4bd
7
+ data.tar.gz: e4d4eb6618457e868bbd076d95de63bbdec36f222129c7c5ad5207334b754f89d761b356e44ae8e53b018fc694320f79bbb99dd30c61e645c03271f2ac6ac3f6
@@ -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: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);})();
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);})();
@@ -15,8 +15,8 @@ module EasyFlow
15
15
  @progress = progress
16
16
  @guide.run(@progress)
17
17
  @answers = @progress.recorded
18
- @question = @guide.next_step(@answers, run: run)
19
- @drawing = @guide.drawing_at(@answers)
18
+ @question = @guide.next_step(@answers, run: run, times: @progress.completed_at)
19
+ @drawing = @guide.drawing_at(@answers, times: @progress.completed_at)
20
20
  flash.now[:alert] = @refused if @refused
21
21
  @waiting = waiting_on(@question)
22
22
  return render :step if @question
@@ -75,7 +75,7 @@ module EasyFlow
75
75
  end
76
76
 
77
77
  def render_completion
78
- @answered = @guide.state_on_path(@answers)
78
+ @answered = @guide.state_on_path(@answers, times: @progress.completed_at)
79
79
  finished(@answered, @progress.finish(@answered))
80
80
  end
81
81
 
@@ -86,7 +86,8 @@ module EasyFlow
86
86
  def progress
87
87
  return @progress ||= Progress.for(run.flow, run: run) if run
88
88
 
89
- @progress ||= Progress.for(flow, answers: submitted_answers, definition: running_definition)
89
+ answers = submitted_answers
90
+ @progress ||= Progress.for(flow, answers: answers, completed_at: submitted_times(answers), definition: running_definition)
90
91
  end
91
92
 
92
93
  def running_definition
@@ -128,6 +129,19 @@ module EasyFlow
128
129
  @refused ? answers.except(key) : answers.merge(key => answer)
129
130
  end
130
131
 
132
+ def submitted_times(answers)
133
+ given = params.fetch(:completed_at, {}).permit(*answers.keys).to_h.symbolize_keys
134
+ times = given.transform_values { |time| readable_time(time) }.compact
135
+ asked = params[:asked].to_s.to_sym
136
+ answers.key?(asked) && !times.key?(asked) ? times.merge(asked => Time.current) : times
137
+ end
138
+
139
+ def readable_time(time)
140
+ Time.zone.parse(time.to_s)
141
+ rescue ArgumentError
142
+ nil
143
+ end
144
+
131
145
  def one_answer_back(answers)
132
146
  answered = @guide.state_on_path(answers)
133
147
  loop do
@@ -187,6 +187,7 @@ const Canvas = ({ base, token, initial }) => {
187
187
  <Inspector node={selectedNode}
188
188
  fields={entryFor?.fields || {}} holds={entryFor?.records || {}}
189
189
  labels={entryFor?.labels || {}} recordLabels={entryFor?.record_labels || {}}
190
+ recordChoices={entryFor?.record_choices || {}} recordOutputsOf={entryFor?.record_outputs_of || {}}
190
191
  choices={offeredTo(selectedNode, entryFor)}
191
192
  onClose={() => setSelected(null)}
192
193
  onSave={(config) => send(`/steps/${selectedNode.id}`, "PATCH", { config })}
@@ -7,7 +7,7 @@ import { Label } from "keystone_ui-react/src/FieldText.jsx"
7
7
 
8
8
  const panel = { width: 280, padding: 20, overflowY: "auto" }
9
9
 
10
- const Inspector = ({ node, fields, holds, labels, recordLabels, choices, onSave, onDelete, onClose }) => {
10
+ const Inspector = ({ node, fields, holds, labels, recordLabels, recordChoices, recordOutputsOf, choices, onSave, onDelete, onClose }) => {
11
11
  const [ draft, setDraft ] = useState(node.config)
12
12
  useEffect(() => setDraft(node.config), [ node.id, node.config ])
13
13
 
@@ -29,6 +29,8 @@ const Inspector = ({ node, fields, holds, labels, recordLabels, choices, onSave,
29
29
  <span style={{ display: "block", marginBottom: 3 }}>{labels[name] || name}</span>
30
30
  {type === "list"
31
31
  ? <Records holds={holds[name] || {}} labels={recordLabels[name] || {}} rows={draft[name]}
32
+ choices={{ ...(recordChoices || {})[name], ...(node.record_choices || {})[name] }}
33
+ outputsOf={(recordOutputsOf || {})[name]}
32
34
  onChange={(next) => setDraft({ ...draft, [name]: next })}
33
35
  onSettle={(next) => settle({ ...draft, [name]: next })} />
34
36
  : <Control type={type} value={draft[name]} choices={choices[name]}
@@ -3,7 +3,13 @@ import Control from "./Control"
3
3
  import { amended } from "./rows"
4
4
  import Button from "keystone_ui-react/src/Button.jsx"
5
5
 
6
- const Records = ({ holds, labels, rows, onChange, onSettle }) => {
6
+ const offeredFor = (choices, outputsOf, name, row) => {
7
+ const offered = (choices || {})[name]
8
+ const source = (outputsOf || {})[name]
9
+ return source ? (offered || {})[row[source]] : offered
10
+ }
11
+
12
+ const Records = ({ holds, labels, rows, choices, outputsOf, onChange, onSettle }) => {
7
13
  const kept = Array.isArray(rows) ? rows : []
8
14
  const amend = (index, name, next, settle) => {
9
15
  const updated = amended(kept, index, name, next)
@@ -17,7 +23,7 @@ const Records = ({ holds, labels, rows, onChange, onSettle }) => {
17
23
  {Object.entries(holds).map(([ name, type ]) => (
18
24
  <label key={name} style={{ display: "block" }}>
19
25
  <span className="text-gray-500 dark:text-gray-400" style={{ display: "block", marginBottom: 2, fontSize: 11 }}>{(labels || {})[name] || name}</span>
20
- <Control type={type} value={row[name]}
26
+ <Control type={type} value={row[name]} choices={offeredFor(choices, outputsOf, name, row)}
21
27
  onChange={(next) => amend(index, name, next, false)}
22
28
  onSettle={(next) => amend(index, name, next, true)} />
23
29
  </label>
@@ -0,0 +1,33 @@
1
+ module EasyFlow
2
+ class AllOrAny
3
+ include Step
4
+
5
+ step_name "All of or Any of"
6
+
7
+ setting :join, type: :select, options: [ "all of", "any of" ], label: "Must hold", required: true
8
+ setting :comparisons, type: :list, required: true do
9
+ setting :step, type: :previous_step, required: true
10
+ setting :output, outputs_of: :step
11
+ setting :comparison, type: :select, options: CompareTwoValues::COMPARISONS.keys, required: true
12
+ setting :amount, type: :float
13
+ setting :other_step, type: :previous_step, label: "Second step"
14
+ setting :other_output, outputs_of: :other_step, label: "Second step's output"
15
+ end
16
+
17
+ output :result, type: :boolean, values: [ true, false ]
18
+
19
+ def route(node, state, times)
20
+ comparisons = Array(node.config["comparisons"])
21
+ return false if comparisons.empty?
22
+
23
+ held = ->(comparison) { holds?(comparison, state, times) }
24
+ node.config["join"] == "any of" ? comparisons.any?(&held) : comparisons.all?(&held)
25
+ end
26
+
27
+ private
28
+
29
+ def holds?(comparison, state, times)
30
+ CompareTwoValues.step_type.route(Node.new(id: "comparison", type: "compare_two_values", config: comparison), state, times)
31
+ end
32
+ end
33
+ end
@@ -22,12 +22,12 @@ module EasyFlow
22
22
  def drawn_step(node)
23
23
  { "id" => node.id, "type" => node.type, "label" => label_for(node),
24
24
  "config" => node.config.merge(kept_values(node)), "ports" => [], "ends_here" => ends_here?(node), "begins_here" => begins_here?(node),
25
- "starts_a_flow" => starts_a_flow?(node), "opens" => opens(node), "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), "record_choices" => record_choices_for(node), "placeholder" => false, **placed[node.id] }
26
26
  end
27
27
 
28
28
  def drawn_placeholder(gap)
29
29
  { "id" => gap[:id], "type" => "placeholder", "label" => gap[:on], "config" => {}, "ports" => [],
30
- "ends_here" => false, "begins_here" => false, "loose" => false, "choices" => {},
30
+ "ends_here" => false, "begins_here" => false, "loose" => false, "choices" => {}, "record_choices" => {},
31
31
  "placeholder" => true, "from" => gap[:from], "on" => gap[:on], **placed[gap[:id]] }
32
32
  end
33
33
 
@@ -91,6 +91,8 @@ module EasyFlow
91
91
  "labels" => step_type.settings.labels.transform_keys(&:to_s),
92
92
  "choices" => step_type.settings.choices.transform_keys(&:to_s),
93
93
  "records" => holdings_of(step_type),
94
+ "record_choices" => step_type.settings.record_choices.to_h { |name, offered| [ name.to_s, offered.transform_keys(&:to_s) ] },
95
+ "record_outputs_of" => step_type.settings.record_outputs_of.to_h { |name, sources| [ name.to_s, sources.to_h { |field, source| [ field.to_s, source.to_s ] } ] },
94
96
  "record_labels" => step_type.settings.record_labels.to_h { |name, held| [ name.to_s, held.transform_keys(&:to_s) ] },
95
97
  "awaits_input" => step_type.awaits_input? }
96
98
  end
@@ -121,6 +123,19 @@ module EasyFlow
121
123
  .merge(drawn_by(node)).merge(named_outputs_by(node))
122
124
  end
123
125
 
126
+ def record_choices_for(node)
127
+ settings = step_type_for(node)&.settings
128
+ settings&.record_fields.to_h.to_h do |list, holds|
129
+ earlier_fields = holds.select { |_name, type| type == :previous_step }.keys
130
+ output_fields = settings.record_outputs_of.fetch(list, {}).keys
131
+ [ list.to_s, earlier_fields.to_h { |name| [ name.to_s, earlier_than(node) ] }.merge(output_fields.to_h { |name| [ name.to_s, outputs_before(node) ] }) ]
132
+ end
133
+ end
134
+
135
+ def outputs_before(node)
136
+ digest.preceding(node.id).to_h { |id| [ id, digest.outputs_of(id) ] }
137
+ end
138
+
124
139
  def naming_flows(node)
125
140
  step_type_for(node)&.settings&.fields.to_h.select { |_name, type| type == :flow }.keys.map(&:to_s)
126
141
  end
@@ -13,10 +13,8 @@ module EasyFlow
13
13
 
14
14
  output :result, type: :boolean, values: [ true, false ]
15
15
 
16
- def route(node, state)
17
- answer = state[node.config["step"]]
18
- answer = answer[node.config["output"]] if answer.is_a?(Hash)
19
- number = Float(answer.to_s, exception: false)
16
+ def route(node, state, times)
17
+ number = Float(CompletedAt.value_of(state, times, *node.config.values_at("step", "output")).to_s, exception: false)
20
18
  return false if number.nil?
21
19
 
22
20
  number.public_send(COMPARISONS.fetch(node.config["comparison"]), node.config["amount"])
@@ -0,0 +1,32 @@
1
+ module EasyFlow
2
+ class CompareTwoValues
3
+ include Step
4
+
5
+ COMPARISONS = { "more than" => :>, "less than" => :<, "at least" => :>=, "at most" => :<=, "equal to" => :== }.freeze
6
+
7
+ step_name "Compare two values"
8
+
9
+ setting :step, type: :previous_step
10
+ setting :output, outputs_of: :step
11
+ setting :comparison, type: :select, options: COMPARISONS.keys, required: true
12
+ setting :other_step, type: :previous_step
13
+ setting :other_output, outputs_of: :other_step
14
+ setting :amount, type: :float
15
+
16
+ output :result, type: :boolean, values: [ true, false ]
17
+
18
+ def route(node, state, times)
19
+ first = number(CompletedAt.value_of(state, times, *node.config.values_at("step", "output")))
20
+ second = node.config["other_step"].present? ? number(CompletedAt.value_of(state, times, *node.config.values_at("other_step", "other_output"))) : 0.0
21
+ return false if first.nil? || second.nil?
22
+
23
+ first.public_send(COMPARISONS.fetch(node.config["comparison"]), second + node.config["amount"].to_f)
24
+ end
25
+
26
+ private
27
+
28
+ def number(value)
29
+ Float(value.to_s, exception: false)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module EasyFlow
2
+ module CompletedAt
3
+ OUTPUTS = {
4
+ "completed_hour" => "Hour completed",
5
+ "completed_weekday" => "Weekday completed",
6
+ "completed_minute" => "Minutes since 1970 when completed"
7
+ }.freeze
8
+
9
+ READERS = {
10
+ "completed_hour" => ->(time) { time.hour },
11
+ "completed_weekday" => ->(time) { time.to_date.cwday },
12
+ "completed_minute" => ->(time) { time.to_i / 60 }
13
+ }.freeze
14
+
15
+ def self.read(output, time)
16
+ READERS.fetch(output).call(time.in_time_zone)
17
+ end
18
+
19
+ def self.value_of(state, times, step, output)
20
+ return times[step] && read(output, times[step]) if OUTPUTS.key?(output)
21
+
22
+ given = state[step]
23
+ given.is_a?(Hash) ? given[output] : given
24
+ end
25
+
26
+ def self.latest(times)
27
+ times.sort_by { |key, _time| key.to_s.split("@").last.to_i }.to_h { |key, time| [ key.to_s.split("@").first, time ] }
28
+ end
29
+
30
+ def self.outputs
31
+ OUTPUTS.map { |value, label| { "value" => value, "label" => label } }
32
+ end
33
+ end
34
+ end
@@ -14,7 +14,9 @@ module EasyFlow
14
14
  names_by { |node| node.config.values_at("step", "comparison", "answer").compact_blank.join(" ").presence }
15
15
 
16
16
  def route(node, state)
17
- matched = state[node.config["step"]] == node.config["answer"]
17
+ answer = state[node.config["step"]]
18
+ answer = answer[node.config["output"]] if answer.is_a?(Hash)
19
+ matched = answer == node.config["answer"]
18
20
 
19
21
  node.config["comparison"] == "is not" ? !matched : matched
20
22
  end
@@ -48,7 +48,8 @@ module EasyFlow
48
48
  named = step(id)
49
49
  return [] unless named
50
50
 
51
- step_type(named)&.outputs.to_a.map { |output| { "value" => output.name.to_s, "label" => output.label } }
51
+ declared = step_type(named)&.outputs_for(named).to_a.map { |output| { "value" => output.name.to_s, "label" => output.label } }
52
+ records_a_value?(named) ? declared + CompletedAt.outputs : declared
52
53
  end
53
54
 
54
55
  def values_out_of(id)
@@ -63,19 +64,19 @@ module EasyFlow
63
64
  step_type(node)&.settings&.requirements_for(node.config).to_a
64
65
  end
65
66
 
66
- def next_step(state)
67
- walk(state).last
67
+ def next_step(state, times = {})
68
+ walk(state, times).last
68
69
  end
69
70
 
70
- def output(state)
71
+ def output(state, times = {})
71
72
  ended = nil
72
- walk(state) { |cursor| ended = cursor }
73
+ walk(state, times) { |cursor| ended = cursor }
73
74
 
74
75
  ended.config["output"].presence if step_type(ended)&.ends_here?
75
76
  end
76
77
 
77
- def state_on_path(state)
78
- state.slice(*walk(state).first)
78
+ def state_on_path(state, times = {})
79
+ state.slice(*walk(state, times).first)
79
80
  end
80
81
 
81
82
  def questions_left(state)
@@ -90,9 +91,10 @@ module EasyFlow
90
91
 
91
92
  private
92
93
 
93
- def walk(state)
94
+ def walk(state, times = {})
94
95
  recorded = []
95
96
  latest = {}
97
+ latest_times = {}
96
98
  visits = Hash.new(0)
97
99
  answers_at_visit = {}
98
100
  answered = 0
@@ -110,14 +112,20 @@ module EasyFlow
110
112
  if state.key?(key)
111
113
  recorded << key
112
114
  latest[cursor.id] = state[key]
115
+ latest_times[cursor.id] = times[key] if times.key?(key)
113
116
  answered += 1 if step_type(cursor)&.awaits_input?
114
117
  end
115
- cursor = successor(cursor, state.merge(latest))
118
+ cursor = successor(cursor, state.merge(latest), latest_times)
116
119
  end
117
120
 
118
121
  [ recorded, nil ]
119
122
  end
120
123
 
124
+ def records_a_value?(node)
125
+ type = step_type(node)
126
+ type.present? && (type.awaits_input? || type.acts? || type.waits?)
127
+ end
128
+
121
129
  def decided?(node, state)
122
130
  return true unless step_type(node)&.routes?
123
131
 
@@ -138,9 +146,9 @@ module EasyFlow
138
146
  step_type(node)&.acts? || false
139
147
  end
140
148
 
141
- def successor(node, state)
149
+ def successor(node, state, times = {})
142
150
  leaving = @document.edges_from(node.id)
143
- port = step_type(node)&.route(node, state)
151
+ port = step_type(node)&.route(node, state, times)
144
152
  taken = port.nil? ? leaving.first : leaving.find { |edge| edge.on.to_s == port.to_s }
145
153
 
146
154
  taken && @document.node(taken.to)
@@ -13,6 +13,10 @@ module EasyFlow
13
13
  @run.recorded
14
14
  end
15
15
 
16
+ def completed_at
17
+ @run.completed_at
18
+ end
19
+
16
20
  def record(id, value)
17
21
  @run.record(id.to_sym, value)
18
22
  end
@@ -1,12 +1,15 @@
1
1
  module EasyFlow
2
2
  module Progress
3
3
  class Loose
4
- def initialize(flow, answers, definition = nil)
4
+ def initialize(flow, answers, definition = nil, completed_at = {})
5
5
  @flow = flow
6
6
  @answers = answers.to_h.symbolize_keys
7
7
  @definition = definition
8
+ @completed_at = completed_at.to_h.symbolize_keys
8
9
  end
9
10
 
11
+ attr_reader :completed_at
12
+
10
13
  def definition
11
14
  @definition || @flow.live_definition
12
15
  end
@@ -17,6 +20,7 @@ module EasyFlow
17
20
 
18
21
  def record(id, value)
19
22
  @answers = @answers.merge(id.to_sym => value)
23
+ @completed_at = @completed_at.merge(id.to_sym => Time.current)
20
24
  end
21
25
 
22
26
  def start_inner(_node); end
@@ -26,13 +30,17 @@ module EasyFlow
26
30
  def finish(state)
27
31
  return unless @flow.on_finish?
28
32
 
29
- Run.start(@flow).tap { |run| run.update!(recorded: state) }
33
+ Run.start(@flow).tap do |run|
34
+ run.update!(recorded: state.to_h { |step_id, value| [ step_id.to_s, { "value" => value, "completed_at" => @completed_at[step_id.to_sym]&.utc&.iso8601 }.compact ] })
35
+ end
30
36
  end
31
37
 
32
38
  def discard_last
33
- last = Runner.new(@flow.live_definition).state_on_path(@answers).keys.last
39
+ last = Runner.new(@flow.live_definition).state_on_path(@answers, times: @completed_at).keys.last
40
+ return unless last
34
41
 
35
- @answers = @answers.except(last) if last
42
+ @answers = @answers.except(last)
43
+ @completed_at = @completed_at.except(last)
36
44
  end
37
45
  end
38
46
  end
@@ -1,9 +1,9 @@
1
1
  module EasyFlow
2
2
  module Progress
3
- def self.for(flow, run: nil, answers: {}, definition: nil)
3
+ def self.for(flow, run: nil, answers: {}, completed_at: {}, definition: nil)
4
4
  return Kept.new(run) if run
5
5
 
6
- Loose.new(flow, answers, definition)
6
+ Loose.new(flow, answers, definition, completed_at)
7
7
  end
8
8
  end
9
9
  end
@@ -15,11 +15,15 @@ module EasyFlow
15
15
  end
16
16
 
17
17
  def record(step_id, value)
18
- update!(recorded: recorded.merge(step_id => value))
18
+ update!(recorded: visits.merge(step_id.to_s => { "value" => value, "completed_at" => Time.current.utc.iso8601 }))
19
19
  end
20
20
 
21
21
  def recorded
22
- super.to_h.symbolize_keys
22
+ visits.to_h { |step_id, visit| [ step_id.to_sym, visit["value"] ] }
23
+ end
24
+
25
+ def completed_at
26
+ visits.select { |_step_id, visit| visit["completed_at"] }.to_h { |step_id, visit| [ step_id.to_sym, Time.zone.parse(visit["completed_at"]) ] }
23
27
  end
24
28
 
25
29
  def advance
@@ -31,11 +35,16 @@ module EasyFlow
31
35
  end
32
36
 
33
37
  def next_step(state)
34
- digest.next_step(state.transform_keys(&:to_s))
38
+ digest.next_step(state.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
35
39
  end
36
40
 
37
41
  def output
38
- digest.output(recorded.transform_keys(&:to_s))
42
+ digest.output(recorded.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
43
+ end
44
+
45
+ def held_until
46
+ stopped_at = next_step(recorded)
47
+ Wait.held_until(digest.step(stopped_at.id), CompletedAt.latest(completed_at)) if stopped_at&.type == "wait"
39
48
  end
40
49
 
41
50
  def waiting_on
@@ -44,7 +53,7 @@ module EasyFlow
44
53
  end
45
54
 
46
55
  def walked(state)
47
- digest.state_on_path(state.transform_keys(&:to_s))
56
+ digest.state_on_path(state.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
48
57
  end
49
58
 
50
59
  def digest
@@ -53,7 +62,7 @@ module EasyFlow
53
62
 
54
63
  def discard_last
55
64
  last = walked(recorded).keys.map(&:to_sym).last
56
- update!(recorded: recorded.except(last)) if last
65
+ update!(recorded: visits.except(last.to_s)) if last
57
66
  end
58
67
 
59
68
  def pinned_steps
@@ -62,6 +71,10 @@ module EasyFlow
62
71
 
63
72
  private
64
73
 
74
+ def visits
75
+ read_attribute(:recorded).to_h
76
+ end
77
+
65
78
  def started_on_a_published_version
66
79
  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
80
  end