easy_flow 0.32.1 → 0.34.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_flow/canvas.js +1 -1
  3. data/app/controllers/concerns/easy_flow/manage/draws_canvas.rb +5 -2
  4. data/app/controllers/easy_flow/flows_controller.rb +3 -0
  5. data/app/controllers/easy_flow/manage/canvas_controller.rb +26 -12
  6. data/app/controllers/easy_flow/manage/flows_controller.rb +1 -0
  7. data/app/javascript/easy_flow/canvas/Canvas.jsx +15 -8
  8. data/app/javascript/easy_flow/canvas/Connector.jsx +5 -5
  9. data/app/javascript/easy_flow/canvas/ConnectorLayer.jsx +3 -3
  10. data/app/javascript/easy_flow/canvas/Panel.jsx +19 -4
  11. data/app/javascript/easy_flow/canvas/StepCard.jsx +15 -4
  12. data/app/javascript/easy_flow/canvas/TypePicker.jsx +2 -2
  13. data/app/javascript/easy_flow/canvas/changes.js +6 -0
  14. data/app/javascript/easy_flow/canvas/flowSender.js +10 -1
  15. data/app/javascript/easy_flow/canvas/useFlow.js +3 -2
  16. data/app/models/easy_flow/canvas.rb +23 -13
  17. data/app/models/easy_flow/change.rb +11 -1
  18. data/app/models/easy_flow/data_step.rb +33 -0
  19. data/app/models/easy_flow/definition.rb +5 -0
  20. data/app/models/easy_flow/document.rb +11 -0
  21. data/app/models/easy_flow/inputs.rb +24 -0
  22. data/app/models/easy_flow/progress/kept.rb +4 -0
  23. data/app/models/easy_flow/progress/loose.rb +4 -0
  24. data/app/models/easy_flow/read_data.rb +11 -0
  25. data/app/models/easy_flow/runner.rb +9 -1
  26. data/app/models/easy_flow/step_type/declaration.rb +5 -1
  27. data/app/models/easy_flow/step_type.rb +3 -2
  28. data/app/models/easy_flow/unconnected_steps.rb +44 -0
  29. data/app/models/easy_flow/write_data.rb +11 -0
  30. data/app/views/easy_flow/flows/refused.html.erb +3 -0
  31. data/app/views/easy_flow/manage/flows/show.html.erb +5 -1
  32. data/lib/easy_flow/engine.rb +2 -0
  33. data/lib/easy_flow/host.rb +17 -3
  34. data/lib/easy_flow/version.rb +1 -1
  35. data/lib/easy_flow.rb +1 -1
  36. data/the_local/agents/easy_flow-develop.md +30 -3
  37. data/the_local/agents/easy_flow-info.md +11 -1
  38. data/the_local/agents/easy_flow-install.md +23 -5
  39. data/the_local/interface.yml +6 -0
  40. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7d99a72a402a80fbee862575e5258e4f4f00d2596146e1937ddf9a29d7ad2fc
4
- data.tar.gz: dce5023f80e0fb23335b811d3918b47c64885263bf0d9fb926f780c2c37459e3
3
+ metadata.gz: 230776aa4cde3ff6b14e622b6556b8fc27c7cc200689fe1760f79c9517419186
4
+ data.tar.gz: 7617ecac9e8bffd31608e407ffb685f21df70311a7693ffb20d923b8abeb7996
5
5
  SHA512:
