easy_flow 0.17.0 → 0.19.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/flows_controller.rb +18 -4
- data/app/javascript/easy_flow/canvas/Canvas.jsx +1 -0
- data/app/javascript/easy_flow/canvas/Inspector.jsx +3 -1
- data/app/javascript/easy_flow/canvas/Records.jsx +8 -2
- data/app/models/easy_flow/all_or_any.rb +33 -0
- data/app/models/easy_flow/canvas.rb +17 -2
- data/app/models/easy_flow/compare.rb +2 -4
- data/app/models/easy_flow/compare_two_values.rb +32 -0
- data/app/models/easy_flow/completed_at.rb +34 -0
- data/app/models/easy_flow/condition.rb +3 -1
- data/app/models/easy_flow/digest.rb +31 -12
- data/app/models/easy_flow/progress/kept.rb +4 -0
- data/app/models/easy_flow/progress/loose.rb +12 -4
- data/app/models/easy_flow/progress.rb +2 -2
- data/app/models/easy_flow/run.rb +27 -6
- data/app/models/easy_flow/runner.rb +11 -11
- data/app/models/easy_flow/settings.rb +5 -2
- data/app/models/easy_flow/step.rb +6 -2
- data/app/models/easy_flow/step_type/declaration.rb +13 -3
- data/app/models/easy_flow/step_type.rb +18 -5
- data/app/models/easy_flow/validator.rb +12 -1
- data/app/models/easy_flow/wait.rb +38 -0
- data/app/views/easy_flow/flows/step.html.erb +6 -0
- data/db/migrate/20261011130000_keep_step_visits_whole_in_easy_flow_runs.rb +23 -0
- data/lib/easy_flow/engine.rb +3 -0
- data/lib/easy_flow/version.rb +1 -1
- data/the_local/agents/easy_flow-develop.md +104 -21
- data/the_local/agents/easy_flow-info.md +24 -11
- data/the_local/agents/easy_flow-install.md +19 -6
- data/the_local/interface.yml +5 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e277531505d578e7a66331a93c7af61f08d0558bed385af31d173ce500fadf1
|
|
4
|
+
data.tar.gz: 2a9b83f04a28d3831ae1786766849adb73a44b91d828d6e044d751fd223d1ebf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4df662a23036aa6b8d70253e980b096dca08f4fad0359cc1b6fc14c31328b70efd77e321c1bb1f7d19476dc215d50ed37157c0d5749893901957c3905bc362f
|
|
7
|
+
data.tar.gz: f36528a6ea1f1b75c2d76386bf55af3ab1548468590aeb03a0a130f0407f89cf7539e86d170a4bb7ed522a99c9ff120ef93d1b0f00070ffb85f99c6edf8489cc
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)&.
|
|
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,30 @@ 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)
|
|
67
|
+
def next_step(state, times = {})
|
|
68
|
+
walk(state, times)[1]
|
|
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
|
|
78
|
-
|
|
78
|
+
def ended_at_end?(state, times = {})
|
|
79
|
+
ended = nil
|
|
80
|
+
_recorded, waiting, = walk(state, times) { |cursor| ended = cursor }
|
|
81
|
+
|
|
82
|
+
waiting.nil? && step_type(ended)&.ends_here? || false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def stopped_on_loop(state, times = {})
|
|
86
|
+
walk(state, times)[2]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def state_on_path(state, times = {})
|
|
90
|
+
state.slice(*walk(state, times).first)
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def questions_left(state)
|
|
@@ -90,16 +102,17 @@ module EasyFlow
|
|
|
90
102
|
|
|
91
103
|
private
|
|
92
104
|
|
|
93
|
-
def walk(state)
|
|
105
|
+
def walk(state, times = {})
|
|
94
106
|
recorded = []
|
|
95
107
|
latest = {}
|
|
108
|
+
latest_times = {}
|
|
96
109
|
visits = Hash.new(0)
|
|
97
110
|
answers_at_visit = {}
|
|
98
111
|
answered = 0
|
|
99
112
|
cursor = entry
|
|
100
113
|
|
|
101
114
|
while cursor
|
|
102
|
-
return [ recorded, nil ] if visits[cursor.id].positive? && answers_at_visit[cursor.id] == answered
|
|
115
|
+
return [ recorded, nil, cursor.id ] if visits[cursor.id].positive? && answers_at_visit[cursor.id] == answered
|
|
103
116
|
|
|
104
117
|
yield cursor if block_given?
|
|
105
118
|
visits[cursor.id] += 1
|
|
@@ -110,14 +123,20 @@ module EasyFlow
|
|
|
110
123
|
if state.key?(key)
|
|
111
124
|
recorded << key
|
|
112
125
|
latest[cursor.id] = state[key]
|
|
126
|
+
latest_times[cursor.id] = times[key] if times.key?(key)
|
|
113
127
|
answered += 1 if step_type(cursor)&.awaits_input?
|
|
114
128
|
end
|
|
115
|
-
cursor = successor(cursor, state.merge(latest))
|
|
129
|
+
cursor = successor(cursor, state.merge(latest), latest_times)
|
|
116
130
|
end
|
|
117
131
|
|
|
118
132
|
[ recorded, nil ]
|
|
119
133
|
end
|
|
120
134
|
|
|
135
|
+
def records_a_value?(node)
|
|
136
|
+
type = step_type(node)
|
|
137
|
+
type.present? && (type.awaits_input? || type.acts? || type.waits?)
|
|
138
|
+
end
|
|
139
|
+
|
|
121
140
|
def decided?(node, state)
|
|
122
141
|
return true unless step_type(node)&.routes?
|
|
123
142
|
|
|
@@ -138,9 +157,9 @@ module EasyFlow
|
|
|
138
157
|
step_type(node)&.acts? || false
|
|
139
158
|
end
|
|
140
159
|
|
|
141
|
-
def successor(node, state)
|
|
160
|
+
def successor(node, state, times = {})
|
|
142
161
|
leaving = @document.edges_from(node.id)
|
|
143
|
-
port = step_type(node)&.route(node, state)
|
|
162
|
+
port = step_type(node)&.route(node, state, times)
|
|
144
163
|
taken = port.nil? ? leaving.first : leaving.find { |edge| edge.on.to_s == port.to_s }
|
|
145
164
|
|
|
146
165
|
taken && @document.node(taken.to)
|
|
@@ -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
|
|
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)
|
|
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
|