alembic 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -22
  3. data/app/assets/builds/alembic/page_builder.css +1 -1
  4. data/app/assets/builds/alembic/page_builder.js +3 -3
  5. data/app/controllers/alembic/application_controller.rb +0 -16
  6. data/app/controllers/alembic/flows_controller.rb +6 -90
  7. data/app/controllers/alembic/manage/base_controller.rb +1 -0
  8. data/app/controllers/alembic/manage/flows_controller.rb +7 -44
  9. data/app/controllers/alembic/manage/pages_controller.rb +8 -1
  10. data/app/controllers/alembic/manage/previews_controller.rb +4 -19
  11. data/app/controllers/alembic/pages_controller.rb +19 -0
  12. data/app/controllers/concerns/alembic/summarizes.rb +38 -0
  13. data/app/helpers/alembic/pages_helper.rb +39 -0
  14. data/app/models/alembic/flow/definition_summary.rb +9 -0
  15. data/app/models/alembic/flow/run_summary.rb +10 -0
  16. data/app/models/alembic/flow/summaries.rb +36 -5
  17. data/app/models/alembic/flow/summary_version.rb +1 -1
  18. data/app/models/alembic/outputs/grouped.rb +1 -1
  19. data/app/models/alembic/outputs/percentage.rb +1 -1
  20. data/app/models/alembic/outputs/weighted_sum.rb +1 -1
  21. data/app/models/alembic/page/drawing.rb +17 -0
  22. data/app/models/alembic/page/options.rb +46 -0
  23. data/app/models/alembic/page/version.rb +13 -0
  24. data/app/models/alembic/page.rb +32 -0
  25. data/app/views/alembic/flows/complete.html.erb +3 -3
  26. data/app/views/alembic/flows/show.html.erb +6 -6
  27. data/app/views/alembic/manage/flows/edit.html.erb +10 -10
  28. data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
  29. data/app/views/alembic/manage/pages/index.html.erb +4 -1
  30. data/app/views/alembic/manage/pages/show.html.erb +7 -1
  31. data/app/views/alembic/pages/show.html.erb +5 -0
  32. data/config/routes.rb +8 -19
  33. data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +21 -1
  34. data/db/migrate/20260922120000_add_slug_to_alembic_pages.rb +6 -0
  35. data/db/migrate/20260922130000_create_alembic_page_versions.rb +16 -0
  36. data/db/migrate/20260924220000_move_flows_onto_easy_flow.rb +166 -0
  37. data/lib/alembic/engine.rb +2 -11
  38. data/lib/alembic/version.rb +1 -1
  39. data/lib/alembic.rb +30 -6
  40. metadata +35 -71
  41. data/app/assets/builds/alembic/canvas.js +0 -1
  42. data/app/controllers/alembic/manage/canvas_controller.rb +0 -183
  43. data/app/controllers/alembic/manage/definitions_controller.rb +0 -15
  44. data/app/controllers/alembic/manage/versions_controller.rb +0 -22
  45. data/app/controllers/concerns/alembic/manage/draws_canvas.rb +0 -35
  46. data/app/javascript/alembic/canvas/Canvas.jsx +0 -197
  47. data/app/javascript/alembic/canvas/Connector.jsx +0 -30
  48. data/app/javascript/alembic/canvas/ConnectorLayer.jsx +0 -31
  49. data/app/javascript/alembic/canvas/Control.jsx +0 -42
  50. data/app/javascript/alembic/canvas/Inspector.jsx +0 -44
  51. data/app/javascript/alembic/canvas/Panel.jsx +0 -67
  52. data/app/javascript/alembic/canvas/Placeholder.jsx +0 -24
  53. data/app/javascript/alembic/canvas/Port.jsx +0 -16
  54. data/app/javascript/alembic/canvas/Records.jsx +0 -34
  55. data/app/javascript/alembic/canvas/StepCard.jsx +0 -57
  56. data/app/javascript/alembic/canvas/Toolbar.jsx +0 -19
  57. data/app/javascript/alembic/canvas/TypePicker.jsx +0 -21
  58. data/app/javascript/alembic/canvas/changes.js +0 -5
  59. data/app/javascript/alembic/canvas/choices.js +0 -4
  60. data/app/javascript/alembic/canvas/flow.js +0 -7
  61. data/app/javascript/alembic/canvas/flowSender.js +0 -22
  62. data/app/javascript/alembic/canvas/ids.js +0 -7
  63. data/app/javascript/alembic/canvas/rows.js +0 -2
  64. data/app/javascript/alembic/canvas/styles.js +0 -3
  65. data/app/javascript/alembic/canvas/useConnectors.js +0 -75
  66. data/app/javascript/alembic/canvas/useFlow.js +0 -17
  67. data/app/javascript/alembic/canvas.jsx +0 -13
  68. data/app/models/alembic/asked.rb +0 -3
  69. data/app/models/alembic/choice.rb +0 -7
  70. data/app/models/alembic/flow/admission.rb +0 -19
  71. data/app/models/alembic/flow/canvas.rb +0 -167
  72. data/app/models/alembic/flow/change.rb +0 -18
  73. data/app/models/alembic/flow/condition.rb +0 -24
  74. data/app/models/alembic/flow/definition.rb +0 -132
  75. data/app/models/alembic/flow/digest.rb +0 -116
  76. data/app/models/alembic/flow/document.rb +0 -131
  77. data/app/models/alembic/flow/drawing.rb +0 -15
  78. data/app/models/alembic/flow/edge.rb +0 -9
  79. data/app/models/alembic/flow/edit_history.rb +0 -75
  80. data/app/models/alembic/flow/layout.rb +0 -88
  81. data/app/models/alembic/flow/name.rb +0 -15
  82. data/app/models/alembic/flow/node.rb +0 -5
  83. data/app/models/alembic/flow/output.rb +0 -23
  84. data/app/models/alembic/flow/progress/kept.rb +0 -38
  85. data/app/models/alembic/flow/progress/loose.rb +0 -43
  86. data/app/models/alembic/flow/progress.rb +0 -11
  87. data/app/models/alembic/flow/registry.rb +0 -25
  88. data/app/models/alembic/flow/run.rb +0 -54
  89. data/app/models/alembic/flow/runner.rb +0 -65
  90. data/app/models/alembic/flow/settings.rb +0 -80
  91. data/app/models/alembic/flow/start.rb +0 -13
  92. data/app/models/alembic/flow/step.rb +0 -42
  93. data/app/models/alembic/flow/step_type/declaration.rb +0 -118
  94. data/app/models/alembic/flow/step_type.rb +0 -69
  95. data/app/models/alembic/flow/switch.rb +0 -17
  96. data/app/models/alembic/flow/terminal.rb +0 -13
  97. data/app/models/alembic/flow/validator.rb +0 -171
  98. data/app/models/alembic/flow/version.rb +0 -26
  99. data/app/models/alembic/flow/violation.rb +0 -9
  100. data/app/models/alembic/flow.rb +0 -44
  101. data/app/models/alembic/runner.rb +0 -13
  102. data/app/models/alembic/steps/question.rb +0 -52
  103. data/app/views/alembic/flow/steps/_choosing.html.erb +0 -9
  104. data/app/views/alembic/flows/step.html.erb +0 -27
  105. data/app/views/alembic/manage/definitions/edit.html.erb +0 -13
  106. data/app/views/alembic/manage/flows/index.html.erb +0 -32
  107. data/app/views/alembic/manage/flows/show.html.erb +0 -26
  108. data/app/views/alembic/manage/versions/index.html.erb +0 -41