6
- metadata.gz: 23a3a16314eee051dbc7d0c3820bfea95d435a754a574302692192fde954d8f28e9a4caf32724cd15e9670d90c3cbd080e526c42f761d92f6d7b4ee622c36be4
7
- data.tar.gz: 1553ec0e69ca6e332ba9d4dcff984acdcb399bbdee59b29e484ac37865d8e3365395199f04a8cf5674ace12570baf63cebd13e3775e87cdc056ae0304e2bf23b
6
+ metadata.gz: dbe9e03688e1a7f6bbe0ec3c1b11112342da52a4a346be1711125530eab22a25c70a52c1b036c42c3dd03c583c324ef44799f713691e2161eaa4d5d64d97a58b
7
+ data.tar.gz: 46ae29e0296da8586207576e9db24fa84e02bab9a1ccd63d3fc5d0e7aca769bbcca126096a31cdadfc869372c4feeb543f96707c1914ab045a516f681f621e99
@@ -1 +1 @@
1
- (()=>{var Lt=globalThis.ReactDOM;var{createPortal:Be,createRoot:Q,flushSync:Te,hydrateRoot:Ee,preconnect:Pe,prefetchDNS:De,preinit:$e,preinitModule:Ae,preload:_e,preloadModule:Le,requestFormReset:Ie,unstable_batchedUpdates:Me,useFormState:Oe,useFormStatus:He,version:Fe}=Lt;var R=globalThis.React,e=R,{Children:We,Component:Ve,Fragment:Ue,PureComponent:Ye,StrictMode:Xe,Suspense:Ge,cloneElement:qe,createContext:Je,createElement:Ke,createRef:Ze,forwardRef:Qe,isValidElement:Re,lazy:to,memo:eo,startTransition:oo,useCallback:ro,useContext:no,useDebugValue:so,useDeferredValue:ao,useEffect:D,useId:io,useImperativeHandle:lo,useLayoutEffect:tt,useMemo:$,useReducer:co,useRef:U,useState:b,useSyncExternalStore:po,useTransition:uo}=R;var et=new Map,M=new Map,ot=(t,o)=>et.set(t,o),rt=()=>{M.forEach(t=>t.unmount()),M.clear()},nt=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(n=>{if(M.has(n))return;let r=et.get(n.dataset.reactUi),a=o(n);M.set(n,a),a.render(e.createElement(r,JSON.parse(n.dataset.props)))});var st=(t,o)=>{let n=()=>nt(t,o);t.addEventListener("turbo:load",n),t.addEventListener("turbo:frame-load",n),t.addEventListener("turbo:before-cache",rt),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",n):n()};var N=(...t)=>t.filter(Boolean).join(" ");var It={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},Mt={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},Ot=({variant:t="primary",size:o="md",className:n,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:N("ks-button",It[t],Mt[o],n)},r)},x=Ot;var Ht="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",Ft="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",jt="border border-accent-600 bg-accent-600 text-white",Wt=({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?jt:o?Ft:Ht,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},t||"next"),at=Wt;var Vt={width:240,boxSizing:"border-box",borderRadius:8},Ut={padding:"8px 14px",textAlign:"center",fontWeight:600},Yt={fontWeight:600,lineHeight:1.3},Xt=(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"},Gt=({node:t,selected:o,armed:n,connecting:r,onSelect:a,onOpen:l,onArm:i,onDragEnd:d,onDragStart:m})=>{let c=t.begins_here||t.ends_here,g=t.begins_here,u=t.ends_here?[]:t.ports,y=r&&!g;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!g,onDragStart:p=>{p.dataTransfer.effectAllowed="move",p.dataTransfer.setData("text/plain",t.id),m()},onDragEnd:d,onClick:t.opens&&!r?()=>l(t.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Xt(y,t.violations.length>0,o)}`,style:{...Vt,padding:c?0:"12px 14px",cursor:g?"default":r?"crosshair":"grab"}},c&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:Ut},t.label),!c&&e.createElement("div",{style:Yt},t.label),!c&&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:p=>{p.stopPropagation(),a()}},"Settings"),t.violations.map(p=>e.createElement("div",{key:p.problem+p.detail,className:"text-red-600 dark:text-red-400",style:{fontSize:11,padding:"0 14px 6px"}},p.problem.replace(/_/g," "),p.detail?`: ${p.detail}`:"")),u.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},u.map(p=>e.createElement(at,{key:p,name:p,connected:t.connected.includes(p),connecting:r,armed:n===p,onArm:v=>i(p,v)}))))},Y=Gt;var X=(t,o)=>t.includes(o)?t.filter(n=>n!==o):[...t,o],it=(t,o)=>({...o?.choices||{},...t?.choices||{}});var qt=({as:t="input",disabled:o,className:n,...r})=>e.createElement(t,{...r,disabled:o,className:N("ks-input",o&&"ks-input-disabled",n)}),T=qt;var Jt=t=>e.createElement(T,{...t,as:"select"}),lt=Jt;var Kt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:N("ks-checkbox",t)}),O=Kt;var dt=t=>(t||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),Zt=({type:t,value:o,choices:n,onChange:r,onSettle:a,onPick:l,pictureUrl:i})=>{if(t==="picture")return e.createElement("div",{className:"mb-3"},o&&i&&e.createElement("img",{src:i,alt:"The step's picture",style:{maxWidth:"100%"}}),o&&e.createElement("button",{type:"button","data-remove-picture":!0,className:"ks-button ks-button-secondary ks-button-sm",onClick:()=>a("")},"Remove picture"),e.createElement("input",{type:"file",accept:"image/*",onChange:d=>d.target.files[0]&&l(d.target.files[0])}));if(t==="boolean")return e.createElement(O,{checked:!!o,onChange:d=>a(d.target.checked)});if(t==="select"||t==="previous_step"||t==="from_step"||t==="flow")return e.createElement(lt,{className:"mb-3",value:o??"",onChange:d=>a(d.target.value)},e.createElement("option",{value:""}),dt(n).map(d=>e.createElement("option",{key:d.value,value:d.value},d.label)));if(t==="multi_select"){let d=Array.isArray(o)?o:[],m=c=>a(X(d,c));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(n||[]).map(c=>e.createElement("label",{key:c,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(O,{checked:d.includes(c),onChange:()=>m(c)}),e.createElement("span",null,c))))}if(t==="flows"){let d=Array.isArray(o)?o:[];return e.createElement("div",{style:{display:"flex",flexDirection:"column",gap:4},className:"mb-3"},dt(n).map(m=>e.createElement("label",{key:m.value,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(O,{checked:d.includes(m.value),onChange:()=>a(X(d,m.value))}),e.createElement("span",null,m.label))))}return t==="integer"||t==="float"?e.createElement(T,{className:"mb-3",type:"number",step:t==="integer"?"1":"any",value:o??"",onChange:d=>r(d.target.value),onBlur:d=>a(d.target.value)}):e.createElement(T,{className:"mb-3",type:"text",value:o??"",onChange:d=>r(d.target.value),onBlur:d=>a(d.target.value)})},H=Zt;var ct=(t,o,n,r)=>t.map((a,l)=>l===o?{...a,[n]:r}:a);var Qt=(t,o,n,r)=>{let a=(t||{})[n],l=(o||{})[n];return l?(a||{})[r[l]]:a},Rt=({holds:t,labels:o,rows:n,choices:r,outputsOf:a,onChange:l,onSettle:i})=>{let d=Array.isArray(n)?n:[],m=(c,g,u,y)=>{let p=ct(d,c,g,u);y?i(p):l(p)};return e.createElement("div",{style:{marginBottom:12}},d.map((c,g)=>e.createElement("div",{key:g,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([u,y])=>e.createElement("label",{key:u,style:{display:"block"}},e.createElement("span",{className:"text-gray-500 dark:text-gray-400",style:{display:"block",marginBottom:2,fontSize:11}},(o||{})[u]||u),e.createElement(H,{type:y,value:c[u],choices:Qt(r,a,u,c),onChange:p=>m(g,u,p,!1),onSettle:p=>m(g,u,p,!0)}))),e.createElement(x,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>i(d.filter((u,y)=>y!==g))},"Remove"))),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>i([...d,{}])},"Add"))},pt=Rt;var te=({as:t="div",className:o,children:n,...r})=>e.createElement(t,{...r,className:N("ks-panel",o)},n),_=te;var F=(t,o)=>({className:n,children:r,...a})=>e.createElement(t,{...a,className:N(o,n)},r),I=F("label","ks-label"),rr=F("p","ks-hint"),nr=F("p","ks-error"),sr=F("span","ks-required");var ee={width:280,padding:20,overflowY:"auto"},oe=({node:t,fields:o,holds:n,labels:r,recordLabels:a,recordChoices:l,recordOutputsOf:i,choices:d,onSave:m,onDelete:c,onClose:g,onPickPicture:u,refusal:y})=>{let[p,v]=b(t.config),[k,B]=b(t.picture_url);D(()=>B(t.picture_url),[t.id,t.picture_url]),D(()=>v(t.config),[t.id,t.config]);let S=h=>{v(h),JSON.stringify(h)!==JSON.stringify(t.config)&&m(h)};return e.createElement(_,{as:"aside",style:ee,"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:g,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),y&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{fontWeight:600,marginBottom:12},"data-refusal":!0},y),Object.entries(o).map(([h,z])=>e.createElement(I,{key:h},e.createElement("span",{style:{display:"block",marginBottom:3}},r[h]||h),z==="list"?e.createElement(pt,{holds:n[h]||{},labels:a[h]||{},rows:p[h],choices:{...(l||{})[h],...(t.record_choices||{})[h]},outputsOf:(i||{})[h],onChange:w=>v({...p,[h]:w}),onSettle:w=>S({...p,[h]:w})}):e.createElement(H,{type:z,value:p[h],choices:d[h],pictureUrl:k,onChange:w=>v({...p,[h]:w}),onSettle:w=>S({...p,[h]:w}),onPick:async w=>{let L=await u(w);L&&(B(L.url),S({...p,[h]:L.reference}))}}))),e.createElement(x,{variant:"danger",size:"sm",className:"w-full",onClick:c},"Delete step"))},ut=oe;var re={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},ne="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",se="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",ae=({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?se:ne,style:re},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")),mt=ae;var ie=({entries:t,at:o,onPick:n,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(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")),ft=ie;var le={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},gt=({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),de=({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:le,title:"Add a step",onClick:n},"+"),e.createElement(gt,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(gt,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),yt=de;var ht=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var xt=({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 ce={width:280,padding:20,overflowY:"auto"},G={fontWeight:600,marginBottom:6,marginTop:14},j={fontSize:12,marginBottom:4,lineHeight:1.4},bt={display:"block",marginBottom:3},vt=(t,o,n)=>r=>{r.target.value!==(t[o]??"")&&n({[o]:r.target.value})},pe=({flow:t,changes:o,problems:n,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:d,onClose:m})=>e.createElement(_,{as:"aside",style:ce,"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:m,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},xt(t)),e.createElement("h2",{style:G},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...j,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...j,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(c=>e.createElement("p",{key:`${c.node}-${c.problem}`,className:"text-amber-700 dark:text-amber-400",style:j,"data-problem":!0},"\u26A0 ",ht(c))),e.createElement("h2",{style:G},"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((c,g)=>e.createElement("p",{key:g,style:j,"data-change":!0},c)),e.createElement("h2",{style:G},"Details"),e.createElement(I,null,e.createElement("span",{style:bt},"Title"),e.createElement(T,{className:"mb-3",defaultValue:t.title??"",onBlur:vt(t,"title",d),"data-flow-title":!0})),e.createElement(I,null,e.createElement("span",{style:bt},"Start label"),e.createElement(T,{className:"mb-3",defaultValue:t.start_label??"",onBlur:vt(t,"start_label",d),"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"))),kt=pe;var wt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ue=({link:t,onInsert:o,onRemove:n,onDrop:r,dragging:a})=>{let[l,i]=b(!1),d=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:m=>{m.preventDefault(),m.dataTransfer.dropEffect="move"},onDrop:m=>{m.preventDefault(),i(!1),r()}},e.createElement("div",{style:{display:"flex",gap:4,justifyContent:"center",alignItems:"center",height:"100%",opacity:d?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:wt},"+"),!a&&e.createElement(x,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:wt},"\xD7")))},Ct=ue;var me=({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(Ct,{key:i.id,link:i,dragging:!!n,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),St=me;var Nt="Your change was not saved because the server could not be reached.",zt=({base:t,token:o,fetch:n,onFlow:r,onError:a,onNotice:l})=>async(i,d,m)=>{let c=await n(t+i,{method:d,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:m&&JSON.stringify(m)}).catch(()=>null);if(!c)return l(null),a(Nt);let g=await c.json().catch(()=>({}));if(!c.ok)return l(null),a(g.error||"That change was refused");a(null),l(g.notice||null),r(await(await n(t+".json",{headers:{Accept:"application/json"}})).json())},Bt=({base:t,token:o,fetch:n,onError:r})=>async a=>{let l=new FormData;l.append("picture",a);let i=await n(t+"/pictures",{method:"POST",headers:{"X-CSRF-Token":o},body:l}).catch(()=>null);if(!i)return r(Nt),null;let d=await i.json().catch(()=>({}));return i.ok?(r(null),d):(r(d.error||"That picture was refused"),null)};var fe=(t,o,n)=>{let[r,a]=b(n),[l,i]=b(null),[d,m]=b(null),c=$(()=>zt({base:t,token:o,fetch:(...u)=>window.fetch(...u),onFlow:a,onError:i,onNotice:m}),[t,o]),g=$(()=>Bt({base:t,token:o,fetch:(...u)=>window.fetch(...u),onError:i}),[t,o]);return{flow:r,error:l,notice:d,send:c,pickPicture:g}},Tt=fe;var W=(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}},ge=(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},ye=(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}},he=(t,o,n,r)=>{let[a,l]=b([]),[i,d]=b({width:"100%",height:"100%"});return tt(()=>{let m=()=>{let g=o.current?.getBoundingClientRect();g&&(d({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(u=>{let y=n.current[u.source]?.getBoundingClientRect(),p=n.current[u.target]?.getBoundingClientRect();if(!y||!p)return[];let v=W(y,u.leaves,g,o.current),k=ge(W(p,u.enters,g,o.current),u.enters),B=(W(y,"bottom",g,o.current).y+W(p,"top",g,o.current).y)/2,S=u.leaves==="left"?Math.min(v.x,k.x)-28:Math.max(v.x,k.x)+28;return[ye(u,v,k,B,S)]})))};m(),window.addEventListener("resize",m);let c=new ResizeObserver(m);return o.current&&c.observe(o.current),()=>{window.removeEventListener("resize",m),c.disconnect()}},r),{links:a,extent:i}},Et=he;var Pt=(t,o)=>{let n=new Set(o),r=t,a=2;for(;n.has(r);)r=`${t}_${a++}`;return r};var ve={display:"flex",height:"100%",minHeight:0,fontSize:13},ke={flex:1,overflow:"auto",position:"relative"},we={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},Ce={display:"grid",rowGap:64,columnGap:0,padding:40,justifyContent:"center",position:"relative"},Se={position:"sticky",zIndex:8,top:8,margin:"8px auto 0",width:"fit-content",padding:"6px 12px",borderRadius:6,fontSize:12},Ne=({port:t,onCancel:o})=>e.createElement("div",{className:"bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100",style:Se},"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")),ze=({base:t,token:o,initial:n})=>{let{flow:r,error:a,notice:l,send:i,pickPicture:d}=Tt(t,o,n),[m,c]=b(null),[g,u]=b(null),[y,p]=b(null),[v,k]=b(null),[B,S]=b(!1),h=U({}),z=U(null);D(()=>{let s=()=>{c(null),S(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),D(()=>{if(!B)return;let s=f=>{f.target.closest("[data-builder-panel], [data-open-panel]")||S(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[B]),D(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let w=$(()=>Object.fromEntries(r.nodes.map(s=>[s.id,s])),[r.nodes]),{links:L,extent:$t}=Et(r,z,h,[r,m,w]),q=$(()=>{let s={};return r.violations.forEach(f=>{var C;return(s[C=f.node]||(s[C]=[])).push(f)}),s},[r.violations]),V=r.nodes.filter(s=>!s.loose),J=r.nodes.filter(s=>s.loose),At=Math.max(0,...V.map(s=>s.row))+1,_t=Math.max(0,...V.map(s=>s.column))+1,K=s=>{if(!y)return;let[f,C]=y;p(null),f!==s&&i("/edges","POST",{from:f,to:s,on:C||null})},E=r.nodes.find(s=>s.id===m),P=r.palette.find(s=>s.type===E?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:ve,onMouseUp:()=>k(null),onKeyDown:s=>{s.key==="Escape"&&(c(null),p(null),u(null))},tabIndex:-1},e.createElement("div",{ref:z,className:"bg-surface-50 dark:bg-surface-950",style:ke,onClick:s=>{s.target===z.current&&(c(null),p(null),S(!1))}},y&&e.createElement(Ne,{port:y[1],onCancel:()=>p(null)}),e.createElement(yt,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>u({at:{x:16,y:52}}),onUndo:()=>{c(null),i("/undo","POST")},onRedo:()=>{c(null),i("/redo","POST")}}),e.createElement(St,{links:L,extent:$t,dragging:v,onInsert:s=>u({from:s.source,to:s.target,at:{x:s.midX+30,y:s.midY}}),onRemove:s=>{c(null),i("/edges","DELETE",{from:s.source,to:s.target})},onDrop:s=>{let f=v;k(null),i("/steps/"+f+"/move","PATCH",{from:s.source,to:s.target})}}),J.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:we},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"),J.map(s=>e.createElement("div",{key:s.id,style:{marginBottom:8}},e.createElement(Y,{node:{...s,ref:f=>{h.current[s.id]=f},violations:q[s.id]||[],connected:[]},selected:m===s.id,armed:y&&y[0]===s.id?y[1]:null,connecting:!!y,onSelect:()=>y?K(s.id):c(s.id),onOpen:f=>window.location.assign(f),onArm:(f,C)=>{C.stopPropagation(),p([s.id,f])},onDragStart:()=>k(s.id),onDragEnd:()=>k(null)})))),e.createElement("div",{style:{...Ce,gridTemplateColumns:`repeat(${_t+1}, ${280/2}px)`,gridTemplateRows:`repeat(${At}, auto)`}},V.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(mt,{node:{...s,ref:f=>{h.current[s.id]=f}},dragging:!!v,onFill:()=>u({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let f=v;k(null),f&&i("/edges","POST",{from:s.from,to:f,on:s.on})}}),!s.placeholder&&e.createElement(Y,{node:{...s,ref:f=>{h.current[s.id]=f},violations:q[s.id]||[],connected:r.edges.filter(f=>f.source===s.id).map(f=>f.label||null)},selected:s.id===m,armed:y&&y[0]===s.id?y[1]:null,connecting:!!y&&y[0]!==s.id,onSelect:()=>y?K(s.id):c(s.id),onOpen:f=>window.location.assign(f),onArm:(f,C)=>{let Z=z.current.getBoundingClientRect();u({from:s.id,on:f,at:{x:C.clientX-Z.left+z.current.scrollLeft+8,y:C.clientY-Z.top+z.current.scrollTop+8}})},onDragEnd:()=>k(null),onDragStart:()=>k(s.id)})))),g&&e.createElement(ft,{entries:r.palette,at:g.at,onDismiss:()=>u(null),onConnect:g.on!==void 0?()=>{p([g.from,g.on]),u(null)}:null,onPick:s=>{let f=g;u(null),i("/steps","POST",{id:Pt(s.type,r.nodes.map(C=>C.id)),type:s.type,from:f.from,to:f.to,on:f.on})}})),B&&!E&&e.createElement(kt,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>S(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:s=>i("/details","PATCH",{flow:s})}),E&&e.createElement(ut,{node:E,fields:P?.fields||{},holds:P?.records||{},labels:P?.labels||{},recordLabels:P?.record_labels||{},recordChoices:P?.record_choices||{},recordOutputsOf:P?.record_outputs_of||{},choices:it(E,P),onClose:()=>c(null),onSave:s=>i(`/steps/${E.id}`,"PATCH",{config:s}),onPickPicture:d,refusal:a,onDelete:()=>{c(null),i(`/steps/${E.id}`,"DELETE")}}))},Dt=ze;ot("easy_flow/flow-editor",Dt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});st(document,Q);})();
1
+ (()=>{var je=globalThis.ReactDOM;var{createPortal:Lt,createRoot:Q,flushSync:It,hydrateRoot:Ot,preconnect:Mt,prefetchDNS:Ht,preinit:jt,preinitModule:Ft,preload:Wt,preloadModule:Vt,requestFormReset:Ut,unstable_batchedUpdates:Yt,useFormState:Xt,useFormStatus:Gt,version:qt}=je;var R=globalThis.React,t=R,{Children:Kt,Component:Zt,Fragment:Qt,PureComponent:Rt,StrictMode:eo,Suspense:to,cloneElement:oo,createContext:ro,createElement:no,createRef:so,forwardRef:ao,isValidElement:lo,lazy:io,memo:co,startTransition:po,useCallback:uo,useContext:mo,useDebugValue:fo,useDeferredValue:go,useEffect:$,useId:yo,useImperativeHandle:ho,useLayoutEffect:ee,useMemo:A,useReducer:bo,useRef:U,useState:k,useSyncExternalStore:xo,useTransition:vo}=R;var te=new Map,O=new Map,oe=(e,o)=>te.set(e,o),re=()=>{O.forEach(e=>e.unmount()),O.clear()},ne=(e,o)=>e.querySelectorAll("[data-react-ui]").forEach(n=>{if(O.has(n))return;let r=te.get(n.dataset.reactUi),a=o(n);O.set(n,a),a.render(t.createElement(r,JSON.parse(n.dataset.props)))});var se=(e,o)=>{let n=()=>ne(e,o);e.addEventListener("turbo:load",n),e.addEventListener("turbo:frame-load",n),e.addEventListener("turbo:before-cache",re),e.readyState==="loading"?e.addEventListener("DOMContentLoaded",n):n()};var S=(...e)=>e.filter(Boolean).join(" ");var Fe={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},We={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},Ve=({variant:e="primary",size:o="md",className:n,children:r,...a})=>{let i=a.href?"a":"button";return t.createElement(i,{...a,className:S("ks-button",Fe[e],We[o],n)},r)},v=Ve;var Ue="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",Ye="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",Xe="border border-accent-600 bg-accent-600 text-white",Ge=({name:e,connected:o,armed:n,onArm:r,connecting:a})=>t.createElement("button",{onClick:i=>{a||(i.stopPropagation(),r(i))},title:n?"Now choose a step to connect to":"Connect this branch",className:n?Xe:o?Ye:Ue,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},e||"next"),ae=Ge;var qe={width:240,boxSizing:"border-box",borderRadius:8},Je={padding:"8px 14px",textAlign:"center",fontWeight:600},Ke={fontWeight:600,lineHeight:1.3},Ze=(e,o,n)=>{let r=n?"ring-4 ring-accent-600/15":"shadow-sm";return e||n&&!o?`border-accent-600 ${r}`:o?`border-red-600 ${r}`:"border-gray-300 shadow-sm dark:border-zinc-700"},Qe=(e,o)=>o&&e.label===e.id&&e.type_name||e.label,Re=(e,o,n)=>n?o===e.type_name?null:e.type_name:e.type,et=({node:e,selected:o,armed:n,connecting:r,onSelect:a,onOpen:i,onArm:c,onDragEnd:l,onDragStart:p,hideStepTypeCodes:f})=>{let g=e.begins_here||e.ends_here,d=e.begins_here,h=e.ends_here?[]:e.ports,u=r&&!d,y=Qe(e,f),w=Re(e,y,f);return t.createElement("div",{ref:e.ref,"data-step":e.id,draggable:!d,onDragStart:b=>{b.dataTransfer.effectAllowed="move",b.dataTransfer.setData("text/plain",e.id),p()},onDragEnd:l,onClick:e.opens&&!r?()=>i(e.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Ze(u,e.violations.length>0,o)}`,style:{...qe,padding:g?0:"12px 14px",cursor:d?"default":r?"crosshair":"grab"}},g&&t.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:Je},y),!g&&t.createElement("div",{style:Ke},y),!g&&w&&t.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginTop:2}},w),e.picture_url&&t.createElement("div",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:11,marginTop:2}},"Has a picture"),e.starts_a_flow&&t.createElement("div",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:11,marginTop:2}},"Runs a flow"),e.opens&&t.createElement(v,{variant:"secondary",size:"sm",className:"mt-2",onClick:b=>{b.stopPropagation(),a()}},"Settings"),e.violations.map(b=>t.createElement("div",{key:b.problem+b.detail,className:"text-red-600 dark:text-red-400",style:{fontSize:11,padding:"0 14px 6px"}},b.problem.replace(/_/g," "),b.detail?`: ${b.detail}`:"")),h.length>0&&t.createElement("div",{style:{padding:e.begins_here?"0 14px 8px":"10px 0 0"}},h.map(b=>t.createElement(ae,{key:b,name:b,connected:e.connected.includes(b),connecting:r,armed:n===b,onArm:C=>c(b,C)}))))},Y=et;var X=(e,o)=>e.includes(o)?e.filter(n=>n!==o):[...e,o],le=(e,o)=>({...o?.choices||{},...e?.choices||{}});var tt=({as:e="input",disabled:o,className:n,...r})=>t.createElement(e,{...r,disabled:o,className:S("ks-input",o&&"ks-input-disabled",n)}),_=tt;var ot=e=>t.createElement(_,{...e,as:"select"}),ie=ot;var rt=({className:e,...o})=>t.createElement("input",{type:"checkbox",...o,className:S("ks-checkbox",e)}),M=rt;var de=e=>(e||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),nt=({type:e,value:o,choices:n,onChange:r,onSettle:a,onPick:i,pictureUrl:c})=>{if(e==="picture")return t.createElement("div",{className:"mb-3"},o&&c&&t.createElement("img",{src:c,alt:"The step's picture",style:{maxWidth:"100%"}}),o&&t.createElement("button",{type:"button","data-remove-picture":!0,className:"ks-button ks-button-secondary ks-button-sm",onClick:()=>a("")},"Remove picture"),t.createElement("input",{type:"file",accept:"image/*",onChange:l=>l.target.files[0]&&i(l.target.files[0])}));if(e==="boolean")return t.createElement(M,{checked:!!o,onChange:l=>a(l.target.checked)});if(e==="select"||e==="previous_step"||e==="from_step"||e==="flow")return t.createElement(ie,{className:"mb-3",value:o??"",onChange:l=>a(l.target.value)},t.createElement("option",{value:""}),de(n).map(l=>t.createElement("option",{key:l.value,value:l.value},l.label)));if(e==="multi_select"){let l=Array.isArray(o)?o:[],p=f=>a(X(l,f));return t.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(n||[]).map(f=>t.createElement("label",{key:f,style:{display:"inline-flex",alignItems:"center",gap:4}},t.createElement(M,{checked:l.includes(f),onChange:()=>p(f)}),t.createElement("span",null,f))))}if(e==="flows"){let l=Array.isArray(o)?o:[];return t.createElement("div",{style:{display:"flex",flexDirection:"column",gap:4},className:"mb-3"},de(n).map(p=>t.createElement("label",{key:p.value,style:{display:"inline-flex",alignItems:"center",gap:4}},t.createElement(M,{checked:l.includes(p.value),onChange:()=>a(X(l,p.value))}),t.createElement("span",null,p.label))))}return e==="integer"||e==="float"?t.createElement(_,{className:"mb-3",type:"number",step:e==="integer"?"1":"any",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)}):t.createElement(_,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},H=nt;var ce=(e,o,n,r)=>e.map((a,i)=>i===o?{...a,[n]:r}:a);var st=(e,o,n,r)=>{let a=(e||{})[n],i=(o||{})[n];return i?(a||{})[r[i]]:a},at=({holds:e,labels:o,rows:n,choices:r,outputsOf:a,onChange:i,onSettle:c})=>{let l=Array.isArray(n)?n:[],p=(f,g,d,h)=>{let u=ce(l,f,g,d);h?c(u):i(u)};return t.createElement("div",{style:{marginBottom:12}},l.map((f,g)=>t.createElement("div",{key:g,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(e).map(([d,h])=>t.createElement("label",{key:d,style:{display:"block"}},t.createElement("span",{className:"text-gray-500 dark:text-gray-400",style:{display:"block",marginBottom:2,fontSize:11}},(o||{})[d]||d),t.createElement(H,{type:h,value:f[d],choices:st(r,a,d,f),onChange:u=>p(g,d,u,!1),onSettle:u=>p(g,d,u,!0)}))),t.createElement(v,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>c(l.filter((d,h)=>h!==g))},"Remove"))),t.createElement(v,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>c([...l,{}])},"Add"))},pe=at;var lt=({as:e="div",className:o,children:n,...r})=>t.createElement(e,{...r,className:S("ks-panel",o)},n),L=lt;var j=(e,o)=>({className:n,children:r,...a})=>t.createElement(e,{...a,className:S(o,n)},r),I=j("label","ks-label"),pr=j("p","ks-hint"),ur=j("p","ks-error"),mr=j("span","ks-required");var it={width:280,padding:20,overflowY:"auto"},dt=({node:e,fields:o,holds:n,labels:r,recordLabels:a,recordChoices:i,recordOutputsOf:c,choices:l,onSave:p,onDelete:f,onClose:g,onPickPicture:d,refusal:h})=>{let[u,y]=k(e.config),[w,b]=k(e.picture_url);$(()=>b(e.picture_url),[e.id,e.picture_url]),$(()=>y(e.config),[e.id,e.config]);let C=x=>{y(x),JSON.stringify(x)!==JSON.stringify(e.config)&&p(x)};return t.createElement(L,{as:"aside",style:it,"data-inspector":!0},t.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},t.createElement("h2",{style:{fontWeight:600,marginBottom:2}},e.label),t.createElement("button",{title:"Close",onClick:g,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),t.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:16}},e.id," \xB7 ",e.type),h&&t.createElement("p",{className:"text-red-800 dark:text-red-400",style:{fontWeight:600,marginBottom:12},"data-refusal":!0},h),Object.entries(o).map(([x,T])=>t.createElement(I,{key:x},t.createElement("span",{style:{display:"block",marginBottom:3}},r[x]||x),T==="list"?t.createElement(pe,{holds:n[x]||{},labels:a[x]||{},rows:u[x],choices:{...(i||{})[x],...(e.record_choices||{})[x]},outputsOf:(c||{})[x],onChange:N=>y({...u,[x]:N}),onSettle:N=>C({...u,[x]:N})}):t.createElement(H,{type:T,value:u[x],choices:l[x],pictureUrl:w,onChange:N=>y({...u,[x]:N}),onSettle:N=>C({...u,[x]:N}),onPick:async N=>{let B=await d(N);B&&(b(B.url),C({...u,[x]:B.reference}))}}))),t.createElement(v,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},ue=dt;var ct={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},pt="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",ut="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",mt=({node:e,dragging:o,onFill:n,onDrop:r})=>t.createElement("div",{ref:e.ref,"data-placeholder":e.id,title:`Choose the step \u201C${e.label}\u201D should lead to`,onClick:n,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?ut:pt,style:ct},t.createElement("div",{style:{fontWeight:600}},e.label),t.createElement("div",{style:{fontSize:11}},o?"drop a step here":"leads nowhere yet \u2014 click to choose")),me=mt;var ft=({entries:e,at:o,onPick:n,onConnect:r,onDismiss:a,hideStepTypeCodes:i})=>t.createElement(L,{className:"shadow-lg",style:{position:"absolute",zIndex:9,top:o.y,left:o.x,width:210,padding:8,borderRadius:8}},t.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{margin:"0 0 6px",fontSize:11}},"Add a step"),e.map(c=>t.createElement(v,{key:c.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>n(c)},c.label,!i&&t.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},c.type))),r&&t.createElement(v,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),t.createElement(v,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),fe=ft;var gt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ge=({label:e,title:o,idle:n,enabled:r,onUse:a})=>t.createElement(v,{variant:"secondary",size:"sm",title:r?o:n,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},e),yt=({undoable:e,redoable:o,onAdd:n,onUndo:r,onRedo:a})=>t.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},t.createElement(v,{variant:"secondary",className:"rounded-full",style:gt,title:"Add a step",onClick:n},"+"),t.createElement(ge,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:e,onUse:r}),t.createElement(ge,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),ye=yt;var ht=({type:e="info",title:o,message:n,onDismiss:r,className:a})=>t.createElement("div",{className:S("ks-alert",`ks-alert-${e}`,a),role:"alert"},t.createElement("div",{className:"ks-alert-body"},t.createElement("div",{className:"ks-alert-content"},o&&t.createElement("p",{className:"ks-alert-title"},o),t.createElement("p",{className:o?"ks-alert-message-titled":"ks-alert-message"},n)),r&&t.createElement("button",{type:"button",className:"ks-alert-dismiss","aria-label":"Dismiss",onClick:r},"\xD7"))),he=ht;var be=e=>{let o=e.problem.replace(/_/g," ");return e.node?`\u201C${e.node}\u201D ${o}`:o},xe=e=>`${e.map(n=>`\u201C${n.label}\u201D`).join(", ")} ${e.length===1?"is":"are"} not connected to the flow and will be removed.`;var ve=({version:e,published:o})=>e?o?o===e?`Version ${e} \xB7 live`:`Version ${e} \xB7 visitors still on ${o}`:`Version ${e} \xB7 never published`:"No version yet";var bt={width:280,padding:20,overflowY:"auto"},G={fontWeight:600,marginBottom:6,marginTop:14},F={fontSize:12,marginBottom:4,lineHeight:1.4},ke={display:"block",marginBottom:3},we=(e,o,n)=>r=>{r.target.value!==(e[o]??"")&&n({[o]:r.target.value})},xt=({flow:e,changes:o,problems:n,refusal:r,notice:a,publishNote:i,warning:c,onCreate:l,onPublish:p,onAccept:f,onDecline:g,onSaveDetails:d,onClose:h})=>t.createElement(L,{as:"aside",style:bt,"data-builder-panel":!0},t.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},t.createElement("h2",{style:{fontWeight:600},"data-flow-name":!0},e.title||e.slug),t.createElement("button",{title:"Close",onClick:h,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),t.createElement("a",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:12,display:"block"},href:e.history_url,"data-history":!0},ve(e)),c?t.createElement("div",{style:{marginTop:14},"data-unconnected-warning":!0},t.createElement(he,{type:"warning",className:"mb-1.5",message:xe(c.steps)}),t.createElement(v,{size:"sm",className:"w-full mb-1.5",onClick:f,"data-accept-removal":!0},c.path==="/publish"?"Remove and publish":"Remove and create version"),t.createElement(v,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:g,"data-decline-removal":!0},"Cancel")):t.createElement("div",{style:{marginTop:14}},t.createElement(v,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),t.createElement(v,{size:"sm",className:"w-full mb-1.5",onClick:p,"data-publish":!0},"Publish"),i&&t.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12,lineHeight:1.4},"data-publish-note":!0},i)),t.createElement("h2",{style:G},"Problems"),r&&t.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...F,fontWeight:600},"data-refusal":!0},r),a&&!r&&t.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...F,fontWeight:600},"data-notice":!0},a),n.length===0&&!r?t.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):n.map(u=>t.createElement("p",{key:`${u.node}-${u.problem}`,className:"text-amber-700 dark:text-amber-400",style:F,"data-problem":!0},"\u26A0 ",be(u))),t.createElement("h2",{style:G},"Changes since the last version"),o.length===0?t.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing has changed."):o.map((u,y)=>t.createElement("p",{key:y,style:F,"data-change":!0},u)),t.createElement("h2",{style:G},"Details"),t.createElement(I,null,t.createElement("span",{style:ke},"Title"),t.createElement(_,{className:"mb-3",defaultValue:e.title??"",onBlur:we(e,"title",d),"data-flow-title":!0})),t.createElement(I,null,t.createElement("span",{style:ke},"Start label"),t.createElement(_,{className:"mb-3",defaultValue:e.start_label??"",onBlur:we(e,"start_label",d),"data-flow-start-label":!0})),t.createElement("div",{style:{marginTop:16}},t.createElement(v,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:e.definition_url,"data-definition":!0},"Definition"),t.createElement(v,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:e.details_url,"data-details":!0},"Edit details"))),Ce=xt;var Ne={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},Se=e=>({opacity:e?1:0,transition:"opacity .12s"}),vt=({link:e,onInsert:o,onRemove:n,onDrop:r,dragging:a,alwaysShowInsertButtons:i})=>{let[c,l]=k(!1),p=c||a;return t.createElement("div",{"data-connector":`${e.source}-${e.target}`,style:{position:"absolute",left:e.midX-34,top:e.midY-20,width:68,height:40,zIndex:4},onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),onDragEnter:()=>l(!0),onDragLeave:()=>l(!1),onDragOver:f=>{f.preventDefault(),f.dataTransfer.dropEffect="move"},onDrop:f=>{f.preventDefault(),l(!1),r()}},t.createElement("div",{style:{display:"flex",gap:4,justifyContent:"center",alignItems:"center",height:"100%"}},t.createElement(v,{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&&c),style:{...Ne,...Se(p||i)}},"+"),!a&&t.createElement(v,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:{...Ne,...Se(p)}},"\xD7")))},ze=vt;var kt=({links:e,extent:o,dragging:n,onInsert:r,onRemove:a,onDrop:i,alwaysShowInsertButtons:c})=>t.createElement(t.Fragment,null,t.createElement("svg",{width:o.width,height:o.height,style:{position:"absolute",top:0,left:0,pointerEvents:"none",zIndex:1}},t.createElement("defs",null,t.createElement("marker",{id:"easy_flow-arrow",viewBox:"0 0 10 10",refX:"8",refY:"5",markerWidth:"5",markerHeight:"5",orient:"auto"},t.createElement("path",{d:"M 0 0 L 10 5 L 0 10 z",className:"fill-gray-400 dark:fill-zinc-500"}))),e.map(l=>t.createElement("path",{key:l.id,"data-link":l.id,className:"stroke-gray-400 dark:stroke-zinc-500",fill:"none",strokeWidth:"1.5",markerEnd:"url(#easy_flow-arrow)",d:l.path}))),e.filter(l=>l.label).map(l=>t.createElement("div",{key:`${l.id}-label`,"data-link-label":l.id,className:"bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400",style:{position:"absolute",left:l.midX+(c?2:-10),top:l.midY-18,zIndex:3,fontSize:11,padding:"0 3px"}},l.label)),e.filter(l=>!l.placeholder).map(l=>t.createElement(ze,{key:l.id,link:l,dragging:!!n,alwaysShowInsertButtons:c,onInsert:()=>r(l),onRemove:()=>a(l),onDrop:()=>i(l)}))),Be=kt;var _e="Your change was not saved because the server could not be reached.",Te=({base:e,token:o,fetch:n,onFlow:r,onError:a,onNotice:i,onWarning:c=()=>{}})=>async(l,p,f)=>{let g=await n(e+l,{method:p,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:f&&JSON.stringify(f)}).catch(()=>null);if(!g)return i(null),a(_e);let d=await g.json().catch(()=>({}));if(d.unconnected)return i(null),a(null),c({path:l,steps:d.unconnected}),r(await(await n(e+".json",{headers:{Accept:"application/json"}})).json());if(c(null),!g.ok)return i(null),a(d.error||"That change was refused");a(null),i(d.notice||null),r(await(await n(e+".json",{headers:{Accept:"application/json"}})).json())},Pe=({base:e,token:o,fetch:n,onError:r})=>async a=>{let i=new FormData;i.append("picture",a);let c=await n(e+"/pictures",{method:"POST",headers:{"X-CSRF-Token":o},body:i}).catch(()=>null);if(!c)return r(_e),null;let l=await c.json().catch(()=>({}));return c.ok?(r(null),l):(r(l.error||"That picture was refused"),null)};var wt=(e,o,n)=>{let[r,a]=k(n),[i,c]=k(null),[l,p]=k(null),[f,g]=k(null),d=A(()=>Te({base:e,token:o,fetch:(...u)=>window.fetch(...u),onFlow:a,onError:c,onNotice:p,onWarning:g}),[e,o]),h=A(()=>Pe({base:e,token:o,fetch:(...u)=>window.fetch(...u),onError:c}),[e,o]);return{flow:r,error:i,notice:l,warning:f,setWarning:g,send:d,pickPicture:h}},Ee=wt;var W=(e,o,n,r)=>{let a=e.left-n.left+r.scrollLeft,i=e.top-n.top+r.scrollTop;return o==="left"?{x:a,y:i+e.height/2}:o==="right"?{x:a+e.width,y:i+e.height/2}:o==="top"?{x:a+e.width/2,y:i}:{x:a+e.width/2,y:i+e.height}},Ct=(e,o)=>o==="top"?{x:e.x,y:e.y-3}:o==="left"?{x:e.x-3,y:e.y}:o==="right"?{x:e.x+3,y:e.y}:{x:e.x,y:e.y+3},Nt=(e,o,n,r,a)=>{let i={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}`}[e.route];return{...e,path:i,midX:e.route==="detour"?a:(o.x+n.x)/2,midY:e.route==="lane"?r:(o.y+n.y)/2}},St=(e,o,n,r)=>{let[a,i]=k([]),[c,l]=k({width:"100%",height:"100%"});return ee(()=>{let p=()=>{let g=o.current?.getBoundingClientRect();g&&(l({width:o.current.scrollWidth,height:o.current.scrollHeight}),i(e.edges.flatMap(d=>{let h=n.current[d.source]?.getBoundingClientRect(),u=n.current[d.target]?.getBoundingClientRect();if(!h||!u)return[];let y=W(h,d.leaves,g,o.current),w=Ct(W(u,d.enters,g,o.current),d.enters),b=(W(h,"bottom",g,o.current).y+W(u,"top",g,o.current).y)/2,C=d.leaves==="left"?Math.min(y.x,w.x)-28:Math.max(y.x,w.x)+28;return[Nt(d,y,w,b,C)]})))};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:c}},$e=St;var Ae=(e,o)=>{let n=new Set(o),r=e,a=2;for(;n.has(r);)r=`${e}_${a++}`;return r};var _t={display:"flex",height:"100%",minHeight:0,fontSize:13},Tt={flex:1,overflow:"auto",position:"relative"},Pt={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},Et={display:"grid",rowGap:64,columnGap:0,padding:40,justifyContent:"center",position:"relative"},$t={position:"sticky",zIndex:8,top:8,margin:"8px auto 0",width:"fit-content",padding:"6px 12px",borderRadius:6,fontSize:12},At=({port:e,onCancel:o})=>t.createElement("div",{className:"bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100",style:$t},"Choose the step \u201C",e||"next","\u201D should lead to \u2014 ",t.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")),Dt=({base:e,token:o,initial:n})=>{let{flow:r,error:a,notice:i,warning:c,setWarning:l,send:p,pickPicture:f}=Ee(e,o,n),[g,d]=k(null),[h,u]=k(null),[y,w]=k(null),[b,C]=k(null),[x,T]=k(!1),N=U({}),B=U(null);$(()=>{let s=()=>{d(null),T(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),$(()=>{if(!x){l(null);return}let s=m=>{m.target.isConnected&&!m.target.closest("[data-builder-panel], [data-open-panel]")&&T(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[x]),$(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let Le=A(()=>Object.fromEntries(r.nodes.map(s=>[s.id,s])),[r.nodes]),{links:Ie,extent:Oe}=$e(r,B,N,[r,g,Le]),q=A(()=>{let s={};return r.violations.forEach(m=>{var z;return(s[z=m.node]||(s[z]=[])).push(m)}),s},[r.violations]),V=r.nodes.filter(s=>!s.loose),J=r.nodes.filter(s=>s.loose),Me=Math.max(0,...V.map(s=>s.row))+1,He=Math.max(0,...V.map(s=>s.column))+1,K=s=>{if(!y)return;let[m,z]=y;w(null),m!==s&&p("/edges","POST",{from:m,to:s,on:z||null})},P=r.nodes.find(s=>s.id===g),E=[...r.palette,...r.unoffered||[]].find(s=>s.type===P?.type);return t.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:_t,onMouseUp:()=>C(null),onKeyDown:s=>{s.key==="Escape"&&(d(null),w(null),u(null))},tabIndex:-1},t.createElement("div",{ref:B,className:"bg-surface-50 dark:bg-surface-950",style:Tt,onClick:s=>{s.target===B.current&&(d(null),w(null),T(!1))}},y&&t.createElement(At,{port:y[1],onCancel:()=>w(null)}),t.createElement(ye,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>u({at:{x:16,y:52}}),onUndo:()=>{d(null),p("/undo","POST")},onRedo:()=>{d(null),p("/redo","POST")}}),t.createElement(Be,{links:Ie,extent:Oe,dragging:b,alwaysShowInsertButtons:r.always_show_insert_buttons,onInsert:s=>u({from:s.source,to:s.target,at:{x:s.midX+30,y:s.midY}}),onRemove:s=>{d(null),p("/edges","DELETE",{from:s.source,to:s.target})},onDrop:s=>{let m=b;C(null),p("/steps/"+m+"/move","PATCH",{from:s.source,to:s.target})}}),J.length>0&&t.createElement("div",{"data-loose":!0,className:"border border-dashed border-gray-300 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-900",style:Pt},t.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"),J.map(s=>t.createElement("div",{key:s.id,style:{marginBottom:8}},t.createElement(Y,{node:{...s,ref:m=>{N.current[s.id]=m},violations:q[s.id]||[],connected:[]},selected:g===s.id,armed:y&&y[0]===s.id?y[1]:null,connecting:!!y,onSelect:()=>y?K(s.id):d(s.id),onOpen:m=>window.location.assign(m),onArm:(m,z)=>{z.stopPropagation(),w([s.id,m])},onDragStart:()=>C(s.id),onDragEnd:()=>C(null),hideStepTypeCodes:r.hide_step_type_codes})))),t.createElement("div",{style:{...Et,gridTemplateColumns:`repeat(${He+1}, ${280/2}px)`,gridTemplateRows:`repeat(${Me}, auto)`}},V.map(s=>t.createElement("div",{key:s.id,style:{gridRow:s.row+1,gridColumn:`${s.column+1} / span 2`,zIndex:2}},s.placeholder&&t.createElement(me,{node:{...s,ref:m=>{N.current[s.id]=m}},dragging:!!b,onFill:()=>u({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let m=b;C(null),m&&p("/edges","POST",{from:s.from,to:m,on:s.on})}}),!s.placeholder&&t.createElement(Y,{node:{...s,ref:m=>{N.current[s.id]=m},violations:q[s.id]||[],connected:r.edges.filter(m=>m.source===s.id).map(m=>m.label||null)},selected:s.id===g,armed:y&&y[0]===s.id?y[1]:null,connecting:!!y&&y[0]!==s.id,onSelect:()=>y?K(s.id):d(s.id),onOpen:m=>window.location.assign(m),onArm:(m,z)=>{let Z=B.current.getBoundingClientRect();u({from:s.id,on:m,at:{x:z.clientX-Z.left+B.current.scrollLeft+8,y:z.clientY-Z.top+B.current.scrollTop+8}})},onDragEnd:()=>C(null),onDragStart:()=>C(s.id),hideStepTypeCodes:r.hide_step_type_codes})))),h&&t.createElement(fe,{entries:r.palette,at:h.at,onDismiss:()=>u(null),hideStepTypeCodes:r.hide_step_type_codes,onConnect:h.on!==void 0?()=>{w([h.from,h.on]),u(null)}:null,onPick:s=>{let m=h;u(null),p("/steps","POST",{id:Ae(s.type,r.nodes.map(z=>z.id)),type:s.type,from:m.from,to:m.to,on:m.on})}})),x&&!P&&t.createElement(Ce,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:i,publishNote:r.publish_note,warning:c,onClose:()=>T(!1),onCreate:()=>p("/versions","POST"),onPublish:()=>p("/publish","POST"),onAccept:()=>p(c.path,"POST",{remove_unconnected:c.steps.map(s=>s.id)}),onDecline:()=>l(null),onSaveDetails:s=>p("/details","PATCH",{flow:s})}),P&&t.createElement(ue,{node:P,fields:E?.fields||{},holds:E?.records||{},labels:E?.labels||{},recordLabels:E?.record_labels||{},recordChoices:E?.record_choices||{},recordOutputsOf:E?.record_outputs_of||{},choices:le(P,E),onClose:()=>d(null),onSave:s=>p(`/steps/${P.id}`,"PATCH",{config:s}),onPickPicture:f,refusal:a,onDelete:()=>{d(null),p(`/steps/${P.id}`,"DELETE")}}))},De=Dt;oe("easy_flow/flow-editor",De);document.addEventListener("easy_flow:flow-named",e=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=e.detail)});se(document,Q);})();
@@ -8,7 +8,10 @@ module EasyFlow
8
8
  def canvas_payload(flow)
9
9
  Canvas.new(canvas_document(flow), host: flow_host, flows: hosted_flows.order(:title), flow_path: ->(id) { manage_flow_path(hosted_flows.find_by(id: id) || id, from: flow) }, flow: flow).to_h
10
10
  .merge("undoable" => flow.edit_history.undoable?, "redoable" => flow.edit_history.redoable?,
11
- "changes" => listed_changes(flow), "flow" => flow_details(flow))
11
+ "changes" => listed_changes(flow), "flow" => flow_details(flow),
12
+ "always_show_insert_buttons" => flow_host.always_show_insert_buttons?,
13
+ "hide_step_type_codes" => flow_host.hide_step_type_codes?,
14
+ "publish_note" => flow_host.publish_note)
12
15
  end
