easy_flow 0.28.0 → 0.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c3f5410fc5f056129c38c8538226461dd426e932bf267d6116bc6c07ad872d1
4
- data.tar.gz: 19221f96eb57398266925771014571f69aeb155bbfab9a3bb55b45dc2401175c
3
+ metadata.gz: 716b3bb1b37acda25c8915e40725db11b6bd9734dca76165aa22edebcb355263
4
+ data.tar.gz: 29f457f14f8f03d4cd9f7d60ff50cde7ca803501ec87fba000badc41cbf196a8
5
5
  SHA512:
6
- metadata.gz: 9786ee8c6b473f189f062870851305bc16ec2031586ea6eb4833cc792558a0efd3b5aa88e4da23ba913fba30e73dfa7c84e3d25ce6132ead745712e9c1cc1c46
7
- data.tar.gz: 89d01cd446c018ae52a86932c8235c77f49ca92de1e65aef00570b9b81511e604a7bcb569df50ce618976a66fcb2a624e3209ac397e17e8920980de00be118f2
6
+ metadata.gz: ed172bae87ce450d5678893e61aead1391b19f30848d20cbc6a6a6311a7fd9039f03e1cf5275f66f766ea4445350c925dcbb312cb8cc7f1e09b57c3ce2550165
7
+ data.tar.gz: 3261c170b1c631272d44c00d2f5557bc71c4949397939a7f9d7e8ec62472a8171b01f854644f0b55885623483750592fda3ca502c8a4c3d7c3fb6d27271a5b1e
@@ -1 +1 @@
1
- (()=>{var Dt=globalThis.ReactDOM;var{createPortal:Ne,createRoot:J,flushSync:ze,hydrateRoot:Be,preconnect:Ee,prefetchDNS:Te,preinit:$e,preinitModule:Ae,preload:De,preloadModule:Pe,requestFormReset:_e,unstable_batchedUpdates:Le,useFormState:Ie,useFormStatus:Me,version:Oe}=Dt;var K=globalThis.React,e=K,{Children:Fe,Component:je,Fragment:Ve,PureComponent:We,StrictMode:Ye,Suspense:Ue,cloneElement:Xe,createContext:Ge,createElement:qe,createRef:Je,forwardRef:Ke,isValidElement:Ze,lazy:Qe,memo:Re,startTransition:to,useCallback:eo,useContext:oo,useDebugValue:ro,useDeferredValue:no,useEffect:T,useId:so,useImperativeHandle:ao,useLayoutEffect:Z,useMemo:D,useReducer:lo,useRef:j,useState:b,useSyncExternalStore:io,useTransition:co}=K;var Q=new Map,_=new Map,R=(t,o)=>Q.set(t,o),tt=()=>{_.forEach(t=>t.unmount()),_.clear()},et=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(n=>{if(_.has(n))return;let r=Q.get(n.dataset.reactUi),a=o(n);_.set(n,a),a.render(e.createElement(r,JSON.parse(n.dataset.props)))});var ot=(t,o)=>{let n=()=>et(t,o);t.addEventListener("turbo:load",n),t.addEventListener("turbo:frame-load",n),t.addEventListener("turbo:before-cache",tt),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",n):n()};var C=(...t)=>t.filter(Boolean).join(" ");var Pt={primary:"ks-button-primary",secondary:"ks-button-secondary",danger:"ks-button-danger"},_t={sm:"ks-button-sm",md:"ks-button-md",lg:"ks-button-lg"},Lt=({variant:t="primary",size:o="md",className:n,children:r,...a})=>{let l=a.href?"a":"button";return e.createElement(l,{...a,className:C("ks-button",Pt[t],_t[o],n)},r)},x=Lt;var It="border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400",Mt="border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400",Ot="border border-accent-600 bg-accent-600 text-white",Ht=({name:t,connected:o,armed:n,onArm:r,connecting:a})=>e.createElement("button",{onClick:l=>{a||(l.stopPropagation(),r(l))},title:n?"Now choose a step to connect to":"Connect this branch",className:n?Ot:o?Mt:It,style:{padding:"1px 8px",marginRight:4,borderRadius:999,fontSize:11,cursor:"pointer"}},t||"next"),rt=Ht;var Ft={width:240,boxSizing:"border-box",borderRadius:8},jt={padding:"8px 14px",textAlign:"center",fontWeight:600},Vt={fontWeight:600,lineHeight:1.3},Wt=(t,o,n)=>{let r=n?"ring-4 ring-accent-600/15":"shadow-sm";return t||n&&!o?`border-accent-600 ${r}`:o?`border-red-600 ${r}`:"border-gray-300 shadow-sm dark:border-zinc-700"},Yt=({node:t,selected:o,armed:n,connecting:r,onSelect:a,onOpen:l,onArm:i,onDragEnd:y,onDragStart:p})=>{let f=t.begins_here||t.ends_here,m=t.begins_here,d=t.ends_here?[]:t.ports,h=r&&!m;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!m,onDragStart:c=>{c.dataTransfer.effectAllowed="move",c.dataTransfer.setData("text/plain",t.id),p()},onDragEnd:y,onClick:t.opens&&!r?()=>l(t.opens):a,className:`bg-white border-2 dark:bg-zinc-900 ${Wt(h,t.violations.length>0,o)}`,style:{...Ft,padding:f?0:"12px 14px",cursor:m?"default":r?"crosshair":"grab"}},f&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:jt},t.label),!f&&e.createElement("div",{style:Vt},t.label),!f&&e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginTop:2}},t.type),t.starts_a_flow&&e.createElement("div",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:11,marginTop:2}},"Runs a flow"),t.opens&&e.createElement(x,{variant:"secondary",size:"sm",className:"mt-2",onClick:c=>{c.stopPropagation(),a()}},"Settings"),t.violations.map(c=>e.createElement("div",{key:c.problem+c.detail,className:"text-red-600 dark:text-red-400",style:{fontSize:11,padding:"0 14px 6px"}},c.problem.replace(/_/g," "),c.detail?`: ${c.detail}`:"")),d.length>0&&e.createElement("div",{style:{padding:t.begins_here?"0 14px 8px":"10px 0 0"}},d.map(c=>e.createElement(rt,{key:c,name:c,connected:t.connected.includes(c),connecting:r,armed:n===c,onArm:g=>i(c,g)}))))},V=Yt;var W=(t,o)=>t.includes(o)?t.filter(n=>n!==o):[...t,o],nt=(t,o)=>({...o?.choices||{},...t?.choices||{}});var Ut=({as:t="input",disabled:o,className:n,...r})=>e.createElement(t,{...r,disabled:o,className:C("ks-input",o&&"ks-input-disabled",n)}),S=Ut;var Xt=t=>e.createElement(S,{...t,as:"select"}),st=Xt;var Gt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:C("ks-checkbox",t)}),L=Gt;var at=t=>(t||[]).map(o=>typeof o=="object"?o:{value:o,label:o}),qt=({type:t,value:o,choices:n,onChange:r,onSettle:a})=>{if(t==="boolean")return e.createElement(L,{checked:!!o,onChange:l=>a(l.target.checked)});if(t==="select"||t==="previous_step"||t==="from_step"||t==="flow")return e.createElement(st,{className:"mb-3",value:o??"",onChange:l=>a(l.target.value)},e.createElement("option",{value:""}),at(n).map(l=>e.createElement("option",{key:l.value,value:l.value},l.label)));if(t==="multi_select"){let l=Array.isArray(o)?o:[],i=y=>a(W(l,y));return e.createElement("div",{style:{display:"flex",flexWrap:"wrap",gap:8}},(n||[]).map(y=>e.createElement("label",{key:y,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(L,{checked:l.includes(y),onChange:()=>i(y)}),e.createElement("span",null,y))))}if(t==="flows"){let l=Array.isArray(o)?o:[];return e.createElement("div",{style:{display:"flex",flexDirection:"column",gap:4},className:"mb-3"},at(n).map(i=>e.createElement("label",{key:i.value,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(L,{checked:l.includes(i.value),onChange:()=>a(W(l,i.value))}),e.createElement("span",null,i.label))))}return t==="integer"||t==="float"?e.createElement(S,{className:"mb-3",type:"number",step:t==="integer"?"1":"any",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)}):e.createElement(S,{className:"mb-3",type:"text",value:o??"",onChange:l=>r(l.target.value),onBlur:l=>a(l.target.value)})},I=qt;var lt=(t,o,n,r)=>t.map((a,l)=>l===o?{...a,[n]:r}:a);var Jt=(t,o,n,r)=>{let a=(t||{})[n],l=(o||{})[n];return l?(a||{})[r[l]]:a},Kt=({holds:t,labels:o,rows:n,choices:r,outputsOf:a,onChange:l,onSettle:i})=>{let y=Array.isArray(n)?n:[],p=(f,m,d,h)=>{let c=lt(y,f,m,d);h?i(c):l(c)};return e.createElement("div",{style:{marginBottom:12}},y.map((f,m)=>e.createElement("div",{key:m,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([d,h])=>e.createElement("label",{key:d,style:{display:"block"}},e.createElement("span",{className:"text-gray-500 dark:text-gray-400",style:{display:"block",marginBottom:2,fontSize:11}},(o||{})[d]||d),e.createElement(I,{type:h,value:f[d],choices:Jt(r,a,d,f),onChange:c=>p(m,d,c,!1),onSettle:c=>p(m,d,c,!0)}))),e.createElement(x,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>i(y.filter((d,h)=>h!==m))},"Remove"))),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>i([...y,{}])},"Add"))},it=Kt;var Zt=({as:t="div",className:o,children:n,...r})=>e.createElement(t,{...r,className:C("ks-panel",o)},n),A=Zt;var M=(t,o)=>({className:n,children:r,...a})=>e.createElement(t,{...a,className:C(o,n)},r),P=M("label","ks-label"),er=M("p","ks-hint"),or=M("p","ks-error"),rr=M("span","ks-required");var Qt={width:280,padding:20,overflowY:"auto"},Rt=({node:t,fields:o,holds:n,labels:r,recordLabels:a,recordChoices:l,recordOutputsOf:i,choices:y,onSave:p,onDelete:f,onClose:m})=>{let[d,h]=b(t.config);T(()=>h(t.config),[t.id,t.config]);let c=g=>{h(g),JSON.stringify(g)!==JSON.stringify(t.config)&&p(g)};return e.createElement(A,{as:"aside",style:Qt,"data-inspector":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600,marginBottom:2}},t.label),e.createElement("button",{title:"Close",onClick:m,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:16}},t.id," \xB7 ",t.type),Object.entries(o).map(([g,w])=>e.createElement(P,{key:g},e.createElement("span",{style:{display:"block",marginBottom:3}},r[g]||g),w==="list"?e.createElement(it,{holds:n[g]||{},labels:a[g]||{},rows:d[g],choices:{...(l||{})[g],...(t.record_choices||{})[g]},outputsOf:(i||{})[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}):e.createElement(I,{type:w,value:d[g],choices:y[g],onChange:v=>h({...d,[g]:v}),onSettle:v=>c({...d,[g]:v})}))),e.createElement(x,{variant:"danger",size:"sm",className:"w-full",onClick:f},"Delete step"))},dt=Rt;var te={width:240,boxSizing:"border-box",padding:"10px 14px",borderRadius:8,textAlign:"center",fontSize:12,cursor:"pointer"},ee="border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",oe="border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200",re=({node:t,dragging:o,onFill:n,onDrop:r})=>e.createElement("div",{ref:t.ref,"data-placeholder":t.id,title:`Choose the step \u201C${t.label}\u201D should lead to`,onClick:n,onDragOver:a=>{o&&(a.preventDefault(),a.dataTransfer.dropEffect="move")},onDrop:a=>{a.preventDefault(),r()},className:o?oe:ee,style:te},e.createElement("div",{style:{fontWeight:600}},t.label),e.createElement("div",{style:{fontSize:11}},o?"drop a step here":"leads nowhere yet \u2014 click to choose")),ct=re;var ne=({entries:t,at:o,onPick:n,onConnect:r,onDismiss:a})=>e.createElement(A,{className:"shadow-lg",style:{position:"absolute",zIndex:9,top:o.y,left:o.x,width:210,padding:8,borderRadius:8}},e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{margin:"0 0 6px",fontSize:11}},"Add a step"),t.map(l=>e.createElement(x,{key:l.type,variant:"secondary",size:"sm",className:"w-full mb-1.5 flex-col",onClick:()=>n(l)},l.label,e.createElement("span",{className:"opacity-75",style:{display:"block",fontSize:11}},l.type))),r&&e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),pt=ne;var se={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},mt=({label:t,title:o,idle:n,enabled:r,onUse:a})=>e.createElement(x,{variant:"secondary",size:"sm",title:r?o:n,disabled:!r,onClick:a,style:{opacity:r?1:.4,cursor:r?"pointer":"default"}},t),ae=({undoable:t,redoable:o,onAdd:n,onUndo:r,onRedo:a})=>e.createElement("div",{style:{position:"absolute",top:16,left:16,zIndex:5,display:"flex",gap:8}},e.createElement(x,{variant:"secondary",className:"rounded-full",style:se,title:"Add a step",onClick:n},"+"),e.createElement(mt,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(mt,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),ut=ae;var ft=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var gt=({version:t,published:o})=>t?o?o===t?`Version ${t} \xB7 live`:`Version ${t} \xB7 visitors still on ${o}`:`Version ${t} \xB7 never published`:"No version yet";var le={width:280,padding:20,overflowY:"auto"},Y={fontWeight:600,marginBottom:6,marginTop:14},O={fontSize:12,marginBottom:4,lineHeight:1.4},yt={display:"block",marginBottom:3},ht=(t,o,n)=>r=>{r.target.value!==(t[o]??"")&&n({[o]:r.target.value})},ie=({flow:t,changes:o,problems:n,refusal:r,notice:a,onCreate:l,onPublish:i,onSaveDetails:y,onClose:p})=>e.createElement(A,{as:"aside",style:le,"data-builder-panel":!0},e.createElement("div",{style:{display:"flex",alignItems:"start",justifyContent:"space-between",gap:8}},e.createElement("h2",{style:{fontWeight:600},"data-flow-name":!0},t.title||t.slug),e.createElement("button",{title:"Close",onClick:p,className:"text-gray-500 dark:text-gray-400",style:{border:"none",background:"none",cursor:"pointer",fontSize:18,lineHeight:1}},"\xD7")),e.createElement("a",{className:"text-accent-600 dark:text-accent-400",style:{fontSize:12,display:"block"},href:t.history_url,"data-history":!0},gt(t)),e.createElement("h2",{style:Y},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...O,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...O,fontWeight:600},"data-notice":!0},a),n.length===0&&!r?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing wrong with this flow."):n.map(f=>e.createElement("p",{key:`${f.node}-${f.problem}`,className:"text-amber-700 dark:text-amber-400",style:O,"data-problem":!0},"\u26A0 ",ft(f))),e.createElement("h2",{style:Y},"Changes since the last version"),o.length===0?e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:12}},"Nothing has changed."):o.map((f,m)=>e.createElement("p",{key:m,style:O,"data-change":!0},f)),e.createElement("h2",{style:Y},"Details"),e.createElement(P,null,e.createElement("span",{style:yt},"Title"),e.createElement(S,{className:"mb-3",defaultValue:t.title??"",onBlur:ht(t,"title",y),"data-flow-title":!0})),e.createElement(P,null,e.createElement("span",{style:yt},"Start label"),e.createElement(S,{className:"mb-3",defaultValue:t.start_label??"",onBlur:ht(t,"start_label",y),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(x,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(x,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),xt=ie;var bt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},de=({link:t,onInsert:o,onRemove:n,onDrop:r,dragging:a})=>{let[l,i]=b(!1),y=l||a;return e.createElement("div",{"data-connector":`${t.source}-${t.target}`,style:{position:"absolute",left:t.midX-34,top:t.midY-20,width:68,height:40,zIndex:4},onMouseEnter:()=>i(!0),onMouseLeave:()=>i(!1),onDragEnter:()=>i(!0),onDragLeave:()=>i(!1),onDragOver:p=>{p.preventDefault(),p.dataTransfer.dropEffect="move"},onDrop:p=>{p.preventDefault(),i(!1),r()}},e.createElement("div",{style:{display:"flex",gap:4,justifyContent:"center",alignItems:"center",height:"100%",opacity:y?1:0,transition:"opacity .12s"}},e.createElement(x,{variant:"secondary",title:a?"Move the step here":"Insert a step here",onClick:o,className:"rounded-full data-[over=true]:ring-2 data-[over=true]:ring-accent-600","data-over":!!(a&&l),style:bt},"+"),!a&&e.createElement(x,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:bt},"\xD7")))},vt=de;var ce=({links:t,extent:o,dragging:n,onInsert:r,onRemove:a,onDrop:l})=>e.createElement(e.Fragment,null,e.createElement("svg",{width:o.width,height:o.height,style:{position:"absolute",top:0,left:0,pointerEvents:"none",zIndex:1}},e.createElement("defs",null,e.createElement("marker",{id:"easy_flow-arrow",viewBox:"0 0 10 10",refX:"8",refY:"5",markerWidth:"5",markerHeight:"5",orient:"auto"},e.createElement("path",{d:"M 0 0 L 10 5 L 0 10 z",className:"fill-gray-400 dark:fill-zinc-500"}))),t.map(i=>e.createElement("path",{key:i.id,"data-link":i.id,className:"stroke-gray-400 dark:stroke-zinc-500",fill:"none",strokeWidth:"1.5",markerEnd:"url(#easy_flow-arrow)",d:i.path}))),t.filter(i=>i.label).map(i=>e.createElement("div",{key:`${i.id}-label`,"data-link-label":i.id,className:"bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400",style:{position:"absolute",left:i.midX-10,top:i.midY-18,zIndex:3,fontSize:11,padding:"0 3px"}},i.label)),t.filter(i=>!i.placeholder).map(i=>e.createElement(vt,{key:i.id,link:i,dragging:!!n,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),kt=ce;var pe="Your change was not saved because the server could not be reached.",wt=({base:t,token:o,fetch:n,onFlow:r,onError:a,onNotice:l})=>async(i,y,p)=>{let f=await n(t+i,{method:y,headers:{"Content-Type":"application/json","X-CSRF-Token":o},body:p&&JSON.stringify(p)}).catch(()=>null);if(!f)return l(null),a(pe);let m=await f.json().catch(()=>({}));if(!f.ok)return l(null),a(m.error||"That change was refused");a(null),l(m.notice||null),r(await(await n(t+".json",{headers:{Accept:"application/json"}})).json())};var me=(t,o,n)=>{let[r,a]=b(n),[l,i]=b(null),[y,p]=b(null),f=D(()=>wt({base:t,token:o,fetch:(...m)=>window.fetch(...m),onFlow:a,onError:i,onNotice:p}),[t,o]);return{flow:r,error:l,notice:y,send:f}},Ct=me;var H=(t,o,n,r)=>{let a=t.left-n.left+r.scrollLeft,l=t.top-n.top+r.scrollTop;return o==="left"?{x:a,y:l+t.height/2}:o==="right"?{x:a+t.width,y:l+t.height/2}:o==="top"?{x:a+t.width/2,y:l}:{x:a+t.width/2,y:l+t.height}},ue=(t,o)=>o==="top"?{x:t.x,y:t.y-3}:o==="left"?{x:t.x-3,y:t.y}:o==="right"?{x:t.x+3,y:t.y}:{x:t.x,y:t.y+3},fe=(t,o,n,r,a)=>{let l={straight:`M ${o.x} ${o.y} L ${n.x} ${n.y}`,turn:`M ${o.x} ${o.y} H ${n.x} V ${n.y}`,lane:`M ${o.x} ${o.y} V ${r} H ${n.x} V ${n.y}`,detour:`M ${o.x} ${o.y} H ${a} V ${n.y} H ${n.x}`}[t.route];return{...t,path:l,midX:t.route==="detour"?a:(o.x+n.x)/2,midY:t.route==="lane"?r:(o.y+n.y)/2}},ge=(t,o,n,r)=>{let[a,l]=b([]),[i,y]=b({width:"100%",height:"100%"});return Z(()=>{let p=()=>{let m=o.current?.getBoundingClientRect();m&&(y({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(d=>{let h=n.current[d.source]?.getBoundingClientRect(),c=n.current[d.target]?.getBoundingClientRect();if(!h||!c)return[];let g=H(h,d.leaves,m,o.current),w=ue(H(c,d.enters,m,o.current),d.enters),v=(H(h,"bottom",m,o.current).y+H(c,"top",m,o.current).y)/2,B=d.leaves==="left"?Math.min(g.x,w.x)-28:Math.max(g.x,w.x)+28;return[fe(d,g,w,v,B)]})))};p(),window.addEventListener("resize",p);let f=new ResizeObserver(p);return o.current&&f.observe(o.current),()=>{window.removeEventListener("resize",p),f.disconnect()}},r),{links:a,extent:i}},St=ge;var Nt=(t,o)=>{let n=new Set(o),r=t,a=2;for(;n.has(r);)r=`${t}_${a++}`;return r};var xe={display:"flex",height:"100%",minHeight:0,fontSize:13},be={flex:1,overflow:"auto",position:"relative"},ve={position:"absolute",top:56,left:16,zIndex:5,width:264,padding:12,borderRadius:8},ke={display:"grid",rowGap:64,columnGap:0,padding:40,justifyContent:"center",position:"relative"},we={position:"sticky",zIndex:8,top:8,margin:"8px auto 0",width:"fit-content",padding:"6px 12px",borderRadius:6,fontSize:12},Ce=({port:t,onCancel:o})=>e.createElement("div",{className:"bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100",style:we},"Choose the step \u201C",t||"next","\u201D should lead to \u2014 ",e.createElement("button",{onClick:o,className:"text-accent-800 underline dark:text-accent-100",style:{border:"none",background:"none",cursor:"pointer",fontSize:12,padding:0}},"cancel")),Se=({base:t,token:o,initial:n})=>{let{flow:r,error:a,notice:l,send:i}=Ct(t,o,n),[y,p]=b(null),[f,m]=b(null),[d,h]=b(null),[c,g]=b(null),[w,v]=b(!1),B=j({}),E=j(null);T(()=>{let s=()=>{p(null),v(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),T(()=>{if(!w)return;let s=u=>{u.target.closest("[data-builder-panel], [data-open-panel]")||v(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[w]),T(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let Bt=D(()=>Object.fromEntries(r.nodes.map(s=>[s.id,s])),[r.nodes]),{links:Et,extent:Tt}=St(r,E,B,[r,y,Bt]),U=D(()=>{let s={};return r.violations.forEach(u=>{var k;return(s[k=u.node]||(s[k]=[])).push(u)}),s},[r.violations]),F=r.nodes.filter(s=>!s.loose),X=r.nodes.filter(s=>s.loose),$t=Math.max(0,...F.map(s=>s.row))+1,At=Math.max(0,...F.map(s=>s.column))+1,G=s=>{if(!d)return;let[u,k]=d;h(null),u!==s&&i("/edges","POST",{from:u,to:s,on:k||null})},N=r.nodes.find(s=>s.id===y),z=r.palette.find(s=>s.type===N?.type);return e.createElement("div",{className:"text-gray-900 dark:text-gray-100",style:xe,onMouseUp:()=>g(null),onKeyDown:s=>{s.key==="Escape"&&(p(null),h(null),m(null))},tabIndex:-1},e.createElement("div",{ref:E,className:"bg-surface-50 dark:bg-surface-950",style:be,onClick:s=>{s.target===E.current&&(p(null),h(null),v(!1))}},d&&e.createElement(Ce,{port:d[1],onCancel:()=>h(null)}),e.createElement(ut,{undoable:r.undoable,redoable:r.redoable,onAdd:()=>m({at:{x:16,y:52}}),onUndo:()=>{p(null),i("/undo","POST")},onRedo:()=>{p(null),i("/redo","POST")}}),e.createElement(kt,{links:Et,extent:Tt,dragging:c,onInsert:s=>m({from:s.source,to:s.target,at:{x:s.midX+30,y:s.midY}}),onRemove:s=>{p(null),i("/edges","DELETE",{from:s.source,to:s.target})},onDrop:s=>{let u=c;g(null),i("/steps/"+u+"/move","PATCH",{from:s.source,to:s.target})}}),X.length>0&&e.createElement("div",{"data-loose":!0,className:"border border-dashed border-gray-300 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-900",style:ve},e.createElement("div",{className:"text-gray-500 dark:text-gray-400",style:{fontSize:11,marginBottom:6}},"Not in the flow yet \u2014 drag one onto a connection"),X.map(s=>e.createElement("div",{key:s.id,style:{marginBottom:8}},e.createElement(V,{node:{...s,ref:u=>{B.current[s.id]=u},violations:U[s.id]||[],connected:[]},selected:y===s.id,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d,onSelect:()=>d?G(s.id):p(s.id),onOpen:u=>window.location.assign(u),onArm:(u,k)=>{k.stopPropagation(),h([s.id,u])},onDragStart:()=>g(s.id),onDragEnd:()=>g(null)})))),e.createElement("div",{style:{...ke,gridTemplateColumns:`repeat(${At+1}, ${280/2}px)`,gridTemplateRows:`repeat(${$t}, auto)`}},F.map(s=>e.createElement("div",{key:s.id,style:{gridRow:s.row+1,gridColumn:`${s.column+1} / span 2`,zIndex:2}},s.placeholder&&e.createElement(ct,{node:{...s,ref:u=>{B.current[s.id]=u}},dragging:!!c,onFill:()=>m({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let u=c;g(null),u&&i("/edges","POST",{from:s.from,to:u,on:s.on})}}),!s.placeholder&&e.createElement(V,{node:{...s,ref:u=>{B.current[s.id]=u},violations:U[s.id]||[],connected:r.edges.filter(u=>u.source===s.id).map(u=>u.label||null)},selected:s.id===y,armed:d&&d[0]===s.id?d[1]:null,connecting:!!d&&d[0]!==s.id,onSelect:()=>d?G(s.id):p(s.id),onOpen:u=>window.location.assign(u),onArm:(u,k)=>{let q=E.current.getBoundingClientRect();m({from:s.id,on:u,at:{x:k.clientX-q.left+E.current.scrollLeft+8,y:k.clientY-q.top+E.current.scrollTop+8}})},onDragEnd:()=>g(null),onDragStart:()=>g(s.id)})))),f&&e.createElement(pt,{entries:r.palette,at:f.at,onDismiss:()=>m(null),onConnect:f.on!==void 0?()=>{h([f.from,f.on]),m(null)}:null,onPick:s=>{let u=f;m(null),i("/steps","POST",{id:Nt(s.type,r.nodes.map(k=>k.id)),type:s.type,from:u.from,to:u.to,on:u.on})}})),w&&!N&&e.createElement(xt,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>v(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:s=>i("/details","PATCH",{flow:s})}),N&&e.createElement(dt,{node:N,fields:z?.fields||{},holds:z?.records||{},labels:z?.labels||{},recordLabels:z?.record_labels||{},recordChoices:z?.record_choices||{},recordOutputsOf:z?.record_outputs_of||{},choices:nt(N,z),onClose:()=>p(null),onSave:s=>i(`/steps/${N.id}`,"PATCH",{config:s}),onDelete:()=>{p(null),i(`/steps/${N.id}`,"DELETE")}}))},zt=Se;R("easy_flow/flow-editor",zt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});ot(document,J);})();
1
+ (()=>{var Lt=globalThis.ReactDOM;var{createPortal:Be,createRoot:Z,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 Q=globalThis.React,e=Q,{Children:Ve,Component:We,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:P,useId:io,useImperativeHandle:lo,useLayoutEffect:R,useMemo:D,useReducer:co,useRef:W,useState:x,useSyncExternalStore:po,useTransition:uo}=Q;var tt=new Map,I=new Map,et=(t,o)=>tt.set(t,o),ot=()=>{I.forEach(t=>t.unmount()),I.clear()},rt=(t,o)=>t.querySelectorAll("[data-react-ui]").forEach(n=>{if(I.has(n))return;let r=tt.get(n.dataset.reactUi),a=o(n);I.set(n,a),a.render(e.createElement(r,JSON.parse(n.dataset.props)))});var nt=(t,o)=>{let n=()=>rt(t,o);t.addEventListener("turbo:load",n),t.addEventListener("turbo:frame-load",n),t.addEventListener("turbo:before-cache",ot),t.readyState==="loading"?t.addEventListener("DOMContentLoaded",n):n()};var S=(...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:S("ks-button",It[t],Mt[o],n)},r)},b=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",Vt=({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"),st=Vt;var Wt={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,y=t.begins_here,u=t.ends_here?[]:t.ports,f=r&&!y;return e.createElement("div",{ref:t.ref,"data-step":t.id,draggable:!y,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(f,t.violations.length>0,o)}`,style:{...Wt,padding:c?0:"12px 14px",cursor:y?"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(b,{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(st,{key:p,name:p,connected:t.connected.includes(p),connecting:r,armed:n===p,onArm:w=>i(p,w)}))))},U=Gt;var Y=(t,o)=>t.includes(o)?t.filter(n=>n!==o):[...t,o],at=(t,o)=>({...o?.choices||{},...t?.choices||{}});var qt=({as:t="input",disabled:o,className:n,...r})=>e.createElement(t,{...r,disabled:o,className:S("ks-input",o&&"ks-input-disabled",n)}),z=qt;var Jt=t=>e.createElement(z,{...t,as:"select"}),it=Jt;var Kt=({className:t,...o})=>e.createElement("input",{type:"checkbox",...o,className:S("ks-checkbox",t)}),M=Kt;var lt=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(M,{checked:!!o,onChange:d=>a(d.target.checked)});if(t==="select"||t==="previous_step"||t==="from_step"||t==="flow")return e.createElement(it,{className:"mb-3",value:o??"",onChange:d=>a(d.target.value)},e.createElement("option",{value:""}),lt(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(Y(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(M,{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"},lt(n).map(m=>e.createElement("label",{key:m.value,style:{display:"inline-flex",alignItems:"center",gap:4}},e.createElement(M,{checked:d.includes(m.value),onChange:()=>a(Y(d,m.value))}),e.createElement("span",null,m.label))))}return t==="integer"||t==="float"?e.createElement(z,{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(z,{className:"mb-3",type:"text",value:o??"",onChange:d=>r(d.target.value),onBlur:d=>a(d.target.value)})},O=Zt;var dt=(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,y,u,f)=>{let p=dt(d,c,y,u);f?i(p):l(p)};return e.createElement("div",{style:{marginBottom:12}},d.map((c,y)=>e.createElement("div",{key:y,className:"rounded-md border border-gray-200 dark:border-zinc-700",style:{padding:"8px 8px 2px",marginBottom:6}},Object.entries(t).map(([u,f])=>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(O,{type:f,value:c[u],choices:Qt(r,a,u,c),onChange:p=>m(y,u,p,!1),onSettle:p=>m(y,u,p,!0)}))),e.createElement(b,{variant:"secondary",size:"sm",className:"mb-1.5",onClick:()=>i(d.filter((u,f)=>f!==y))},"Remove"))),e.createElement(b,{variant:"secondary",size:"sm",className:"w-full",onClick:()=>i([...d,{}])},"Add"))},ct=Rt;var te=({as:t="div",className:o,children:n,...r})=>e.createElement(t,{...r,className:S("ks-panel",o)},n),A=te;var H=(t,o)=>({className:n,children:r,...a})=>e.createElement(t,{...a,className:S(o,n)},r),L=H("label","ks-label"),rr=H("p","ks-hint"),nr=H("p","ks-error"),sr=H("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:y,onPickPicture:u})=>{let[f,p]=x(t.config),[w,k]=x(t.picture_url);P(()=>k(t.picture_url),[t.id,t.picture_url]),P(()=>p(t.config),[t.id,t.config]);let N=h=>{p(h),JSON.stringify(h)!==JSON.stringify(t.config)&&m(h)};return e.createElement(A,{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:y,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(([h,B])=>e.createElement(L,{key:h},e.createElement("span",{style:{display:"block",marginBottom:3}},r[h]||h),B==="list"?e.createElement(ct,{holds:n[h]||{},labels:a[h]||{},rows:f[h],choices:{...(l||{})[h],...(t.record_choices||{})[h]},outputsOf:(i||{})[h],onChange:v=>p({...f,[h]:v}),onSettle:v=>N({...f,[h]:v})}):e.createElement(O,{type:B,value:f[h],choices:d[h],pictureUrl:w,onChange:v=>p({...f,[h]:v}),onSettle:v=>N({...f,[h]:v}),onPick:async v=>{let _=await u(v);_&&(k(_.url),N({...f,[h]:_.reference}))}}))),e.createElement(b,{variant:"danger",size:"sm",className:"w-full",onClick:c},"Delete step"))},pt=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")),ut=ae;var ie=({entries:t,at:o,onPick:n,onConnect:r,onDismiss:a})=>e.createElement(A,{className:"shadow-lg",style:{position:"absolute",zIndex:9,top:o.y,left:o.x,width:210,padding:8,borderRadius:8}},e.createElement("p",{className:"text-gray-500 dark:text-gray-400",style:{margin:"0 0 6px",fontSize:11}},"Add a step"),t.map(l=>e.createElement(b,{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(b,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:r},"Connect to a step already here"),e.createElement(b,{variant:"secondary",size:"sm",className:"w-full",onClick:a},"Cancel")),mt=ie;var le={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ft=({label:t,title:o,idle:n,enabled:r,onUse:a})=>e.createElement(b,{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(b,{variant:"secondary",className:"rounded-full",style:le,title:"Add a step",onClick:n},"+"),e.createElement(ft,{label:"\u21B6 Undo",title:"Undo the last change",idle:"Nothing to undo",enabled:t,onUse:r}),e.createElement(ft,{label:"\u21B7 Redo",title:"Redo the change you undid",idle:"Nothing to redo",enabled:o,onUse:a})),gt=de;var yt=t=>{let o=t.problem.replace(/_/g," ");return t.node?`\u201C${t.node}\u201D ${o}`:o};var ht=({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"},X={fontWeight:600,marginBottom:6,marginTop:14},F={fontSize:12,marginBottom:4,lineHeight:1.4},bt={display:"block",marginBottom:3},xt=(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(A,{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},ht(t)),e.createElement("h2",{style:X},"Problems"),r&&e.createElement("p",{className:"text-red-800 dark:text-red-400",style:{...F,fontWeight:600},"data-refusal":!0},r),a&&!r&&e.createElement("p",{className:"text-emerald-800 dark:text-emerald-400",style:{...F,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:F,"data-problem":!0},"\u26A0 ",yt(c))),e.createElement("h2",{style:X},"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,y)=>e.createElement("p",{key:y,style:F,"data-change":!0},c)),e.createElement("h2",{style:X},"Details"),e.createElement(L,null,e.createElement("span",{style:bt},"Title"),e.createElement(z,{className:"mb-3",defaultValue:t.title??"",onBlur:xt(t,"title",d),"data-flow-title":!0})),e.createElement(L,null,e.createElement("span",{style:bt},"Start label"),e.createElement(z,{className:"mb-3",defaultValue:t.start_label??"",onBlur:xt(t,"start_label",d),"data-flow-start-label":!0})),e.createElement("div",{style:{marginTop:16}},e.createElement(b,{variant:"secondary",size:"sm",className:"w-full mb-1.5",onClick:l,"data-create-version":!0},"Create version"),e.createElement(b,{size:"sm",className:"w-full mb-1.5",onClick:i,"data-publish":!0},"Publish"),e.createElement(b,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.definition_url,"data-definition":!0},"Definition"),e.createElement(b,{variant:"secondary",size:"sm",className:"w-full mb-1.5",href:t.details_url,"data-details":!0},"Edit details"))),vt=pe;var kt={width:24,height:24,padding:0,fontSize:15,lineHeight:"15px"},ue=({link:t,onInsert:o,onRemove:n,onDrop:r,dragging:a})=>{let[l,i]=x(!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(b,{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:kt},"+"),!a&&e.createElement(b,{variant:"danger",title:"Remove this connection",onClick:n,className:"rounded-full",style:kt},"\xD7")))},wt=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(wt,{key:i.id,link:i,dragging:!!n,onInsert:()=>r(i),onRemove:()=>a(i),onDrop:()=>l(i)}))),Ct=me;var St="Your change was not saved because the server could not be reached.",Nt=({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(St);let y=await c.json().catch(()=>({}));if(!c.ok)return l(null),a(y.error||"That change was refused");a(null),l(y.notice||null),r(await(await n(t+".json",{headers:{Accept:"application/json"}})).json())},zt=({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(St),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]=x(n),[l,i]=x(null),[d,m]=x(null),c=D(()=>Nt({base:t,token:o,fetch:(...u)=>window.fetch(...u),onFlow:a,onError:i,onNotice:m}),[t,o]),y=D(()=>zt({base:t,token:o,fetch:(...u)=>window.fetch(...u),onError:i}),[t,o]);return{flow:r,error:l,notice:d,send:c,pickPicture:y}},Bt=fe;var j=(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]=x([]),[i,d]=x({width:"100%",height:"100%"});return R(()=>{let m=()=>{let y=o.current?.getBoundingClientRect();y&&(d({width:o.current.scrollWidth,height:o.current.scrollHeight}),l(t.edges.flatMap(u=>{let f=n.current[u.source]?.getBoundingClientRect(),p=n.current[u.target]?.getBoundingClientRect();if(!f||!p)return[];let w=j(f,u.leaves,y,o.current),k=ge(j(p,u.enters,y,o.current),u.enters),N=(j(f,"bottom",y,o.current).y+j(p,"top",y,o.current).y)/2,h=u.leaves==="left"?Math.min(w.x,k.x)-28:Math.max(w.x,k.x)+28;return[ye(u,w,k,N,h)]})))};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}},Tt=he;var Et=(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}=Bt(t,o,n),[m,c]=x(null),[y,u]=x(null),[f,p]=x(null),[w,k]=x(null),[N,h]=x(!1),B=W({}),v=W(null);P(()=>{let s=()=>{c(null),h(!0)};return document.addEventListener("easy_flow:open-flow",s),()=>document.removeEventListener("easy_flow:open-flow",s)},[]),P(()=>{if(!N)return;let s=g=>{g.target.closest("[data-builder-panel], [data-open-panel]")||h(!1)};return document.addEventListener("click",s),()=>document.removeEventListener("click",s)},[N]),P(()=>{document.dispatchEvent(new CustomEvent("easy_flow:flow-named",{detail:r.flow?.title||r.flow?.slug}))},[r.flow?.title,r.flow?.slug]);let _=D(()=>Object.fromEntries(r.nodes.map(s=>[s.id,s])),[r.nodes]),{links:Dt,extent:$t}=Tt(r,v,B,[r,m,_]),G=D(()=>{let s={};return r.violations.forEach(g=>{var C;return(s[C=g.node]||(s[C]=[])).push(g)}),s},[r.violations]),V=r.nodes.filter(s=>!s.loose),q=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,J=s=>{if(!f)return;let[g,C]=f;p(null),g!==s&&i("/edges","POST",{from:g,to:s,on:C||null})},T=r.nodes.find(s=>s.id===m),E=r.palette.find(s=>s.type===T?.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:v,className:"bg-surface-50 dark:bg-surface-950",style:ke,onClick:s=>{s.target===v.current&&(c(null),p(null),h(!1))}},f&&e.createElement(Ne,{port:f[1],onCancel:()=>p(null)}),e.createElement(gt,{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(Ct,{links:Dt,extent:$t,dragging:w,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 g=w;k(null),i("/steps/"+g+"/move","PATCH",{from:s.source,to:s.target})}}),q.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"),q.map(s=>e.createElement("div",{key:s.id,style:{marginBottom:8}},e.createElement(U,{node:{...s,ref:g=>{B.current[s.id]=g},violations:G[s.id]||[],connected:[]},selected:m===s.id,armed:f&&f[0]===s.id?f[1]:null,connecting:!!f,onSelect:()=>f?J(s.id):c(s.id),onOpen:g=>window.location.assign(g),onArm:(g,C)=>{C.stopPropagation(),p([s.id,g])},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(ut,{node:{...s,ref:g=>{B.current[s.id]=g}},dragging:!!w,onFill:()=>u({from:s.from,on:s.on,at:{x:16,y:52}}),onDrop:()=>{let g=w;k(null),g&&i("/edges","POST",{from:s.from,to:g,on:s.on})}}),!s.placeholder&&e.createElement(U,{node:{...s,ref:g=>{B.current[s.id]=g},violations:G[s.id]||[],connected:r.edges.filter(g=>g.source===s.id).map(g=>g.label||null)},selected:s.id===m,armed:f&&f[0]===s.id?f[1]:null,connecting:!!f&&f[0]!==s.id,onSelect:()=>f?J(s.id):c(s.id),onOpen:g=>window.location.assign(g),onArm:(g,C)=>{let K=v.current.getBoundingClientRect();u({from:s.id,on:g,at:{x:C.clientX-K.left+v.current.scrollLeft+8,y:C.clientY-K.top+v.current.scrollTop+8}})},onDragEnd:()=>k(null),onDragStart:()=>k(s.id)})))),y&&e.createElement(mt,{entries:r.palette,at:y.at,onDismiss:()=>u(null),onConnect:y.on!==void 0?()=>{p([y.from,y.on]),u(null)}:null,onPick:s=>{let g=y;u(null),i("/steps","POST",{id:Et(s.type,r.nodes.map(C=>C.id)),type:s.type,from:g.from,to:g.to,on:g.on})}})),N&&!T&&e.createElement(vt,{flow:r.flow||{},changes:r.changes||[],problems:r.violations,refusal:a,notice:l,onClose:()=>h(!1),onCreate:()=>i("/versions","POST"),onPublish:()=>i("/publish","POST"),onSaveDetails:s=>i("/details","PATCH",{flow:s})}),T&&e.createElement(pt,{node:T,fields:E?.fields||{},holds:E?.records||{},labels:E?.labels||{},recordLabels:E?.record_labels||{},recordChoices:E?.record_choices||{},recordOutputsOf:E?.record_outputs_of||{},choices:at(T,E),onClose:()=>c(null),onSave:s=>i(`/steps/${T.id}`,"PATCH",{config:s}),onPickPicture:d,onDelete:()=>{c(null),i(`/steps/${T.id}`,"DELETE")}}))},Pt=ze;et("easy_flow/flow-editor",Pt);document.addEventListener("easy_flow:flow-named",t=>{let o=document.querySelector("[data-flow-heading]");o&&(o.textContent=t.detail)});nt(document,Z);})();
@@ -50,6 +50,13 @@ module EasyFlow
50
50
  end
51
51
  end
52
52
 
53
+ def keep_picture
54
+ reference = EasyFlow.keep_picture(params.expect(:picture), flow: flow)
55
+ render json: { reference: reference, url: EasyFlow.picture_url(reference) }
56
+ rescue NoPictureStore
57
+ render json: { error: "Pictures cannot be added yet. Ask the app's developer to set up a picture store." }, status: :unprocessable_entity
58
+ end
59
+
53
60
  def configure_step
54
61
  apply(:updated, params[:step]) { |edited| edited.configure(params[:step], coerced(edited, params[:step])) }
55
62
  end
@@ -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 } = useFlow(base, token, initial)
29
+ const { flow, error, notice, 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)
@@ -191,6 +191,7 @@ const Canvas = ({ base, token, initial }) => {
191
191
  choices={offeredTo(selectedNode, entryFor)}
192
192
  onClose={() => setSelected(null)}
193
193
  onSave={(config) => send(`/steps/${selectedNode.id}`, "PATCH", { config })}
194
+ onPickPicture={pickPicture}
194
195
  onDelete={() => { setSelected(null); send(`/steps/${selectedNode.id}`, "DELETE") }} />
195
196
  )}
196
197
  </div>
@@ -7,7 +7,15 @@ import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
7
7
  const offered = (choices) =>
8
8
  (choices || []).map((choice) => (typeof choice === "object" ? choice : { value: choice, label: choice }))
9
9
 
10
- const Control = ({ type, value, choices, onChange, onSettle }) => {
10
+ const Control = ({ type, value, choices, onChange, onSettle, onPick, pictureUrl }) => {
11
+ if (type === "picture") {
12
+ return <div className="mb-3">
13
+ {value && pictureUrl && <img src={pictureUrl} alt="The step's picture" style={{ maxWidth: "100%" }} />}
14
+ {value && <button type="button" data-remove-picture className="ks-button ks-button-secondary ks-button-sm" onClick={() => onSettle("")}>Remove picture</button>}
15
+ <input type="file" accept="image/*" onChange={(e) => e.target.files[0] && onPick(e.target.files[0])} />
16
+ </div>
17
+ }
18
+
11
19
  if (type === "boolean") return <Checkbox checked={Boolean(value)} onChange={(e) => onSettle(e.target.checked)} />
12
20
 
13
21
  if (type === "select" || type === "previous_step" || type === "from_step" || type === "flow") {
@@ -7,8 +7,10 @@ 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, recordChoices, recordOutputsOf, choices, onSave, onDelete, onClose }) => {
10
+ const Inspector = ({ node, fields, holds, labels, recordLabels, recordChoices, recordOutputsOf, choices, onSave, onDelete, onClose, onPickPicture }) => {
11
11
  const [ draft, setDraft ] = useState(node.config)
12
+ const [ pictureUrl, setPictureUrl ] = useState(node.picture_url)
13
+ useEffect(() => setPictureUrl(node.picture_url), [ node.id, node.picture_url ])
12
14
  useEffect(() => setDraft(node.config), [ node.id, node.config ])
13
15
 
14
16
  const settle = (next) => {
@@ -33,9 +35,15 @@ const Inspector = ({ node, fields, holds, labels, recordLabels, recordChoices, r
33
35
  outputsOf={(recordOutputsOf || {})[name]}
34
36
  onChange={(next) => setDraft({ ...draft, [name]: next })}
35
37
  onSettle={(next) => settle({ ...draft, [name]: next })} />
36
- : <Control type={type} value={draft[name]} choices={choices[name]}
38
+ : <Control type={type} value={draft[name]} choices={choices[name]} pictureUrl={pictureUrl}
37
39
  onChange={(next) => setDraft({ ...draft, [name]: next })}
38
- onSettle={(next) => settle({ ...draft, [name]: next })} />}
40
+ onSettle={(next) => settle({ ...draft, [name]: next })}
41
+ onPick={async (file) => {
42
+ const kept = await onPickPicture(file)
43
+ if (!kept) return
44
+ setPictureUrl(kept.url)
45
+ settle({ ...draft, [name]: kept.reference })
46
+ }} />}
39
47
  </Label>
40
48
  ))}
41
49
  <Button variant="danger" size="sm" className="w-full" onClick={onDelete}>Delete step</Button>
@@ -20,3 +20,22 @@ export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice
20
20
  onNotice(answered.notice || null)
21
21
  onFlow(await (await fetch(base + ".json", { headers: { Accept: "application/json" } })).json())
22
22
  }
23
+
24
+ export const createPictureSender = ({ base, token, fetch, onError }) => async (file) => {
25
+ const body = new FormData()
26
+ body.append("picture", file)
27
+ const response = await fetch(base + "/pictures", { method: "POST", headers: { "X-CSRF-Token": token }, body }).catch(() => null)
28
+ if (!response) {
29
+ onError(UNREACHABLE)
30
+ return null
31
+ }
32
+
33
+ const answered = await response.json().catch(() => ({}))
34
+ if (!response.ok) {
35
+ onError(answered.error || "That picture was refused")
36
+ return null
37
+ }
38
+
39
+ onError(null)
40
+ return answered
41
+ }
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useState } from "react"
2
- import { createFlowSender } from "./flowSender"
2
+ import { createFlowSender, createPictureSender } from "./flowSender"
3
3
 
4
4
  const useFlow = (base, token, initial) => {
5
5
  const [ flow, setFlow ] = useState(initial)
@@ -11,7 +11,12 @@ const useFlow = (base, token, initial) => {
11
11
  [ base, token ]
12
12
  )
13
13
 
14
- return { flow, error, notice, send }
14
+ const pickPicture = useMemo(
15
+ () => createPictureSender({ base, token, fetch: (...request) => window.fetch(...request), onError: setError }),
16
+ [ base, token ]
17
+ )
18
+
19
+ return { flow, error, notice, send, pickPicture }
15
20
  }
16
21
 
17
22
  export default useFlow
@@ -1,3 +1,7 @@
1
1
  module EasyFlow
2
- Asked = Data.define(:id, :text, :choices)
2
+ Asked = Data.define(:id, :text, :choices, :picture_url) do
3
+ def initialize(id:, text:, choices:, picture_url: nil)
4
+ super
5
+ end
6
+ end
3
7
  end
@@ -22,7 +22,11 @@ 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), "record_choices" => record_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, "picture_url" => picture_url(node), **placed[node.id] }
26
+ end
27
+
28
+ def picture_url(node)
29
+ EasyFlow.picture_url(node.config["picture"]) if node.config["picture"].present?
26
30
  end
27
31
 
28
32
  def drawn_placeholder(gap)
@@ -3,11 +3,16 @@ module EasyFlow
3
3
  class FillInTheBlank
4
4
  include Step
5
5
 
6
- Asked = Data.define(:id, :text)
6
+ Asked = Data.define(:id, :text, :picture_url) do
7
+ def initialize(id:, text:, picture_url: nil)
8
+ super
9
+ end
10
+ end
7
11
 
8
12
  step_name "Fill in the blank"
9
13
 
10
14
  setting :question, type: :string
15
+ setting :picture, type: :picture
11
16
  setting :category, type: :string
12
17
  setting :required, type: :boolean
13
18
  setting :accepted_answers, type: :list, required: true do
@@ -19,7 +24,7 @@ module EasyFlow
19
24
  drawn_by "easy_flow/steps/typing"
20
25
  answer_check { |node, value| "Type an answer to go on." if node.config["required"] && value.blank? }
21
26
 
22
- displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"]) }
27
+ displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"], picture_url: EasyFlow.picture_url(node.config["picture"])) }
23
28
  end
24
29
  end
25
30
  end
@@ -6,6 +6,7 @@ module EasyFlow
6
6
  step_name "Multiple select"
7
7
 
8
8
  setting :question, type: :string
9
+ setting :picture, type: :picture
9
10
  setting :category, type: :string
10
11
  setting :required, type: :boolean
11
12
  setting :answers, type: :list, required: true do
@@ -19,7 +20,7 @@ module EasyFlow
19
20
  drawn_by "easy_flow/steps/ticking"
20
21
  answer_check { |node, value| "Pick at least one answer to go on." if node.config["required"] && Array(value).compact_blank.empty? }
21
22
 
22
- displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"], choices: Question.choices_in(node)) }
23
+ displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"], choices: Question.choices_in(node), picture_url: EasyFlow.picture_url(node.config["picture"])) }
23
24
  end
24
25
  end
25
26
  end
@@ -6,6 +6,7 @@ module EasyFlow
6
6
  step_name "Question"
7
7
 
8
8
  setting :question, type: :string
9
+ setting :picture, type: :picture
9
10
  setting :category, type: :string
10
11
  setting :required, type: :boolean
11
12
  setting :answers, type: :list, required: true do
@@ -22,7 +23,7 @@ module EasyFlow
22
23
  awaits_input
23
24
  answer_check { |node, value| "Fill this in to go on." if node.config["required"] && value.blank? }
24
25
 
25
- displays_by { |node| Asked.new(id: node.id.to_sym, text: asked(node.config), choices: choices_in(node)) }
26
+ displays_by { |node| Asked.new(id: node.id.to_sym, text: asked(node.config), choices: choices_in(node), picture_url: EasyFlow.picture_url(node.config["picture"])) }
26
27
 
27
28
  def self.asked(step)
28
29
  step.to_h["question"] || step.to_h["text"]
@@ -1,9 +1,10 @@
1
1
  <fieldset>
2
+ <%= render "easy_flow/steps/picture", step: step %>
2
3
  <legend class="ks-section-title mb-5 text-xl font-semibold leading-snug"><%= step.text %></legend>
3
4
 
4
5
  <div class="space-y-3">
5
6
  <% step.choices.each do |choice| %>
6
- <%= ui_radio_card(name: "answers[#{step.id}]", value: choice.value, label: choice.label.presence || choice.value, hint: choice.hint, info: choice.info) %>
7
+ <%= ui_radio_card(name: "answers[#{step.id}]", value: choice.value, label: choice.label.presence || choice.value, hint: choice.hint, info: choice.info, checked: choice.value == local_assigns[:chosen]) %>
7
8
  <% end %>
8
9
  </div>
9
10
  </fieldset>
@@ -0,0 +1,4 @@
1
+ <%# locals: (step:) %>
2
+ <% if step.picture_url %>
3
+ <img src="<%= step.picture_url %>" alt="The question's picture" class="mb-4 w-full max-w-full rounded">
4
+ <% end %>
@@ -1,10 +1,11 @@
1
1
  <fieldset>
2
+ <%= render "easy_flow/steps/picture", step: step %>
2
3
  <legend class="ks-section-title mb-5 text-xl font-semibold leading-snug"><%= step.text %></legend>
3
4
 
4
5
  <%= hidden_field_tag "answers[#{step.id}][]", "", id: nil %>
5
6
  <div class="space-y-3">
6
7
  <% step.choices.each do |choice| %>
7
- <%= ui_checkbox_row(name: "answers[#{step.id}][]", value: choice.value, label: choice.label.presence || choice.value, info: choice.info) %>
8
+ <%= ui_checkbox_row(name: "answers[#{step.id}][]", value: choice.value, label: choice.label.presence || choice.value, info: choice.info, checked: Array(local_assigns[:chosen]).include?(choice.value)) %>
8
9
  <% end %>
9
10
  </div>
10
11
  </fieldset>
@@ -1,4 +1,5 @@
1
1
  <label class="block">
2
+ <%= render "easy_flow/steps/picture", step: step %>
2
3
  <span class="ks-section-title mb-5 block text-xl font-semibold leading-snug"><%= step.text %></span>
3
- <%= ui_input(name: "answers[#{step.id}]") %>
4
+ <%= ui_input(name: "answers[#{step.id}]", value: local_assigns[:chosen]) %>
4
5
  </label>
data/config/routes.rb CHANGED
@@ -21,6 +21,7 @@ EasyFlow::Engine.routes.draw do
21
21
  post "redo", action: :redo
22
22
  post "edges", action: :connect
23
23
  delete "edges", action: :disconnect
24
+ post "pictures", action: :keep_picture
24
25
  end
25
26
  end
26
27
  end
@@ -1,3 +1,3 @@
1
1
  module EasyFlow
2
- VERSION = "0.28.0"
2
+ VERSION = "0.30.0"
3
3
  end
data/lib/easy_flow.rb CHANGED
@@ -7,7 +7,7 @@ require "easy_flow/host_routes"
7
7
  require "easy_flow/engine"
8
8
 
9
9
  module EasyFlow
10
- FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list flow flows].freeze
10
+ FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list flow flows picture].freeze
11
11
 
12
12
  OUTPUT_TYPES = %i[string integer float boolean].freeze
13
13
  OPTIONAL_CHECKS = %i[unrouted_value unfollowed_path dead_end].freeze
@@ -23,12 +23,13 @@ module EasyFlow
23
23
  class InnerFlowError < StandardError; end
24
24
  class Withdrawn < StandardError; end
25
25
  class NoFileStore < StandardError; end
26
+ class NoPictureStore < StandardError; end
26
27
 
27
28
  DRAWING = "easy_flow/steps/choosing".freeze
28
29
 
29
30
  class << self
30
31
  attr_writer :base_controller
31
- attr_accessor :file_store
32
+ attr_accessor :file_store, :picture_store
32
33
 
33
34
  def base_controller
34
35
  @base_controller || "ActionController::Base"
@@ -70,6 +71,16 @@ module EasyFlow
70
71
  file_store.keep(file)
71
72
  end
72
73
 
74
+ def keep_picture(file, flow:)
75
+ raise NoPictureStore, "set EasyFlow.picture_store to a store that keeps a picture for a flow and gives an address for it" unless picture_store
76
+
77
+ picture_store.keep(file, flow: flow)
78
+ end
79
+
80
+ def picture_url(reference)
81
+ picture_store.url_of(reference) if reference.present?
82
+ end
83
+
73
84
  def check(name)
74
85
  raise UnknownCheck, "#{name} is not one of #{OPTIONAL_CHECKS.join(', ')}" unless OPTIONAL_CHECKS.include?(name)
75
86
 
@@ -49,7 +49,7 @@ easy_flow declares no commands of its own for this local. Its surface is split b
49
49
  - **Canvas** — the admin screen where steps are added, configured, moved, removed and connected, with undo and redo. Publishing happens there.
50
50
  - **Preview** — an admin walking the current flow without starting a stored run.
51
51
  - **Step type** — what a kind of step is: its display name, its settings (the fields an admin fills in), its outputs (the values it records), and whether it waits for the visitor, acts on its own, or waits for something outside the flow. A step that acts on its own may record a value worked out from the answers so far, pick which connection to follow, or both.
52
- - **Display** — what a step type hands the visitor's page to draw a step, such as its question text and its answers. A step type may build its display from the step alone, or from the step and the visitor's stored run, so the same step can show something different to each visitor. There is no stored run in an admin's preview or in a flow that carries its answers in the page, and the display is then built without one.
52
+ - **Display** — what a step type hands the visitor's page to draw a step, such as its question text and its answers. A step type may build its display from the step alone, or from the step and the visitor's stored run, so the same step can show something different to each visitor. There is no stored run in an admin's preview or in a flow that carries its answers in the page, and the display is then built without one. A host can draw a single-choice, multiple-select or fill-in-the-blank step with an answer already chosen by passing it as `chosen`, and the step is drawn blank when none is passed.
53
53
  - **Answer check** — a rule a step type may carry that looks at a visitor's answer and, when the answer is not acceptable, returns the message the visitor sees. A step type with no such rule accepts every answer.
54
54
  - **Setting** — one field an admin fills in when configuring a step on the canvas. It is text, a whole number, a decimal, a yes or no, a pick from a list, several picks from a list, an earlier step, an earlier step's output, another flow of the same host, or a list of entries that each hold their own fields. A setting can be required, limited to a number of picks, or checked by a rule the step type supplies.
55
55
  - **Options** — the values a pick-from-a-list setting offers. They are either a fixed list written into the step type, or a lookup the app provides that is read each time the canvas or a save asks for them, so they can come from the app's own data. A saved value that is not among the options is refused.
@@ -19,6 +19,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
19
19
  - `EasyFlow.host` — declares a named host with its visitor layout, admin layout, admin authentication method, visitor authorization method, refusal method, owner method, and the step types its admins can add on the canvas.
20
20
  - `EasyFlow.draws_with` — the partial that draws a visitor's step when the step's type names none. Defaults to the engine's own `easy_flow/steps/choosing`.
21
21
  - `EasyFlow.file_store` — the object that keeps a file a visitor uploads at a File upload step and names it again later. Defaults to none, and with none set an uploaded file is refused with `EasyFlow::NoFileStore`.
22
+ - `EasyFlow.picture_store` — the object that keeps a picture an admin adds to a question step on the canvas, for the flow it belongs to, and gives the address the picture is loaded from. Defaults to none, and with none set an admin who chooses a picture is told pictures cannot be added yet.
22
23
  - `EasyFlow.check` — turns on an optional flow check: `:unrouted_value`, `:unfollowed_path` or `:dead_end`.
23
24
  - `/manage/flows` — the admin pages, under each mount path, where flows are listed, created, drawn on the canvas, previewed and published.
24
25
 
@@ -88,6 +89,16 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
88
89
  - When several files are sent for one step, `keep` is called once for each file, and the run records the list of references it returned, in the order the files were sent.
89
90
  - The store answers `name_of(reference)`, called with a reference `keep` returned. It returns the name the visitor's completion page shows for that answer. When it returns nothing, the page shows the reference itself.
90
91
  - With no store set, a visitor who submits a file at any step of a stored run gets the app's error page, raised as `EasyFlow::NoFileStore`.
92
+
93
+ Ask the developer too where the app keeps the pictures admins add to question, multiple select and fill-in-the-blank steps. The engine keeps only a reference to each picture in the step's settings and has no default store. Ask the developer to point at the app's store or write one, and do not pick a storage service for them. Set it in the initializer:
94
+
95
+ ```ruby
96
+ EasyFlow.picture_store = QuestionPictures.new
97
+ ```
98
+
99
+ - The store answers `keep(file, flow:)`, called with the chosen picture and the flow whose step it is added to. It keeps the picture and returns a string reference to it, and the step keeps that reference as its `picture` setting.
100
+ - The store answers `url_of(reference)`, called with a reference `keep` returned. It returns the address a page loads the picture from.
101
+ - With no store set, an admin who chooses a picture on the canvas is told "Pictures cannot be added yet. Ask the app's developer to set up a picture store." and the step is left unchanged.
91
102
  11. Leave `EasyFlow.check` out unless the developer asks for it. The engine already turns on `:unrouted_value`, `:unfollowed_path` and `:dead_end` at boot. Any other name raises `EasyFlow::UnknownCheck` when the app boots.
92
103
  12. Check that the app loads Active Job (`require "rails/all"` or `require "active_job/railtie"` in `config/application.rb`) and has a queue adapter that runs jobs from the `default` queue. When a run started by a Flow step, or by a step type of the app's own that starts another flow, ends, the engine records its output in the run it was started from and queues a job on `default` that moves that run on. If no adapter runs that queue, the run that holds the step stays at it until its visitor next loads its page. If `config.active_job.queue_adapter` is not set for an environment, ask the developer which adapter to use there.
93
104
  13. Check `config.time_zone` in `config/application.rb`. A Wait step's time of day and date, and the "Hour completed" and "Weekday completed" outputs every completed step offers, are read in that zone, and with none set it is UTC. If it is not set, ask the developer which zone the app's admins mean when they write a time.
@@ -7,6 +7,7 @@ install:
7
7
  - EasyFlow.host
8
8
  - EasyFlow.draws_with
9
9
  - EasyFlow.file_store
10
+ - EasyFlow.picture_store
10
11
  - EasyFlow.check
11
12
  - /manage/flows
12
13
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -157,6 +157,7 @@ files:
157
157
  - app/views/easy_flow/manage/flows/show.html.erb
158
158
  - app/views/easy_flow/manage/versions/index.html.erb
159
159
  - app/views/easy_flow/steps/_choosing.html.erb
160
+ - app/views/easy_flow/steps/_picture.html.erb
160
161
  - app/views/easy_flow/steps/_ticking.html.erb
161
162
  - app/views/easy_flow/steps/_typing.html.erb
162
163
  - app/views/easy_flow/steps/_uploading.html.erb