@@ -1,67 +0,0 @@
1
- import React from "react"
2
- import KeystonePanel from "keystone_ui-react/src/Panel.jsx"
3
- import Button from "keystone_ui-react/src/Button.jsx"
4
- import Input from "keystone_ui-react/src/Input.jsx"
5
- import Textarea from "keystone_ui-react/src/Textarea.jsx"
6
- import { Label } from "keystone_ui-react/src/FieldText.jsx"
7
- import { worded } from "./changes"
8
- import { standing } from "./flow"
9
-
10
- const sheet = { width: 280, padding: 20, overflowY: "auto" }
11
- const heading = { fontWeight: 600, marginBottom: 6, marginTop: 14 }
12
- const item = { fontSize: 12, marginBottom: 4, lineHeight: 1.4 }
13
- const caption = { display: "block", marginBottom: 3 }
14
-
15
- const settling = (flow, name, onSaveDetails) => (event) => {
16
- if (event.target.value !== (flow[name] ?? "")) onSaveDetails({ [name]: event.target.value })
17
- }
18
-
19
- const Panel = ({ flow, changes, problems, refusal, notice, onCreate, onPublish, onSaveDetails, onClose }) => (
20
- <KeystonePanel as="aside" style={sheet} data-builder-panel>
21
- <div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 8 }}>
22
- <h2 style={{ fontWeight: 600 }} data-flow-name>{flow.title || flow.slug}</h2>
23
- <button title="Close" onClick={onClose} className="text-gray-500 dark:text-gray-400"
24
- style={{ border: "none", background: "none", cursor: "pointer", fontSize: 18, lineHeight: 1 }}>×</button>
25
- </div>
26
- <a className="text-accent-600 dark:text-accent-400" style={{ fontSize: 12, display: "block" }} href={flow.history_url} data-history>{standing(flow)}</a>
27
-
28
- <h2 style={heading}>Problems</h2>
29
- {refusal && <p className="text-red-800 dark:text-red-400" style={{ ...item, fontWeight: 600 }} data-refusal>{refusal}</p>}
30
- {notice && !refusal && <p className="text-emerald-800 dark:text-emerald-400" style={{ ...item, fontWeight: 600 }} data-notice>{notice}</p>}
31
- {problems.length === 0 && !refusal
32
- ? <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 12 }}>Nothing wrong with this flow.</p>
33
- : problems.map((problem) => (
34
- <p key={`${problem.node}-${problem.problem}`} className="text-amber-700 dark:text-amber-400" style={item} data-problem>⚠ {worded(problem)}</p>
35
- ))}
36
-
37
- <h2 style={heading}>Changes since the last version</h2>
38
- {changes.length === 0
39
- ? <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 12 }}>Nothing has changed.</p>
40
- : changes.map((change, at) => <p key={at} style={item} data-change>{change}</p>)}
41
-
42
- <h2 style={heading}>Details</h2>
43
- <Label>
44
- <span style={caption}>Title</span>
45
- <Input className="mb-3" defaultValue={flow.title ?? ""} onBlur={settling(flow, "title", onSaveDetails)} data-flow-title />
46
- </Label>
47
-
48
- <Label>
49
- <span style={caption}>Summary</span>
50
- <Textarea className="mb-3" rows={4} defaultValue={flow.summary ?? ""} onBlur={settling(flow, "summary", onSaveDetails)} data-flow-summary />
51
- </Label>
52
-
53
- <Label>
54
- <span style={caption}>Start label</span>
55
- <Input className="mb-3" defaultValue={flow.start_label ?? ""} onBlur={settling(flow, "start_label", onSaveDetails)} data-flow-start-label />
56
- </Label>
57
-
58
- <div style={{ marginTop: 16 }}>
59
- <Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onCreate} data-create-version>Create version</Button>
60
- <Button size="sm" className="w-full mb-1.5" onClick={onPublish} data-publish>Publish</Button>
61
- <Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.definition_url} data-definition>Definition</Button>
62
- <Button variant="secondary" size="sm" className="w-full mb-1.5" href={flow.details_url} data-details>Edit details</Button>
63
- </div>
64
- </KeystonePanel>
65
- )
66
-
67
- export default Panel
@@ -1,24 +0,0 @@
1
- import React from "react"
2
- import { CARD } from "./styles"
3
-
4
- const waiting = {
5
- width: CARD, boxSizing: "border-box", padding: "10px 14px", borderRadius: 8, textAlign: "center", fontSize: 12, cursor: "pointer"
6
- }
7
-
8
- const open = "border-2 border-dashed border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
9
- const receiving = "border-2 border-dashed border-accent-600 bg-accent-50 text-accent-800 dark:bg-accent-950 dark:text-accent-200"
10
-
11
- const Placeholder = ({ node, dragging, onFill, onDrop }) => (
12
- <div ref={node.ref} data-placeholder={node.id}
13
- title={`Choose the step “${node.label}” should lead to`}
14
- onClick={onFill}
15
- onDragOver={(event) => { if (dragging) { event.preventDefault(); event.dataTransfer.dropEffect = "move" } }}
16
- onDrop={(event) => { event.preventDefault(); onDrop() }}
17
- className={dragging ? receiving : open}
18
- style={waiting}>
19
- <div style={{ fontWeight: 600 }}>{node.label}</div>
20
- <div style={{ fontSize: 11 }}>{dragging ? "drop a step here" : "leads nowhere yet — click to choose"}</div>
21
- </div>
22
- )
23
-
24
- export default Placeholder
@@ -1,16 +0,0 @@
1
- import React from "react"
2
-
3
- const open = "border border-amber-500 bg-white text-amber-700 dark:bg-zinc-900 dark:text-amber-400"
4
- const joined = "border border-gray-300 bg-white text-gray-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-gray-400"
5
- const choosing = "border border-accent-600 bg-accent-600 text-white"
6
-
7
- const Port = ({ name, connected, armed, onArm, connecting }) => (
8
- <button onClick={(event) => { if (connecting) return; event.stopPropagation(); onArm(event) }}
9
- title={armed ? "Now choose a step to connect to" : "Connect this branch"}
10
- className={armed ? choosing : connected ? joined : open}
11
- style={{
12
- padding: "1px 8px", marginRight: 4, borderRadius: 999, fontSize: 11, cursor: "pointer"
13
- }}>{name || "next"}</button>
14
- )
15
-
16
- export default Port
@@ -1,34 +0,0 @@
1
- import React from "react"
2
- import Control from "./Control"
3
- import { amended } from "./rows"
4
- import Button from "keystone_ui-react/src/Button.jsx"
5
-
6
- const Records = ({ holds, labels, rows, onChange, onSettle }) => {
7
- const kept = Array.isArray(rows) ? rows : []
8
- const amend = (index, name, next, settle) => {
9
- const updated = amended(kept, index, name, next)
10
- settle ? onSettle(updated) : onChange(updated)
11
- }
12
-
13
- return (
14
- <div style={{ marginBottom: 12 }}>
15
- {kept.map((row, index) => (
16
- <div key={index} className="rounded-md border border-gray-200 dark:border-zinc-700" style={{ padding: "8px 8px 2px", marginBottom: 6 }}>
17
- {Object.entries(holds).map(([ name, type ]) => (
18
- <label key={name} style={{ display: "block" }}>
19
- <span className="text-gray-500 dark:text-gray-400" style={{ display: "block", marginBottom: 2, fontSize: 11 }}>{(labels || {})[name] || name}</span>
20
- <Control type={type} value={row[name]}
21
- onChange={(next) => amend(index, name, next, false)}
22
- onSettle={(next) => amend(index, name, next, true)} />
23
- </label>
24
- ))}
25
- <Button variant="secondary" size="sm" className="mb-1.5"
26
- onClick={() => onSettle(kept.filter((_, at) => at !== index))}>Remove</Button>
27
- </div>
28
- ))}
29
- <Button variant="secondary" size="sm" className="w-full" onClick={() => onSettle([ ...kept, {} ])}>Add</Button>
30
- </div>
31
- )
32
- }
33
-
34
- export default Records
@@ -1,57 +0,0 @@
1
- import React from "react"
2
- import Port from "./Port"
3
- import { CARD } from "./styles"
4
-
5
- const card = { width: CARD, boxSizing: "border-box", borderRadius: 8 }
6
-
7
- const bookendCard = { padding: "8px 14px", textAlign: "center", fontWeight: 600 }
8
-
9
- const named = { fontWeight: 600, lineHeight: 1.3 }
10
-
11
- const edge = (target, troubled, selected) => {
12
- const lift = selected ? "ring-4 ring-accent-600/15" : "shadow-sm"
13
- if (target || (selected && !troubled)) return `border-accent-600 ${lift}`
14
- if (troubled) return `border-red-600 ${lift}`
15
- return "border-gray-300 shadow-sm dark:border-zinc-700"
16
- }
17
-
18
- const StepCard = ({ node, selected, armed, connecting, onSelect, onArm, onDragEnd, onDragStart }) => {
19
- const bookend = node.begins_here || node.ends_here
20
- const pinned = node.begins_here
21
- const ports = node.ends_here ? [] : node.ports
22
- const target = connecting && !pinned
23
-
24
- return (
25
- <div ref={node.ref}
26
- data-step={node.id}
27
- draggable={!pinned}
28
- onDragStart={(event) => { event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", node.id); onDragStart() }}
29
- onDragEnd={onDragEnd}
30
- onClick={onSelect}
31
- className={`bg-white border-2 dark:bg-zinc-900 ${edge(target, node.violations.length > 0, selected)}`}
32
- style={{
33
- ...card,
34
- padding: bookend ? 0 : "12px 14px",
35
- cursor: pinned ? "default" : connecting ? "crosshair" : "grab"
36
- }}>
37
- {bookend && <div className="text-gray-500 dark:text-gray-400" style={bookendCard}>{node.label}</div>}
38
- {!bookend && <div style={named}>{node.label}</div>}
39
- {!bookend && <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginTop: 2 }}>{node.type}</div>}
40
- {node.violations.map((violation) => (
41
- <div key={violation.problem + violation.detail} className="text-red-600 dark:text-red-400" style={{ fontSize: 11, padding: "0 14px 6px" }}>
42
- {violation.problem.replace(/_/g, " ")}{violation.detail ? `: ${violation.detail}` : ""}
43
- </div>
44
- ))}
45
- {ports.length > 0 && (
46
- <div style={{ padding: node.begins_here ? "0 14px 8px" : "10px 0 0" }}>
47
- {ports.map((port) => (
48
- <Port key={port} name={port} connected={node.connected.includes(port)}
49
- connecting={connecting} armed={armed === port} onArm={(event) => onArm(port, event)} />
50
- ))}
51
- </div>
52
- )}
53
- </div>
54
- )
55
- }
56
-
57
- export default StepCard
@@ -1,19 +0,0 @@
1
- import React from "react"
2
- import Button from "keystone_ui-react/src/Button.jsx"
3
-
4
- const round = { width: 24, height: 24, padding: 0, fontSize: 15, lineHeight: "15px" }
5
-
6
- const Stepper = ({ label, title, idle, enabled, onUse }) => (
7
- <Button variant="secondary" size="sm" title={enabled ? title : idle} disabled={!enabled} onClick={onUse}
8
- style={{ opacity: enabled ? 1 : 0.4, cursor: enabled ? "pointer" : "default" }}>{label}</Button>
9
- )
10
-
11
- const Toolbar = ({ undoable, redoable, onAdd, onUndo, onRedo }) => (
12
- <div style={{ position: "absolute", top: 16, left: 16, zIndex: 5, display: "flex", gap: 8 }}>
13
- <Button variant="secondary" className="rounded-full" style={round} title="Add a step" onClick={onAdd}>+</Button>
14
- <Stepper label="↶ Undo" title="Undo the last change" idle="Nothing to undo" enabled={undoable} onUse={onUndo} />
15
- <Stepper label="↷ Redo" title="Redo the change you undid" idle="Nothing to redo" enabled={redoable} onUse={onRedo} />
16
- </div>
17
- )
18
-
19
- export default Toolbar
@@ -1,21 +0,0 @@
1
- import React from "react"
2
- import Panel from "keystone_ui-react/src/Panel.jsx"
3
- import Button from "keystone_ui-react/src/Button.jsx"
4
-
5
- const TypePicker = ({ entries, at, onPick, onConnect, onDismiss }) => (
6
- <Panel className="shadow-lg" style={{ position: "absolute", zIndex: 9, top: at.y, left: at.x, width: 210, padding: 8, borderRadius: 8 }}>
7
- <p className="text-gray-500 dark:text-gray-400" style={{ margin: "0 0 6px", fontSize: 11 }}>Add a step</p>
8
- {entries.map((entry) => (
9
- <Button key={entry.type} variant="secondary" size="sm" className="w-full mb-1.5 flex-col" onClick={() => onPick(entry)}>
10
- {entry.label}
11
- <span className="opacity-75" style={{ display: "block", fontSize: 11 }}>{entry.type}</span>
12
- </Button>
13
- ))}
14
- {onConnect && (
15
- <Button variant="secondary" size="sm" className="w-full mb-1.5" onClick={onConnect}>Connect to a step already here</Button>
16
- )}
17
- <Button variant="secondary" size="sm" className="w-full" onClick={onDismiss}>Cancel</Button>
18
- </Panel>
19
- )
20
-
21
- export default TypePicker
@@ -1,5 +0,0 @@
1
- export const worded = (violation) => {
2
- const problem = violation.problem.replace(/_/g, " ")
3
-
4
- return violation.node ? `“${violation.node}” ${problem}` : problem
5
- }
@@ -1,4 +0,0 @@
1
- export const toggled = (chosen, choice) =>
2
- chosen.includes(choice) ? chosen.filter((each) => each !== choice) : [ ...chosen, choice ]
3
-
4
- export const offeredTo = (node, entry) => ({ ...(entry?.choices || {}), ...(node?.choices || {}) })
@@ -1,7 +0,0 @@
1
- export const standing = ({ version, published }) => {
2
- if (!version) return "No version yet"
3
- if (!published) return `Version ${version} · never published`
4
- if (published === version) return `Version ${version} · live`
5
-
6
- return `Version ${version} · visitors still on ${published}`
7
- }
@@ -1,22 +0,0 @@
1
- const UNREACHABLE = "Your change was not saved because the server could not be reached."
2
-
3
- export const createFlowSender = ({ base, token, fetch, onFlow, onError, onNotice }) => async (path, method, body) => {
4
- const response = await fetch(base + path, {
5
- method, headers: { "Content-Type": "application/json", "X-CSRF-Token": token }, body: body && JSON.stringify(body)
6
- }).catch(() => null)
7
- if (!response) {
8
- onNotice(null)
9
- return onError(UNREACHABLE)
10
- }
11
-
12
- const answered = await response.json().catch(() => ({}))
13
-
14
- if (!response.ok) {
15
- onNotice(null)
16
- return onError(answered.error || "That change was refused")
17
- }
18
-
19
- onError(null)
20
- onNotice(answered.notice || null)
21
- onFlow(await (await fetch(base + ".json", { headers: { Accept: "application/json" } })).json())
22
- }
@@ -1,7 +0,0 @@
1
- export const nextId = (type, taken) => {
2
- const used = new Set(taken)
3
- let candidate = type
4
- let suffix = 2
5
- while (used.has(candidate)) candidate = `${type}_${suffix++}`
6
- return candidate
7
- }
@@ -1,2 +0,0 @@
1
- export const amended = (rows, index, name, next) =>
2
- rows.map((row, at) => (at === index ? { ...row, [name]: next } : row))
@@ -1,3 +0,0 @@
1
- export const CARD = 240
2
- export const GAP_X = 40
3
- export const GAP_Y = 64
@@ -1,75 +0,0 @@
1
- import { useLayoutEffect, useState } from "react"
2
-
3
- export const anchor = (box, side, frame, surface) => {
4
- const left = box.left - frame.left + surface.scrollLeft
5
- const top = box.top - frame.top + surface.scrollTop
6
-
7
- if (side === "left") return { x: left, y: top + box.height / 2 }
8
- if (side === "right") return { x: left + box.width, y: top + box.height / 2 }
9
- if (side === "top") return { x: left + box.width / 2, y: top }
10
- return { x: left + box.width / 2, y: top + box.height }
11
- }
12
-
13
- export const shy = (point, side) => {
14
- if (side === "top") return { x: point.x, y: point.y - 3 }
15
- if (side === "left") return { x: point.x - 3, y: point.y }
16
- if (side === "right") return { x: point.x + 3, y: point.y }
17
- return { x: point.x, y: point.y + 3 }
18
- }
19
-
20
- export const drawn = (edge, a, b, lane, detour) => {
21
- const path = {
22
- straight: `M ${a.x} ${a.y} L ${b.x} ${b.y}`,
23
- turn: `M ${a.x} ${a.y} H ${b.x} V ${b.y}`,
24
- lane: `M ${a.x} ${a.y} V ${lane} H ${b.x} V ${b.y}`,
25
- detour: `M ${a.x} ${a.y} H ${detour} V ${b.y} H ${b.x}`
26
- }[edge.route]
27
-
28
- return {
29
- ...edge, path,
30
- midX: edge.route === "detour" ? detour : (a.x + b.x) / 2,
31
- midY: edge.route === "lane" ? lane : (a.y + b.y) / 2
32
- }
33
- }
34
-
35
- const useConnectors = (flow, surface, cards, watching) => {
36
- const [ links, setLinks ] = useState([])
37
- const [ extent, setExtent ] = useState({ width: "100%", height: "100%" })
38
-
39
- useLayoutEffect(() => {
40
- const measure = () => {
41
- const frame = surface.current?.getBoundingClientRect()
42
- if (!frame) return
43
-
44
- setExtent({ width: surface.current.scrollWidth, height: surface.current.scrollHeight })
45
-
46
- setLinks(flow.edges.flatMap((edge) => {
47
- const fromBox = cards.current[edge.source]?.getBoundingClientRect()
48
- const toBox = cards.current[edge.target]?.getBoundingClientRect()
49
- if (!fromBox || !toBox) return []
50
-
51
- const a = anchor(fromBox, edge.leaves, frame, surface.current)
52
- const b = shy(anchor(toBox, edge.enters, frame, surface.current), edge.enters)
53
- const lane = (anchor(fromBox, "bottom", frame, surface.current).y + anchor(toBox, "top", frame, surface.current).y) / 2
54
- const detour = edge.leaves === "left" ? Math.min(a.x, b.x) - 28 : Math.max(a.x, b.x) + 28
55
-
56
- return [ drawn(edge, a, b, lane, detour) ]
57
- }))
58
- }
59
-
60
- measure()
61
- window.addEventListener("resize", measure)
62
-
63
- const watcher = new ResizeObserver(measure)
64
- if (surface.current) watcher.observe(surface.current)
65
-
66
- return () => {
67
- window.removeEventListener("resize", measure)
68
- watcher.disconnect()
69
- }
70
- }, watching)
71
-
72
- return { links, extent }
73
- }
74
-
75
- export default useConnectors
@@ -1,17 +0,0 @@
1
- import { useMemo, useState } from "react"
2
- import { createFlowSender } from "./flowSender"
3
-
4
- const useFlow = (base, token, initial) => {
5
- const [ flow, setFlow ] = useState(initial)
6
- const [ error, setError ] = useState(null)
7
- const [ notice, setNotice ] = useState(null)
8
-
9
- const send = useMemo(
10
- () => createFlowSender({ base, token, fetch: (...request) => window.fetch(...request), onFlow: setFlow, onError: setError, onNotice: setNotice }),
11
- [ base, token ]
12
- )
13
-
14
- return { flow, error, notice, send }
15
- }
16
-
17
- export default useFlow
@@ -1,13 +0,0 @@
1
- import { createRoot } from "react-dom/client"
2
- import { register } from "keystone_ui-react/src/registry.js"
3
- import { startMounting } from "keystone_ui-react/src/mounting.js"
4
- import Canvas from "./canvas/Canvas"
5
-
6
- register("alembic/flow-editor", Canvas)
7
-
8
- document.addEventListener("alembic:flow-named", (event) => {
9
- const heading = document.querySelector("[data-flow-heading]")
10
- if (heading) heading.textContent = event.detail
11
- })
12
-
13
- startMounting(document, createRoot)
@@ -1,3 +0,0 @@
1
- module Alembic
2
- Asked = Data.define(:id, :text, :choices)
3
- end
@@ -1,7 +0,0 @@
1
- module Alembic
2
- Choice = Data.define(:value, :label, :hint) do
3
- def initialize(value:, label: nil, hint: nil)
4
- super(value: value, label: label.presence || value, hint: hint)
5
- end
6
- end
7
- end
@@ -1,19 +0,0 @@
1
- module Alembic
2
- module Flow
3
- module Admission
4
- def self.of(diagnostic, permitted:)
5
- raise NotPublished if diagnostic.nil? || diagnostic.live_definition.blank?
6
- raise NotPermitted unless permitted && diagnostic.available?
7
-
8
- diagnostic
9
- end
10
-
11
- def self.of_run(run, permitted: true)
12
- raise Withdrawn if run.definition_version.withdrawn?
13
- raise NotPermitted unless permitted && run.flow.available?
14
-
15
- run
16
- end
17
- end
18
- end
19
- end
@@ -1,167 +0,0 @@
1
- module Alembic
2
- module Flow
3
- class Canvas
4
- def initialize(document, registry: Flow.registry)
5
- @document = document
6
- @registry = registry
7
- end
8
-
9
- def to_h
10
- { "nodes" => nodes, "edges" => edges, "palette" => palette, "violations" => violations }
11
- end
12
-
13
- private
14
-
15
- def nodes
16
- @document.nodes.map { |node| drawn_step(node) } + waiting.map { |gap| drawn_placeholder(gap) }
17
- end
18
-
19
- def drawn_step(node)
20
- { "id" => node.id, "type" => node.type, "label" => label_for(node),
21
- "config" => node.config, "ports" => [], "ends_here" => ends_here?(node), "begins_here" => begins_here?(node),
22
- "loose" => loose?(node), "choices" => choices_for(node), "placeholder" => false, **placed[node.id] }
23
- end
24
-
25
- def drawn_placeholder(gap)
26
- { "id" => gap[:id], "type" => "placeholder", "label" => gap[:on], "config" => {}, "ports" => [],
27
- "ends_here" => false, "begins_here" => false, "loose" => false, "choices" => {},
28
- "placeholder" => true, "from" => gap[:from], "on" => gap[:on], **placed[gap[:id]] }
29
- end
30
-
31
- def edges
32
- drawn.edges.each_with_index.map do |edge, index|
33
- { "id" => "#{edge.from}-#{edge.to}-#{index}", "source" => edge.from, "target" => edge.to, "label" => edge.on&.to_s,
34
- "placeholder" => waiting.any? { |gap| gap[:id] == edge.to } }.merge(routing(edge, placed))
35
- end
36
- end
37
-
38
- def placed
39
- @placed ||= Layout.new(drawn).positions
40
- end
41
-
42
- def drawn
43
- @drawn ||= Document.new(@document.to_h.merge(
44
- "nodes" => Array(@document.to_h["nodes"]) + waiting.map { |gap| { "id" => gap[:id], "type" => "placeholder" } },
45
- "edges" => Array(@document.to_h["edges"]) + waiting.map { |gap| { "from" => gap[:from], "to" => gap[:id], "on" => gap[:on] } }
46
- ), registry: @registry)
47
- end
48
-
49
- def waiting
50
- @waiting ||= @document.nodes.flat_map { |node| gaps_after(node) }
51
- end
52
-
53
- def gaps_after(node)
54
- wired = @document.edges_from(node.id).map { |edge| edge.on.to_s }
55
- results = digest.routing_values(node.id)
56
- return [ { id: "#{node.id}--", from: node.id, on: nil } ] if results.empty? && leads_nowhere?(node)
57
-
58
- results.reject { |value| wired.include?(value) }
59
- .map { |value| { id: "#{node.id}--#{value}", from: node.id, on: value } }
60
- end
61
-
62
- def leads_nowhere?(node)
63
- !ends_here?(node) && !loose?(node) && @document.edges_from(node.id).none?
64
- end
65
-
66
- def routing(edge, placed)
67
- from = placed[edge.from]
68
- to = placed[edge.to]
69
- return { "leaves" => "bottom", "enters" => "top", "route" => "straight" } unless from && to
70
-
71
- return alongside_routing(from, to) unless to["row"] > from["row"]
72
-
73
- { "leaves" => "bottom", "enters" => "top",
74
- "route" => to["column"] == from["column"] ? "straight" : "lane" }
75
- end
76
-
77
- def alongside_routing(from, to)
78
- leftward = to["column"] < from["column"]
79
-
80
- { "leaves" => leftward ? "left" : "right", "enters" => leftward ? "right" : "left",
81
- "route" => to["row"] == from["row"] ? "straight" : "detour" }
82
- end
83
-
84
- def palette
85
- @registry.step_types.reject(&:begins_here?).map do |step_type|
86
- { "type" => step_type.id.to_s, "label" => step_type.step_name,
87
- "fields" => step_type.settings.fields.transform_keys(&:to_s).transform_values(&:to_s),
88
- "labels" => step_type.settings.labels.transform_keys(&:to_s),
89
- "choices" => step_type.settings.choices.transform_keys(&:to_s),
90
- "records" => holdings_of(step_type),
91
- "record_labels" => step_type.settings.record_labels.to_h { |name, held| [ name.to_s, held.transform_keys(&:to_s) ] },
92
- "awaits_input" => step_type.awaits_input? }
93
- end
94
- end
95
-
96
- def holdings_of(step_type)
97
- step_type.settings.record_fields.to_h do |name, holds|
98
- [ name.to_s, holds.transform_keys(&:to_s).transform_values(&:to_s) ]
99
- end
100
- end
101
-
102
- def violations
103
- Validator.new(@document, registry: @registry).violations.map do |violation|
104
- { "node" => violation.node, "problem" => violation.problem.to_s, "detail" => violation.detail }
105
- end
106
- end
107
-
108
- def label_for(node)
109
- Name.of(node, @registry)
110
- end
111
-
112
- def choices_for(node)
113
- naming_steps(node).index_with { earlier_than(node) }.merge(drawn_by(node)).merge(named_outputs_by(node))
114
- end
115
-
116
- def named_outputs_by(node)
117
- step_type_for(node)&.settings&.outputs_of.to_h.to_h do |name, source|
118
- [ name.to_s, digest.outputs_of(node.config[source.to_s]) ]
119
- end
120
- end
121
-
122
- def drawn_by(node)
123
- step_type_for(node)&.settings&.drawn_from.to_h.to_h do |name, source|
124
- [ name.to_s, digest.values_of(step_named_by(node, source), node.config[source.to_s]) ]
125
- end
126
- end
127
-
128
- def step_named_by(node, source)
129
- node.config[step_type_for(node).settings.outputs_of[source].to_s]
130
- end
131
-
132
- def digest
133
- Digest.new(@document, registry: @registry)
134
- end
135
-
136
- def naming_steps(node)
137
- step_type_for(node)&.settings&.naming_steps.to_a.map(&:to_s)
138
- end
139
-
140
- def earlier_than(node)
141
- digest.preceding(node.id).map do |id|
142
- { "value" => id, "label" => label_for(@document.node(id)) }
143
- end
144
- end
145
-
146
- def ends_here?(node)
147
- step_type_for(node)&.ends_here? || false
148
- end
149
-
150
- def loose?(node)
151
- @document.entry.present? && @document.reachable.exclude?(node.id)
152
- end
153
-
154
- def begins_here?(node)
155
- step_type_for(node)&.begins_here? || false
156
- end
157
-
158
- def ports_for(node)
159
- digest.routing_values(node.id)
160
- end
161
-
162
- def step_type_for(node)
163
- @registry.fetch(node.type) if @registry.registered?(node.type)
164
- end
165
- end
166
- end
167
- end
@@ -1,18 +0,0 @@
1
- module Alembic
2
- module Flow
3
- module Change
4
- PHRASING = { "added" => "Added", "updated" => "Updated", "removed" => "Removed",
5
- "moved" => "Moved", "connected" => "Connected", "disconnected" => "Disconnected", "returned" => "Returned to" }.freeze
6
-
7
- def self.phrase(change)
8
- named = Array(change["named"]).compact_blank.map { |name| "“#{name}”" }
9
- verb = PHRASING.fetch(change["action"], change["action"])
10
-
11
- return "#{verb} #{named.join(' → ')}" if named.any?
12
- return "#{verb} #{change['detail']}" if change["detail"].present?
13
-
14
- verb
15
- end
16
- end
17
- end
18
- end