13
16
 
14
17
  def canvas_document(flow)
@@ -16,7 +19,7 @@ module EasyFlow
16
19
  end
17
20
 
18
21
  def listed_changes(flow)
19
- flow.changes_since_version.to_a.map { |change| Change.phrase(change) }
22
+ Change.listed(flow.changes_since_version.to_a)
20
23
  end
21
24
 
22
25
  def flow_details(flow)
@@ -17,6 +17,9 @@ module EasyFlow
17
17
  return redirect_to run_location(run.waiting_on) if run&.waiting_on
18
18
 
19
19
  @answers = @progress.recorded
20
+ @unsaved_refusal = @guide.unsaved_refusal(@answers, times: @progress.completed_at) unless @progress.saved?
21
+ return render :refused if @unsaved_refusal
22
+
20
23
  @question = @guide.next_step(@answers, run: run, times: @progress.completed_at)
21
24
  @drawing = @guide.drawing_at(@answers, times: @progress.completed_at)
22
25
  flash.now[:alert] = @refused if @refused
@@ -7,23 +7,21 @@ module EasyFlow
7
7
  end
8
8
 
9
9
  def create
10
- objections = Validator.new(document, flow: flow).violations
11
- return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
10
+ with_unconnected_settled do
11
+ stood_at = flow.current_definition_version&.number
12
+ flow.create_version
12
13
 
