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,183 +0,0 @@
1
- module Alembic
2
- module Manage
3
- class CanvasController < BaseController
4
- include DrawsCanvas
5
- def show
6
- render json: canvas_payload(diagnostic)
7
- end
8
-
9
- def create
10
- objections = Flow::Validator.new(document).violations
11
- return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
12
-
13
- stood_at = diagnostic.current_definition_version&.number
14
- diagnostic.create_version
15
-
16
- render json: { notice: created(stood_at) }
17
- end
18
-
19
- def publish
20
- objections = Flow::Validator.new(document).violations
21
- return render json: { error: refusal(objections) }, status: :unprocessable_entity if objections.any?
22
-
23
- ran = diagnostic.live_version&.number
24
- diagnostic.publish
25
-
26
- render json: { notice: published(ran) }
27
- end
28
-
29
- def details
30
- diagnostic.update!(details_params)
31
- render json: { notice: "Saved the flow's details." }
32
- end
33
-
34
- def undo
35
- diagnostic.edit_history.undo_change
36
- head :no_content
37
- end
38
-
39
- def redo
40
- diagnostic.edit_history.redo_change
41
- head :no_content
42
- end
43
-
44
- def add_step
45
- apply(:added, params[:id]) do |flow|
46
- next flow.insert(new_step, on: edge_endpoints, leaving: first_port) if placed_on_edge?
47
- next flow.add(new_step).connect(from: params[:from], to: params[:id], on: params[:on].presence) if branched_from?
48
-
49
- flow.add(new_step)
50
- end
51
- end
52
-
53
- def configure_step
54
- apply(:updated, params[:step]) { |flow| flow.configure(params[:step], coerced(flow, params[:step])) }
55
- end
56
-
57
- def move_step
58
- apply(:moved, params[:step]) { |flow| flow.move(params[:step], on: edge_endpoints, leaving: port_for(params[:step])) }
59
- end
60
-
61
- def remove_step
62
- return render json: { error: refusal_to_remove }, status: :unprocessable_entity if begins_the_flow?(params[:step])
63
-
64
- apply(:removed, params[:step]) { |flow| flow.remove(params[:step]) }
65
- end
66
-
67
- def connect
68
- apply(:connected, params[:from], params[:to]) { |flow| flow.connect(from: params[:from], to: params[:to], on: params[:on].presence) }
69
- end
70
-
71
- def disconnect
72
- apply(:disconnected, params[:from], params[:to]) { |flow| flow.disconnect(from: params[:from], to: params[:to]) }
73
- end
74
-
75
- private
76
-
77
- def apply(action = nil, *steps)
78
- before = document.to_h
79
- diagnostic.edit_history.record(yield(document), action, steps, before)
80
- head :no_content
81
- rescue Flow::InvalidEdit => invalid
82
- render json: { error: invalid.message }, status: :unprocessable_entity
83
- end
84
-
85
- def diagnostic
86
- @diagnostic ||= Flow::Definition.find(params[:flow_id])
87
- end
88
-
89
- def document
90
- Flow::Document.new(diagnostic.document || diagnostic.definition || {})
91
- end
92
-
93
- def details_params
94
- params.require(:flow).permit(:title, :summary, :start_label, :persists)
95
- end
96
-
97
- def new_step
98
- { "id" => params.require(:id), "type" => params.require(:type) }
99
- end
100
-
101
- def first_port
102
- first_value_of(Flow::Node.new(id: nil, type: params[:type], config: {}))
103
- end
104
-
105
- def port_for(id)
106
- first_value_of(document.node(id))
107
- end
108
-
109
- def first_value_of(node)
110
- return unless node && Flow.registry.registered?(node.type)
111
-
112
- step_type = Flow.registry.fetch(node.type)
113
- return unless step_type.routes?
114
-
115
- step_type.outputs.flat_map { |output| output.values_for(node) }.first&.fetch("value", nil)&.to_s
116
- end
117
-
118
- def begins_the_flow?(id)
119
- node = document.node(id)
120
-
121
- node.present? && document.begins_here?(node)
122
- end
123
-
124
- def placed_on_edge?
125
- params[:from].present? && params[:to].present?
126
- end
127
-
128
- def branched_from?
129
- params[:from].present?
130
- end
131
-
132
- def edge_endpoints
133
- [ params[:from], params[:to] ]
134
- end
135
-
136
- def configuration
137
- params.fetch(:config, {}).permit!.to_h
138
- end
139
-
140
- def created(stood_at)
141
- now_at = diagnostic.reload.current_definition_version&.number
142
- return "Nothing has changed since version #{stood_at}." if now_at == stood_at
143
-
144
- "Created version #{now_at}."
145
- end
146
-
147
- def published(ran)
148
- now_at = diagnostic.reload.live_version&.number
149
- return "Visitors already run version #{now_at}." if now_at == ran
150
-
151
- "Published version #{now_at}. Visitors run it now."
152
- end
153
-
154
- def refusal_to_remove
155
- "Cannot remove where a flow begins."
156
- end
157
-
158
- def refusal(objections)
159
- "Cannot publish: #{objections.map { |problem| worded(problem) }.join(', ')}."
160
- end
161
-
162
- def worded(problem)
163
- trouble = problem.problem.to_s.humanize(capitalize: false)
164
-
165
- problem.node ? "“#{problem.node}” is #{trouble}" : trouble
166
- end
167
-
168
- def coerced(flow, id)
169
- step_type = Flow::Digest.new(flow).step(id)&.type
170
- return configuration unless step_type && Flow.registry.registered?(step_type)
171
-
172
- settled(Flow.registry.fetch(step_type))
173
- end
174
-
175
- def settled(step_type)
176
- step_type.settings.coerce(configuration).tap do |values|
177
- objections = step_type.settings.objections(values)
178
- raise Flow::InvalidEdit, objections.join(", ") if objections.any?
179
- end
180
- end
181
- end
182
- end
183
- end
@@ -1,15 +0,0 @@
1
- module Alembic
2
- module Manage
3
- class DefinitionsController < BaseController
4
- def edit
5
- @diagnostic = Flow::Definition.find(params[:flow_id])
6
- end
7
-
8
- def update
9
- @diagnostic = Flow::Definition.find(params[:flow_id])
10
- @diagnostic.edit_history.edit_document(JSON.parse(params.require(:definition)))
11
- redirect_to manage_flow_path(@diagnostic), notice: "Definition saved."
12
- end
13
- end
14
- end
15
- end
@@ -1,22 +0,0 @@
1
- module Alembic
2
- module Manage
3
- class VersionsController < BaseController
4
- def index
5
- @diagnostic = diagnostic
6
- @versions = @diagnostic.definition_versions.order(number: :desc)
7
- end
8
-
9
- def return
10
- diagnostic.return_to(diagnostic.definition_versions.find(params[:id]))
11
-
12
- redirect_to manage_flow_path(diagnostic), notice: "The flow is back to that version."
13
- end
14
-
15
- private
16
-
17
- def diagnostic
18
- @diagnostic ||= Flow::Definition.find(params[:flow_id])
19
- end
20
- end
21
- end
22
- end
@@ -1,35 +0,0 @@
1
- module Alembic
2
- module Manage
3
- module DrawsCanvas
4
- extend ActiveSupport::Concern
5
-
6
- private
7
-
8
- def canvas_payload(diagnostic)
9
- Flow::Canvas.new(canvas_document(diagnostic)).to_h
10
- .merge("undoable" => diagnostic.edit_history.undoable?, "redoable" => diagnostic.edit_history.redoable?,
11
- "changes" => listed_changes(diagnostic), "flow" => flow_details(diagnostic))
12
- end
13
-
14
- def canvas_document(diagnostic)
15
- Flow::Document.new(diagnostic.document || diagnostic.definition || {})
16
- end
17
-
18
- def listed_changes(diagnostic)
19
- diagnostic.changes_since_version.to_a.map { |change| Flow::Change.phrase(change) }
20
- end
21
-
22
- def flow_details(diagnostic)
23
- { "title" => diagnostic.title,
24
- "slug" => diagnostic.slug,
25
- "summary" => diagnostic.summary,
26
- "start_label" => diagnostic.start_label,
27
- "version" => diagnostic.current_definition_version&.number,
28
- "published" => diagnostic.live_version&.number,
29
- "definition_url" => edit_manage_flow_definition_path(diagnostic),
30
- "details_url" => edit_manage_flow_path(diagnostic),
31
- "history_url" => manage_flow_versions_path(diagnostic) }
32
- end
33
- end
34
- end
35
- end
@@ -1,197 +0,0 @@
1
- import React, { useEffect, useMemo, useRef, useState } from "react"
2
- import StepCard from "./StepCard"
3
- import Inspector from "./Inspector"
4
- import Placeholder from "./Placeholder"
5
- import { offeredTo } from "./choices"
6
- import TypePicker from "./TypePicker"
7
- import Toolbar from "./Toolbar"
8
- import Panel from "./Panel"
9
- import ConnectorLayer from "./ConnectorLayer"
10
- import useFlow from "./useFlow"
11
- import useConnectors from "./useConnectors"
12
- import { CARD, GAP_X, GAP_Y } from "./styles"
13
- import { nextId } from "./ids"
14
-
15
- const page = { display: "flex", height: "100%", minHeight: 0, fontSize: 13 }
16
- const scroll = { flex: 1, overflow: "auto", position: "relative" }
17
- const waiting = { position: "absolute", top: 56, left: 16, zIndex: 5, width: CARD + 24, padding: 12, borderRadius: 8 }
18
-
19
- const grid = { display: "grid", rowGap: GAP_Y, columnGap: 0, padding: 40, justifyContent: "center", position: "relative" }
20
- const notice = { position: "sticky", zIndex: 8, top: 8, margin: "8px auto 0", width: "fit-content", padding: "6px 12px", borderRadius: 6, fontSize: 12 }
21
-
22
- export const Choosing = ({ port, onCancel }) => (
23
- <div className="bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-100" style={notice}>
24
- Choose the step “{port || "next"}” should lead to — <button onClick={onCancel} className="text-accent-800 underline dark:text-accent-100" style={{ border: "none", background: "none", cursor: "pointer", fontSize: 12, padding: 0 }}>cancel</button>
25
- </div>
26
- )
27
-
28
- const Canvas = ({ base, token, initial }) => {
29
- const { flow, error, notice, send } = useFlow(base, token, initial)
30
- const [ selected, setSelected ] = useState(null)
31
- const [ adding, setAdding ] = useState(null)
32
- const [ armed, setArmed ] = useState(null)
33
- const [ dragging, setDragging ] = useState(null)
34
- const [ showing, setShowing ] = useState(false)
35
- const cards = useRef({})
36
- const surface = useRef(null)
37
-
38
- useEffect(() => {
39
- const open = () => { setSelected(null); setShowing(true) }
40
- document.addEventListener("alembic:open-flow", open)
41
- return () => document.removeEventListener("alembic:open-flow", open)
42
- }, [])
43
-
44
- useEffect(() => {
45
- if (!showing) return
46
-
47
- const away = (event) => {
48
- if (!event.target.closest("[data-builder-panel], [data-open-panel]")) setShowing(false)
49
- }
50
- document.addEventListener("click", away)
51
- return () => document.removeEventListener("click", away)
52
- }, [ showing ])
53
-
54
- useEffect(() => {
55
- document.dispatchEvent(new CustomEvent("alembic:flow-named", { detail: flow.flow?.title || flow.flow?.slug }))
56
- }, [ flow.flow?.title, flow.flow?.slug ])
57
-
58
- const byId = useMemo(() => Object.fromEntries(flow.nodes.map((node) => [ node.id, node ])), [ flow.nodes ])
59
- const { links, extent } = useConnectors(flow, surface, cards, [ flow, selected, byId ])
60
-
61
- const violationsFor = useMemo(() => {
62
- const grouped = {}
63
- flow.violations.forEach((violation) => (grouped[violation.node] ||= []).push(violation))
64
- return grouped
65
- }, [ flow.violations ])
66
-
67
- const attached = flow.nodes.filter((node) => !node.loose)
68
- const loose = flow.nodes.filter((node) => node.loose)
69
- const rows = Math.max(0, ...attached.map((node) => node.row)) + 1
70
- const columns = Math.max(0, ...attached.map((node) => node.column)) + 1
71
-
72
- const connectTo = (target) => {
73
- if (!armed) return
74
-
75
- const [ source, port ] = armed
76
- setArmed(null)
77
- if (source !== target) send("/edges", "POST", { from: source, to: target, on: port || null })
78
- }
79
-
80
- const selectedNode = flow.nodes.find((node) => node.id === selected)
81
- const entryFor = flow.palette.find((entry) => entry.type === selectedNode?.type)
82
-
83
- return (
84
- <div className="text-gray-900 dark:text-gray-100" style={page} onMouseUp={() => setDragging(null)}
85
- onKeyDown={(event) => { if (event.key === "Escape") { setSelected(null); setArmed(null); setAdding(null) } }}
86
- tabIndex={-1}>
87
- <div ref={surface} className="bg-surface-50 dark:bg-surface-950" style={scroll}
88
- onClick={(event) => { if (event.target === surface.current) { setSelected(null); setArmed(null); setShowing(false) } }}>
89
- {armed && (
90
- <Choosing port={armed[1]} onCancel={() => setArmed(null)} />
91
- )}
92
- <Toolbar undoable={flow.undoable} redoable={flow.redoable}
93
- onAdd={() => setAdding({ at: { x: 16, y: 52 } })}
94
- onUndo={() => { setSelected(null); send("/undo", "POST") }}
95
- onRedo={() => { setSelected(null); send("/redo", "POST") }} />
96
-
97
- <ConnectorLayer links={links} extent={extent} dragging={dragging}
98
- onInsert={(link) => setAdding({ from: link.source, to: link.target, at: { x: link.midX + 30, y: link.midY } })}
99
- onRemove={(link) => { setSelected(null); send("/edges", "DELETE", { from: link.source, to: link.target }) }}
100
- onDrop={(link) => { const held = dragging; setDragging(null); send("/steps/" + held + "/move", "PATCH", { from: link.source, to: link.target }) }} />
101
-
102
- {loose.length > 0 && (
103
- <div data-loose className="border border-dashed border-gray-300 bg-gray-50 dark:border-zinc-700 dark:bg-zinc-900" style={waiting}>
104
- <div className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginBottom: 6 }}>Not in the flow yet — drag one onto a connection</div>
105
- {loose.map((node) => (
106
- <div key={node.id} style={{ marginBottom: 8 }}>
107
- <StepCard
108
- node={{
109
- ...node,
110
- ref: (element) => { cards.current[node.id] = element },
111
- violations: violationsFor[node.id] || [],
112
- connected: []
113
- }}
114
- selected={selected === node.id}
115
- armed={armed && armed[0] === node.id ? armed[1] : null}
116
- connecting={Boolean(armed)}
117
- onSelect={() => (armed ? connectTo(node.id) : setSelected(node.id))}
118
- onArm={(port, event) => { event.stopPropagation(); setArmed([ node.id, port ]) }}
119
- onDragStart={() => setDragging(node.id)}
120
- onDragEnd={() => setDragging(null)} />
121
- </div>
122
- ))}
123
- </div>
124
- )}
125
-
126
- <div style={{ ...grid, gridTemplateColumns: `repeat(${columns + 1}, ${(CARD + GAP_X) / 2}px)`, gridTemplateRows: `repeat(${rows}, auto)` }}>
127
- {attached.map((node) => (
128
- <div key={node.id} style={{ gridRow: node.row + 1, gridColumn: `${node.column + 1} / span 2`, zIndex: 2 }}>
129
- {node.placeholder && (
130
- <Placeholder node={{ ...node, ref: (element) => { cards.current[node.id] = element } }}
131
- dragging={Boolean(dragging)}
132
- onFill={() => setAdding({ from: node.from, on: node.on, at: { x: 16, y: 52 } })}
133
- onDrop={() => {
134
- const held = dragging
135
- setDragging(null)
136
- if (held) send("/edges", "POST", { from: node.from, to: held, on: node.on })
137
- }} />
138
- )}
139
- {!node.placeholder && <StepCard
140
- node={{
141
- ...node,
142
- ref: (element) => { cards.current[node.id] = element },
143
- violations: violationsFor[node.id] || [],
144
- connected: flow.edges.filter((edge) => edge.source === node.id).map((edge) => edge.label || null)
145
- }}
146
- selected={node.id === selected}
147
- armed={armed && armed[0] === node.id ? armed[1] : null}
148
- connecting={Boolean(armed) && armed[0] !== node.id}
149
- onSelect={() => (armed ? connectTo(node.id) : setSelected(node.id))}
150
- onArm={(port, event) => {
151
- const frame = surface.current.getBoundingClientRect()
152
- setAdding({
153
- from: node.id, on: port,
154
- at: { x: event.clientX - frame.left + surface.current.scrollLeft + 8,
155
- y: event.clientY - frame.top + surface.current.scrollTop + 8 }
156
- })
157
- }}
158
- onDragEnd={() => setDragging(null)}
159
- onDragStart={() => setDragging(node.id)}
160
- />}
161
- </div>
162
- ))}
163
- </div>
164
-
165
- {adding && (
166
- <TypePicker entries={flow.palette} at={adding.at} onDismiss={() => setAdding(null)}
167
- onConnect={adding.on !== undefined ? () => { setArmed([ adding.from, adding.on ]); setAdding(null) } : null}
168
- onPick={(entry) => {
169
- const where = adding
170
- setAdding(null)
171
- send("/steps", "POST", { id: nextId(entry.type, flow.nodes.map((node) => node.id)), type: entry.type, from: where.from, to: where.to, on: where.on })
172
- }} />
173
- )}
174
- </div>
175
-
176
- {showing && !selectedNode && (
177
- <Panel flow={flow.flow || {}} changes={flow.changes || []} problems={flow.violations} refusal={error} notice={notice}
178
- onClose={() => setShowing(false)}
179
- onCreate={() => send("/versions", "POST")}
180
- onPublish={() => send("/publish", "POST")}
181
- onSaveDetails={(details) => send("/details", "PATCH", { flow: details })} />
182
- )}
183
-
184
- {selectedNode && (
185
- <Inspector node={selectedNode}
186
- fields={entryFor?.fields || {}} holds={entryFor?.records || {}}
187
- labels={entryFor?.labels || {}} recordLabels={entryFor?.record_labels || {}}
188
- choices={offeredTo(selectedNode, entryFor)}
189
- onClose={() => setSelected(null)}
190
- onSave={(config) => send(`/steps/${selectedNode.id}`, "PATCH", { config })}
191
- onDelete={() => { setSelected(null); send(`/steps/${selectedNode.id}`, "DELETE") }} />
192
- )}
193
- </div>
194
- )
195
- }
196
-
197
- export default Canvas
@@ -1,30 +0,0 @@
1
- import React, { useState } from "react"
2
- import Button from "keystone_ui-react/src/Button.jsx"
3
- const round = { width: 24, height: 24, padding: 0, fontSize: 15, lineHeight: "15px" }
4
-
5
- const Connector = ({ link, onInsert, onRemove, onDrop, dragging }) => {
6
- const [ over, setOver ] = useState(false)
7
- const showing = over || dragging
8
-
9
- return (
10
- <div data-connector={`${link.source}-${link.target}`}
11
- style={{ position: "absolute", left: link.midX - 34, top: link.midY - 20, width: 68, height: 40, zIndex: 4 }}
12
- onMouseEnter={() => setOver(true)} onMouseLeave={() => setOver(false)}
13
- onDragEnter={() => setOver(true)} onDragLeave={() => setOver(false)}
14
- onDragOver={(event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move" }}
15
- onDrop={(event) => { event.preventDefault(); setOver(false); onDrop() }}>
16
- <div style={{ display: "flex", gap: 4, justifyContent: "center", alignItems: "center", height: "100%",
17
- opacity: showing ? 1 : 0, transition: "opacity .12s" }}>
18
- <Button variant="secondary" title={dragging ? "Move the step here" : "Insert a step here"} onClick={onInsert}
19
- className="rounded-full data-[over=true]:ring-2 data-[over=true]:ring-accent-600"
20
- data-over={Boolean(dragging && over)} style={round}>+</Button>
21
- {!dragging && (
22
- <Button variant="danger" title="Remove this connection" onClick={onRemove}
23
- className="rounded-full" style={round}>×</Button>
24
- )}
25
- </div>
26
- </div>
27
- )
28
- }
29
-
30
- export default Connector
@@ -1,31 +0,0 @@
1
- import React from "react"
2
- import Connector from "./Connector"
3
-
4
- const ConnectorLayer = ({ links, extent, dragging, onInsert, onRemove, onDrop }) => (
5
- <>
6
- <svg width={extent.width} height={extent.height}
7
- style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none", zIndex: 1 }}>
8
- <defs>
9
- <marker id="alembic-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
10
- <path d="M 0 0 L 10 5 L 0 10 z" className="fill-gray-400 dark:fill-zinc-500" />
11
- </marker>
12
- </defs>
13
- {links.map((link) => (
14
- <path key={link.id} data-link={link.id} className="stroke-gray-400 dark:stroke-zinc-500" fill="none" strokeWidth="1.5" markerEnd="url(#alembic-arrow)" d={link.path} />
15
- ))}
16
- </svg>
17
-
18
- {links.filter((link) => link.label).map((link) => (
19
- <div key={`${link.id}-label`} data-link-label={link.id} className="bg-surface-50 text-gray-500 dark:bg-surface-950 dark:text-gray-400"
20
- style={{ position: "absolute", left: link.midX - 10, top: link.midY - 18, zIndex: 3,
21
- fontSize: 11, padding: "0 3px" }}>{link.label}</div>
22
- ))}
23
-
24
- {links.filter((link) => !link.placeholder).map((link) => (
25
- <Connector key={link.id} link={link} dragging={Boolean(dragging)}
26
- onInsert={() => onInsert(link)} onRemove={() => onRemove(link)} onDrop={() => onDrop(link)} />
27
- ))}
28
- </>
29
- )
30
-
31
- export default ConnectorLayer
@@ -1,42 +0,0 @@
1
- import React from "react"
2
- import { toggled } from "./choices"
3
- import Input from "keystone_ui-react/src/Input.jsx"
4
- import Select from "keystone_ui-react/src/Select.jsx"
5
- import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
6
-
7
- const offered = (choices) =>
8
- (choices || []).map((choice) => (typeof choice === "object" ? choice : { value: choice, label: choice }))
9
-
10
- const Control = ({ type, value, choices, onChange, onSettle }) => {
11
- if (type === "boolean") return <Checkbox checked={Boolean(value)} onChange={(e) => onSettle(e.target.checked)} />
12
-
13
- if (type === "select" || type === "previous_step" || type === "from_step") {
14
- return <Select className="mb-3" value={value ?? ""} onChange={(e) => onSettle(e.target.value)}>
15
- <option value=""></option>
16
- {offered(choices).map((choice) => <option key={choice.value} value={choice.value}>{choice.label}</option>)}
17
- </Select>
18
- }
19
-
20
- if (type === "multi_select") {
21
- const chosen = Array.isArray(value) ? value : []
22
- const toggle = (choice) => onSettle(toggled(chosen, choice))
23
- return <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
24
- {(choices || []).map((choice) => (
25
- <label key={choice} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
26
- <Checkbox checked={chosen.includes(choice)} onChange={() => toggle(choice)} />
27
- <span>{choice}</span>
28
- </label>
29
- ))}
30
- </div>
31
- }
32
-
33
- if (type === "integer" || type === "float") {
34
- return <Input className="mb-3" type="number" step={type === "integer" ? "1" : "any"} value={value ?? ""}
35
- onChange={(e) => onChange(e.target.value)} onBlur={(e) => onSettle(e.target.value)} />
36
- }
37
-
38
- return <Input className="mb-3" type="text" value={value ?? ""}
39
- onChange={(e) => onChange(e.target.value)} onBlur={(e) => onSettle(e.target.value)} />
40
- }
41
-
42
- export default Control
@@ -1,44 +0,0 @@
1
- import React, { useEffect, useState } from "react"
2
- import Control from "./Control"
3
- import Records from "./Records"
4
- import Panel from "keystone_ui-react/src/Panel.jsx"
5
- import Button from "keystone_ui-react/src/Button.jsx"
6
- import { Label } from "keystone_ui-react/src/FieldText.jsx"
7
-
8
- const panel = { width: 280, padding: 20, overflowY: "auto" }
9
-
10
- const Inspector = ({ node, fields, holds, labels, recordLabels, choices, onSave, onDelete, onClose }) => {
11
- const [ draft, setDraft ] = useState(node.config)
12
- useEffect(() => setDraft(node.config), [ node.id, node.config ])
13
-
14
- const settle = (next) => {
15
- setDraft(next)
16
- if (JSON.stringify(next) !== JSON.stringify(node.config)) onSave(next)
17
- }
18
-
19
- return (
20
- <Panel as="aside" style={panel} data-inspector>
21
- <div style={{ display: "flex", alignItems: "start", justifyContent: "space-between", gap: 8 }}>
22
- <h2 style={{ fontWeight: 600, marginBottom: 2 }}>{node.label}</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
- <p className="text-gray-500 dark:text-gray-400" style={{ fontSize: 11, marginBottom: 16 }}>{node.id} · {node.type}</p>
27
- {Object.entries(fields).map(([ name, type ]) => (
28
- <Label key={name}>
29
- <span style={{ display: "block", marginBottom: 3 }}>{labels[name] || name}</span>
30
- {type === "list"
31
- ? <Records holds={holds[name] || {}} labels={recordLabels[name] || {}} rows={draft[name]}
32
- onChange={(next) => setDraft({ ...draft, [name]: next })}
33
- onSettle={(next) => settle({ ...draft, [name]: next })} />
34
- : <Control type={type} value={draft[name]} choices={choices[name]}
35
- onChange={(next) => setDraft({ ...draft, [name]: next })}
36
- onSettle={(next) => settle({ ...draft, [name]: next })} />}
37
- </Label>
38
- ))}
39
- <Button variant="danger" size="sm" className="w-full" onClick={onDelete}>Delete step</Button>
40
- </Panel>
41
- )
42
- }
43
-
44
- export default Inspector