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 +4 -4
- data/app/assets/builds/easy_flow/canvas.js +1 -1
- data/app/controllers/easy_flow/manage/canvas_controller.rb +7 -0
- data/app/javascript/easy_flow/canvas/Canvas.jsx +2 -1
- data/app/javascript/easy_flow/canvas/Control.jsx +9 -1
- data/app/javascript/easy_flow/canvas/Inspector.jsx +11 -3
- data/app/javascript/easy_flow/canvas/flowSender.js +19 -0
- data/app/javascript/easy_flow/canvas/useFlow.js +7 -2
- data/app/models/easy_flow/asked.rb +5 -1
- data/app/models/easy_flow/canvas.rb +5 -1
- data/app/models/easy_flow/steps/fill_in_the_blank.rb +7 -2
- data/app/models/easy_flow/steps/multiple_select.rb +2 -1
- data/app/models/easy_flow/steps/question.rb +2 -1
- data/app/views/easy_flow/steps/_choosing.html.erb +2 -1
- data/app/views/easy_flow/steps/_picture.html.erb +4 -0
- data/app/views/easy_flow/steps/_ticking.html.erb +2 -1
- data/app/views/easy_flow/steps/_typing.html.erb +2 -1
- data/config/routes.rb +1 -0
- data/lib/easy_flow/version.rb +1 -1
- data/lib/easy_flow.rb +13 -2
- data/the_local/agents/easy_flow-info.md +1 -1
- data/the_local/agents/easy_flow-install.md +11 -0
- data/the_local/interface.yml +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 716b3bb1b37acda25c8915e40725db11b6bd9734dca76165aa22edebcb355263
|
|
4
|
+
data.tar.gz: 29f457f14f8f03d4cd9f7d60ff50cde7ca803501ec87fba000badc41cbf196a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
@@ -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>
|
|
@@ -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
data/lib/easy_flow/version.rb
CHANGED
data/lib/easy_flow.rb
CHANGED
|
@@ -7,7 +7,7 @@ require "easy_flow/host_routes"
|
|
|
7
7
|
require "easy_flow/engine"
|
|
8
8
|
|
|
9
9
|
module EasyFlow
|
|
10
|
-
FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list flow 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.
|
data/the_local/interface.yml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_flow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.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
|