13
- stood_at = flow.current_definition_version&.number
14
- flow.create_version
15
-
16
- render json: { notice: created(stood_at) }
14
+ render json: { notice: created(stood_at) }
15
+ end
17
16
  end
18
17
 
19
18
  def publish
20
- objections = Validator.new(document, flow: flow).violations
21
- return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
19
+ with_unconnected_settled do
20
+ ran = flow.live_version&.number
21
+ flow.publish
22
22
 
23
- ran = flow.live_version&.number
24
- flow.publish
25
-
26
- render json: { notice: published(ran) }
23
+ render json: { notice: published(ran) }
24
+ end
27
25
  end
28
26
 
29
27
  def details
@@ -83,6 +81,22 @@ module EasyFlow
83
81
 
84
82
  private
85
83
 
84
+ def with_unconnected_settled
85
+ flow.with_lock do
86
+ unconnected = UnconnectedSteps.new(document, flow: flow)
87
+ objections = unconnected.objections
88
+
89
+ if objections.any?
90
+ render json: { error: refusal(objections) }, status: :unprocessable_entity
91
+ elsif unconnected.ids.any? && !unconnected.accepted?(params[:remove_unconnected])
92
+ render json: { error: refusal(Validator.new(document, flow: flow).violations), unconnected: unconnected.named }, status: :unprocessable_entity
93
+ else
94
+ flow.remove_unconnected(unconnected.ids) if unconnected.ids.any?
95
+ yield
96
+ end
97
+ end
98
+ end
99
+
86
100
  def apply(action = nil, *steps)
87
101
  before = document.to_h
88
102
  flow.edit_history.record(yield(document), action, steps, before)
@@ -23,6 +23,7 @@ module EasyFlow
23
23
  def show
24
24
  @flow = hosted_flows.find_by_public_code!(params[:id])
25
25
  @parent = parent_flow
26
+ @back_link = flow_host.back_link
26
27
  @canvas = canvas_payload(@flow)
27
28
  end
28
29
 
@@ -26,7 +26,7 @@ export const Choosing = ({ port, onCancel }) => (
26
26
  )
27
27
 
28
28
  const Canvas = ({ base, token, initial }) => {
29
- const { flow, error, notice, send, pickPicture } = useFlow(base, token, initial)
29
+ const { flow, error, notice, warning, setWarning, send, pickPicture } = useFlow(base, token, initial)
30
30
  const [ selected, setSelected ] = useState(null)
31
31
  const [ adding, setAdding ] = useState(null)
32
32
  const [ armed, setArmed ] = useState(null)
@@ -42,10 +42,13 @@ const Canvas = ({ base, token, initial }) => {
42
42
  }, [])
43
43
 
44
44
  useEffect(() => {
45
- if (!showing) return
45
+ if (!showing) {
46
+ setWarning(null)
47
+ return
48
+ }
46
49
 
47
50
  const away = (event) => {
48
- if (!event.target.closest("[data-builder-panel], [data-open-panel]")) setShowing(false)
51
+ if (event.target.isConnected && !event.target.closest("[data-builder-panel], [data-open-panel]")) setShowing(false)
49
52
  }
50
53
  document.addEventListener("click", away)
51
54
  return () => document.removeEventListener("click", away)
@@ -78,7 +81,7 @@ const Canvas = ({ base, token, initial }) => {
78
81
  }
79
82
 
80
83
  const selectedNode = flow.nodes.find((node) => node.id === selected)
81
- const entryFor = flow.palette.find((entry) => entry.type === selectedNode?.type)
84
+ const entryFor = [ ...flow.palette, ...(flow.unoffered || []) ].find((entry) => entry.type === selectedNode?.type)
82
85
 
83
86
  return (
84
87
  <div className="text-gray-900 dark:text-gray-100" style={page} onMouseUp={() => setDragging(null)}
@@ -94,7 +97,7 @@ const Canvas = ({ base, token, initial }) => {
94
97
  onUndo={() => { setSelected(null); send("/undo", "POST") }}
95
98
  onRedo={() => { setSelected(null); send("/redo", "POST") }} />
96
99
 
97
- <ConnectorLayer links={links} extent={extent} dragging={dragging}
100
+ <ConnectorLayer links={links} extent={extent} dragging={dragging} alwaysShowInsertButtons={flow.always_show_insert_buttons}
98
101
  onInsert={(link) => setAdding({ from: link.source, to: link.target, at: { x: link.midX + 30, y: link.midY } })}
99
102
  onRemove={(link) => { setSelected(null); send("/edges", "DELETE", { from: link.source, to: link.target }) }}
100
103
  onDrop={(link) => { const held = dragging; setDragging(null); send("/steps/" + held + "/move", "PATCH", { from: link.source, to: link.target }) }} />
@@ -118,7 +121,8 @@ const Canvas = ({ base, token, initial }) => {
118
121
  onOpen={(path) => window.location.assign(path)}
119
122
  onArm={(port, event) => { event.stopPropagation(); setArmed([ node.id, port ]) }}
120
123
  onDragStart={() => setDragging(node.id)}
121
- onDragEnd={() => setDragging(null)} />
124
+ onDragEnd={() => setDragging(null)}
125
+ hideStepTypeCodes={flow.hide_step_type_codes} />
122
126
  </div>
123
127
  ))}
124
128
  </div>
@@ -159,13 +163,14 @@ const Canvas = ({ base, token, initial }) => {
159
163
  }}
160
164
  onDragEnd={() => setDragging(null)}
161
165
  onDragStart={() => setDragging(node.id)}
166
+ hideStepTypeCodes={flow.hide_step_type_codes}
162
167
  />}
163
168
  </div>
164
169
  ))}
165
170
  </div>
166
171
 
167
172
  {adding && (
168
- <TypePicker entries={flow.palette} at={adding.at} onDismiss={() => setAdding(null)}
173
+ <TypePicker entries={flow.palette} at={adding.at} onDismiss={() => setAdding(null)} hideStepTypeCodes={flow.hide_step_type_codes}
169
174
  onConnect={adding.on !== undefined ? () => { setArmed([ adding.from, adding.on ]); setAdding(null) } : null}
170
175
  onPick={(entry) => {
171
176
  const where = adding
@@ -176,10 +181,12 @@ const Canvas = ({ base, token, initial }) => {
176
181
  </div>
177
182
 
178
183
  {showing && !selectedNode && (
179
- <Panel flow={flow.flow || {}} changes={flow.changes || []} problems={flow.violations} refusal={error} notice={notice}
184
+ <Panel flow={flow.flow || {}} changes={flow.changes || []} problems={flow.violations} refusal={error} notice={notice} publishNote={flow.publish_note} warning={warning}
180
185
  onClose={() => setShowing(false)}
181
186
  onCreate={() => send("/versions", "POST")}
182
187
  onPublish={() => send("/publish", "POST")}
188
+ onAccept={() => send(warning.path, "POST", { remove_unconnected: warning.steps.map((step) => step.id) })}
189
+ onDecline={() => setWarning(null)}
183
190
  onSaveDetails={(details) => send("/details", "PATCH", { flow: details })} />
184
191
  )}
185
192
 
@@ -1,8 +1,9 @@
1
1
  import React, { useState } from "react"
2
2
  import Button from "keystone_ui-react/src/Button.jsx"
3
3
  const round = { width: 24, height: 24, padding: 0, fontSize: 15, lineHeight: "15px" }
4
+ const shownWhen = (shown) => ({ opacity: shown ? 1 : 0, transition: "opacity .12s" })
4
5
 
5
- const Connector = ({ link, onInsert, onRemove, onDrop, dragging }) => {
6
+ const Connector = ({ link, onInsert, onRemove, onDrop, dragging, alwaysShowInsertButtons }) => {
6
7
  const [ over, setOver ] = useState(false)
7
8
  const showing = over || dragging
8
9
 
@@ -13,14 +14,13 @@ const Connector = ({ link, onInsert, onRemove, onDrop, dragging }) => {
13
14
  onDragEnter={() => setOver(true)} onDragLeave={() => setOver(false)}
14
15
  onDragOver={(event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move" }}
15
16
  onDrop={(event) => { event.preventDefault(); setOver(false); onDrop() }}>
16
- <div style={{ display: "flex", gap: 4, justifyContent: "center", alignItems: "center", height: "100%",
17
- opacity: showing ? 1 : 0, transition: "opacity .12s" }}>
17
+ <div style={{ display: "flex", gap: 4, justifyContent: "center", alignItems: "center", height: "100%" }}>
18
18
  <Button variant="secondary" title={dragging ? "Move the step here" : "Insert a step here"} onClick={onInsert}
19
19
  className="rounded-full data-[over=true]:ring-2 data-[over=true]:ring-accent-600"
20
- data-over={Boolean(dragging && over)} style={round}>+</Button>
20
+ data-over={Boolean(dragging && over)} style={{ ...round, ...shownWhen(showing || alwaysShowInsertButtons) }}>+</Button>
21
21
  {!dragging && (
22
22
  <Button variant="danger" title="Remove this connection" onClick={onRemove}
23
- className="rounded-full" style={round}>×</Button>
23
+ className="rounded-full" style={{ ...round, ...shownWhen(showing) }}>×</Button>
24
24
  )}
25
25
  </div>
26
26
  </div>
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import Connector from "./Connector"
3
3
 
4
- const ConnectorLayer = ({ links, extent, dragging, onInsert, onRemove, onDrop }) => (
4
+ const ConnectorLayer = ({ links, extent, dragging, onInsert, onRemove, onDrop, alwaysShowInsertButtons }) => (
5
5
  <>
6
6
  <svg width={extent.width} height={extent.height}
7
7
  style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none", zIndex: 1 }}>
@@ -17,12 +17,12 @@ const ConnectorLayer = ({ links, extent, dragging, onInsert, onRemove, onDrop })
17
17
 
18
18
  {links.filter((link) => link.label).map((link) => (
19
19
  <div key={`${link.id}-label`} data-link-label={link.id} className="bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400"
20
- style={{ position: "absolute", left: link.midX - 10, top: link.midY - 18, zIndex: 3,
20
+ style={{ position: "absolute", left: link.midX + (alwaysShowInsertButtons ? 2 : -10), top: link.midY - 18, zIndex: 3,
21
21
  fontSize: 11, padding: "0 3px" }}>{link.label}</div>
22
22
  ))}
23
23
 
24
24
  {links.filter((link) => !link.placeholder).map((link) => (
25
- <Connector key={link.id} link={link} dragging={Boolean(dragging)}
25
+ <Connector key={link.id} link={link} dragging={Boolean(dragging)} alwaysShowInsertButtons={alwaysShowInsertButtons}
26
26
  onInsert={() => onInsert(link)} onRemove={() => onRemove(link)} onDrop={() => onDrop(link)} />
27
27
  ))}
28
28
  </>
@@ -2,8 +2,9 @@ import React from "react"
2
2
  import KeystonePanel from "keystone_ui-react/src/Panel.jsx"
3
3
  import Button from "keystone_ui-react/src/Button.jsx"
4
4
  import Input from "keystone_ui-react/src/Input.jsx"
5
+ import Alert from "keystone_ui-react/src/Alert.jsx"
5
6
  import { Label } from "keystone_ui-react/src/FieldText.jsx"
6
- import { worded } from "./changes"
7
+ import { warned, worded } from "./changes"
7
8
  import { standing } from "./flow"
8
9
 
9
10
  const sheet = { width: 280, padding: 20, overflowY: "auto" }
@@ -15,7 +16,7 @@ const settling = (flow, name, onSaveDetails) => (event) => {
15
16
  if (event.target.value !== (flow[name] ?? "")) onSaveDetails({ [name]: event.target.value })
16
17
  }
17
18
 
18
- const Panel = ({ flow, changes, problems, refusal, notice, onCreate, onPublish, onSaveDetails, onClose }) => (
19
+ const Panel = ({ flow, changes, problems, refusal, notice, publishNote, warning, onCreate, onPublish, onAccept, onDecline, onSaveDetails, onClose }) => (
19
20
  <KeystonePanel as="aside" style={sheet} data-builder-panel>
20
21
  <div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 8 }}>
21
22
  <h2 style={{ fontWeight: 600 }} data-flow-name>{flow.title || flow.slug}</h2>
@@ -24,6 +25,22 @@ const Panel = ({ flow, changes, problems, refusal, notice, onCreate, onPublish,
24
25
  </div>
25
26
  <a className="text-accent-600 dark:text-accent-400" style={{ fontSize: 12, display: "block" }} href={flow.history_url} data-history>{standing(flow)}</a>
26
27
 
28
+ {warning
29
+ ? (
30
+ <div style={{ marginTop: 14 }} data-unconnected-warning>
31
+ <Alert type="warning" className="mb-1.5" message={warned(warning.steps)} />
32
+ <Button size="sm" className="w-full mb-1.5" onClick={onAccept} data-accept-removal>{warning.path === "/publish" ? "Remove and publish" : "Remove and create version"}</Button>
33
+ <Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onDecline} data-decline-removal>Cancel</Button>
34
+ </div>
35
+ )
36
+ : (
37
+ <div style={{ marginTop: 14 }}>
38
+ <Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onCreate} data-create-version>Create version</Button>
39
+ <Button size="sm" className="w-full mb-1.5" onClick={onPublish} data-publish>Publish</Button>
40
+ {publishNote && <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 12, lineHeight: 1.4 }} data-publish-note>{publishNote}</p>}
41
+ </div>
42
+ )}
43
+
27
44
  <h2 style={heading}>Problems</h2>
28
45
  {refusal && <p className="text-red-800 dark:text-red-400" style={{ ...item, fontWeight: 600 }} data-refusal>{refusal}</p>}
29
46
  {notice && !refusal && <p className="text-emerald-800 dark:text-emerald-400" style={{ ...item, fontWeight: 600 }} data-notice>{notice}</p>}
@@ -50,8 +67,6 @@ const Panel = ({ flow, changes, problems, refusal, notice, onCreate, onPublish,
50
67
  </Label>
51
68
 
52
69
  <div style={{ marginTop: 16 }}>
53
- <Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onCreate} data-create-version>Create version</Button>
54
- <Button size="sm" className="w-full mb-1.5" onClick={onPublish} data-publish>Publish</Button>
55
70
  <Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.definition_url} data-definition>Definition</Button>
56
71
  <Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.details_url} data-details>Edit details</Button>
57
72
  </div>
@@ -16,11 +16,21 @@ const edge = (target, troubled, selected) => {
16
16
  return "border-gray-300 shadow-sm dark:border-zinc-700"
17
17
  }
18
18
 
19
- const StepCard = ({ node, selected, armed, connecting, onSelect, onOpen, onArm, onDragEnd, onDragStart }) => {
19
+ const headingOf = (node, hideStepTypeCodes) =>
20
+ hideStepTypeCodes && node.label === node.id ? node.type_name || node.label : node.label
21
+
22
+ const typeLineOf = (node, heading, hideStepTypeCodes) => {
23
+ if (!hideStepTypeCodes) return node.type
24
+ return heading === node.type_name ? null : node.type_name
25
+ }
26
+
27
+ const StepCard = ({ node, selected, armed, connecting, onSelect, onOpen, onArm, onDragEnd, onDragStart, hideStepTypeCodes }) => {
20
28
  const bookend = node.begins_here || node.ends_here
21
29
  const pinned = node.begins_here
22
30
  const ports = node.ends_here ? [] : node.ports
23
31
  const target = connecting && !pinned
32
+ const heading = headingOf(node, hideStepTypeCodes)
33
+ const typeLine = typeLineOf(node, heading, hideStepTypeCodes)
24
34
 
25
35
  return (
26
36
  <div ref={node.ref}
@@ -35,9 +45,10 @@ const StepCard = ({ node, selected, armed, connecting, onSelect, onOpen, onArm,
35
45
  padding: bookend ? 0 : "12px 14px",
36
46
  cursor: pinned ? "default" : connecting ? "crosshair" : "grab"
37
47
  }}>
38
- {bookend && <div className="text-gray-500 dark:text-gray-400" style={bookendCard}>{node.label}</div>}
39
- {!bookend && <div style={named}>{node.label}</div>}
40
- {!bookend && <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginTop: 2 }}>{node.type}</div>}
48
+ {bookend && <div className="text-gray-500 dark:text-gray-400" style={bookendCard}>{heading}</div>}
49
+ {!bookend && <div style={named}>{heading}</div>}
50
+ {!bookend && typeLine && <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginTop: 2 }}>{typeLine}</div>}
51
+ {node.picture_url && <div className="text-accent-600 dark:text-accent-400" style={{ fontSize: 11, marginTop: 2 }}>Has a picture</div>}
41
52
  {node.starts_a_flow && <div className="text-accent-600 dark:text-accent-400" style={{ fontSize: 11, marginTop: 2 }}>Runs a flow</div>}
42
53
  {node.opens && <Button variant="secondary" size="sm" className="mt-2" onClick={(event) => { event.stopPropagation(); onSelect() }}>Settings</Button>}
43
54
  {node.violations.map((violation) => (
@@ -2,13 +2,13 @@ import React from "react"
2
2
  import Panel from "keystone_ui-react/src/Panel.jsx"
3
3
  import Button from "keystone_ui-react/src/Button.jsx"
4
4
 
5
- const TypePicker = ({ entries, at, onPick, onConnect, onDismiss }) => (
5
+ const TypePicker = ({ entries, at, onPick, onConnect, onDismiss, hideStepTypeCodes }) => (
6
6
  <Panel className="shadow-lg" style={{ position: "absolute", zIndex: 9, top: at.y, left: at.x, width: 210, padding: 8, borderRadius: 8 }}>
7
7
  <p className="text-gray-500 dark:text-gray-400" style={{ margin: "0 0 6px", fontSize: 11 }}>Add a step</p>
8
8
  {entries.map((entry) => (
9
9
  <Button key={entry.type} variant="secondary" size="sm" className="w-full mb-1.5 flex-col" onClick={() => onPick(entry)}>
10
10
  {entry.label}
11
- <span className="opacity-75" style={{ display: "block", fontSize: 11 }}>{entry.type}</span>
11
+ {!hideStepTypeCodes && <span className="opacity-75" style={{ display: "block", fontSize: 11 }}>{entry.type}</span>}
12
12
  </Button>
13
13
  ))}
14
14
  {onConnect && (
@@ -3,3 +3,9 @@ export const worded = (violation) => {
3
3
 
4
4
  return violation.node ? `“${violation.node}” ${problem}` : problem
5
5
  }
6
+
7
+ export const warned = (steps) => {
8
+ const named = steps.map((step) => `“${step.label}”`).join(", ")
9
+
10
+ return `${named} ${steps.length === 1 ? "is" : "are"} not connected to the flow and will be removed.`
11
+ }
@@ -1,6 +1,6 @@
1
1
  const UNREACHABLE = "Your change was not saved because the server could not be reached."
2
2
 
3
- export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice }) => async (path, method, body) => {
3
+ export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice, onWarning = () => {} }) => async (path, method, body) => {
4
4
  const response = await fetch(base + path, {
5
5
  method, headers: { "Content-Type": "application/json", "X-CSRF-Token": token }, body: body && JSON.stringify(body)
6
6
  }).catch(() => null)
@@ -11,6 +11,15 @@ export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice
11
11
 
12
12
  const answered = await response.json().catch(() => ({}))
13
13
 
14
+ if (answered.unconnected) {
15
+ onNotice(null)
16
+ onError(null)
17
+ onWarning({ path, steps: answered.unconnected })
18
+ return onFlow(await (await fetch(base + ".json", { headers: { Accept: "application/json" } })).json())
19
+ }
20
+
21
+ onWarning(null)
22
+
14
23
  if (!response.ok) {
15
24
  onNotice(null)
16
25
  return onError(answered.error || "That change was refused")
@@ -5,9 +5,10 @@ const useFlow = (base, token, initial) => {
5
5
  const [ flow, setFlow ] = useState(initial)
6
6
  const [ error, setError ] = useState(null)
7
7
  const [ notice, setNotice ] = useState(null)
8
+ const [ warning, setWarning ] = useState(null)
8
9
 
9
10
  const send = useMemo(
10
- () => createFlowSender({ base, token, fetch: (...request) => window.fetch(...request), onFlow: setFlow, onError: setError, onNotice: setNotice }),
11
+ () => createFlowSender({ base, token, fetch: (...request) => window.fetch(...request), onFlow: setFlow, onError: setError, onNotice: setNotice, onWarning: setWarning }),
11
12
  [ base, token ]
12
13
  )
13
14
 
@@ -16,7 +17,7 @@ const useFlow = (base, token, initial) => {
16
17
  [ base, token ]
17
18
  )
18
19
 
19
- return { flow, error, notice, send, pickPicture }
20
+ return { flow, error, notice, warning, setWarning, send, pickPicture }
20
21
  }
21
22
 
22
23
  export default useFlow
@@ -10,7 +10,7 @@ module EasyFlow
10
10
  end
11
11
 
12
12
  def to_h
13
- { "nodes" => nodes, "edges" => edges, "palette" => palette, "violations" => violations }
13
+ { "nodes" => nodes, "edges" => edges, "palette" => palette, "unoffered" => unoffered, "violations" => violations }
14
14
  end
15
15
 
16
16
  private
@@ -20,7 +20,7 @@ module EasyFlow
20
20
  end
21
21
 
22
22
  def drawn_step(node)
23
- { "id" => node.id, "type" => node.type, "label" => label_for(node),
23
+ { "id" => node.id, "type" => node.type, "type_name" => step_type_for(node)&.step_name, "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
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, "picture_url" => picture_url(node), **placed[node.id] }
26
26
  end
@@ -89,17 +89,27 @@ module EasyFlow
89
89
  end
90
90
 
91
91
  def palette
92
- @registry.step_types.reject(&:begins_here?).select { |step_type| @host.nil? || step_type.ends_here? || @host.offers?(step_type.id) }.map do |step_type|
93
- { "type" => step_type.id.to_s, "label" => step_type.step_name,
94
- "fields" => step_type.settings.fields.transform_keys(&:to_s).transform_values(&:to_s),
95
- "labels" => step_type.settings.labels.transform_keys(&:to_s),
96
- "choices" => step_type.settings.choices.transform_keys(&:to_s),
97
- "records" => holdings_of(step_type),
98
- "record_choices" => step_type.settings.record_choices.to_h { |name, offered| [ name.to_s, offered.transform_keys(&:to_s) ] },
99
- "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 ] } ] },
100
- "record_labels" => step_type.settings.record_labels.to_h { |name, held| [ name.to_s, held.transform_keys(&:to_s) ] },
101
- "awaits_input" => step_type.awaits_input? }
102
- end
92
+ offered_step_types.map { |step_type| palette_entry(step_type) }
93
+ end
94
+
95
+ def unoffered
96
+ (@document.nodes.filter_map { |node| step_type_for(node) }.uniq.reject(&:begins_here?) - offered_step_types).map { |step_type| palette_entry(step_type) }
97
+ end
98
+
99
+ def offered_step_types
100
+ @registry.step_types.reject(&:begins_here?).select { |step_type| @host.nil? || step_type.ends_here? || @host.offers?(step_type.id) }
101
+ end
102
+
103
+ def palette_entry(step_type)
104
+ { "type" => step_type.id.to_s, "label" => step_type.step_name,
105
+ "fields" => step_type.settings.fields.transform_keys(&:to_s).transform_values(&:to_s),
106
+ "labels" => step_type.settings.labels.transform_keys(&:to_s),
107
+ "choices" => step_type.settings.choices.transform_keys(&:to_s),
108
+ "records" => holdings_of(step_type),
109
+ "record_choices" => step_type.settings.record_choices.to_h { |name, offered| [ name.to_s, offered.transform_keys(&:to_s) ] },
110
+ "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 ] } ] },
111
+ "record_labels" => step_type.settings.record_labels.to_h { |name, held| [ name.to_s, held.transform_keys(&:to_s) ] },
112
+ "awaits_input" => step_type.awaits_input? }
103
113
  end
104
114
 
105
115
  def holdings_of(step_type)