alembic 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +105 -0
- data/Rakefile +6 -0
- data/app/assets/builds/alembic/canvas.js +1 -0
- data/app/assets/builds/alembic/page_builder.css +1 -0
- data/app/assets/builds/alembic/page_builder.js +5 -0
- data/app/assets/stylesheets/alembic/application.css +15 -0
- data/app/controllers/alembic/application_controller.rb +32 -0
- data/app/controllers/alembic/flows_controller.rb +104 -0
- data/app/controllers/alembic/manage/base_controller.rb +16 -0
- data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
- data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
- data/app/controllers/alembic/manage/flows_controller.rb +55 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
- data/app/controllers/alembic/manage/pages_controller.rb +38 -0
- data/app/controllers/alembic/manage/previews_controller.rb +38 -0
- data/app/controllers/alembic/manage/versions_controller.rb +22 -0
- data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
- data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
- data/app/helpers/alembic/application_helper.rb +11 -0
- data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
- data/app/javascript/alembic/canvas/Connector.jsx +30 -0
- data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
- data/app/javascript/alembic/canvas/Control.jsx +42 -0
- data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
- data/app/javascript/alembic/canvas/Panel.jsx +67 -0
- data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
- data/app/javascript/alembic/canvas/Port.jsx +16 -0
- data/app/javascript/alembic/canvas/Records.jsx +34 -0
- data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
- data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
- data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
- data/app/javascript/alembic/canvas/changes.js +5 -0
- data/app/javascript/alembic/canvas/choices.js +4 -0
- data/app/javascript/alembic/canvas/flow.js +7 -0
- data/app/javascript/alembic/canvas/flowSender.js +22 -0
- data/app/javascript/alembic/canvas/ids.js +7 -0
- data/app/javascript/alembic/canvas/rows.js +2 -0
- data/app/javascript/alembic/canvas/styles.js +3 -0
- data/app/javascript/alembic/canvas/useConnectors.js +75 -0
- data/app/javascript/alembic/canvas/useFlow.js +17 -0
- data/app/javascript/alembic/canvas.jsx +13 -0
- data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
- data/app/javascript/alembic/page_builder.jsx +10 -0
- data/app/jobs/alembic/application_job.rb +4 -0
- data/app/mailers/alembic/application_mailer.rb +6 -0
- data/app/models/alembic/application_record.rb +5 -0
- data/app/models/alembic/asked.rb +3 -0
- data/app/models/alembic/choice.rb +7 -0
- data/app/models/alembic/flow/admission.rb +19 -0
- data/app/models/alembic/flow/canvas.rb +167 -0
- data/app/models/alembic/flow/change.rb +18 -0
- data/app/models/alembic/flow/condition.rb +24 -0
- data/app/models/alembic/flow/definition.rb +132 -0
- data/app/models/alembic/flow/digest.rb +116 -0
- data/app/models/alembic/flow/document.rb +131 -0
- data/app/models/alembic/flow/drawing.rb +15 -0
- data/app/models/alembic/flow/edge.rb +9 -0
- data/app/models/alembic/flow/edit_history.rb +75 -0
- data/app/models/alembic/flow/layout.rb +88 -0
- data/app/models/alembic/flow/name.rb +15 -0
- data/app/models/alembic/flow/node.rb +5 -0
- data/app/models/alembic/flow/output.rb +23 -0
- data/app/models/alembic/flow/progress/kept.rb +38 -0
- data/app/models/alembic/flow/progress/loose.rb +43 -0
- data/app/models/alembic/flow/progress.rb +11 -0
- data/app/models/alembic/flow/registry.rb +25 -0
- data/app/models/alembic/flow/run.rb +54 -0
- data/app/models/alembic/flow/runner.rb +65 -0
- data/app/models/alembic/flow/settings.rb +80 -0
- data/app/models/alembic/flow/start.rb +13 -0
- data/app/models/alembic/flow/step.rb +42 -0
- data/app/models/alembic/flow/step_type/declaration.rb +118 -0
- data/app/models/alembic/flow/step_type.rb +69 -0
- data/app/models/alembic/flow/summaries.rb +44 -0
- data/app/models/alembic/flow/summary_version.rb +13 -0
- data/app/models/alembic/flow/switch.rb +17 -0
- data/app/models/alembic/flow/terminal.rb +13 -0
- data/app/models/alembic/flow/validator.rb +171 -0
- data/app/models/alembic/flow/version.rb +26 -0
- data/app/models/alembic/flow/violation.rb +9 -0
- data/app/models/alembic/flow.rb +44 -0
- data/app/models/alembic/outputs/band.rb +22 -0
- data/app/models/alembic/outputs/grouped.rb +26 -0
- data/app/models/alembic/outputs/lowest.rb +20 -0
- data/app/models/alembic/outputs/percentage.rb +31 -0
- data/app/models/alembic/outputs/tally.rb +23 -0
- data/app/models/alembic/outputs/weighted_sum.rb +26 -0
- data/app/models/alembic/page.rb +11 -0
- data/app/models/alembic/runner.rb +13 -0
- data/app/models/alembic/steps/question.rb +52 -0
- data/app/models/alembic/summary/output_type.rb +40 -0
- data/app/models/alembic/summary/registry.rb +25 -0
- data/app/models/alembic/summary/report.rb +31 -0
- data/app/models/alembic/summary/result.rb +5 -0
- data/app/models/alembic/summary/run.rb +13 -0
- data/app/models/alembic/summary.rb +15 -0
- data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
- data/app/views/alembic/flows/complete.html.erb +34 -0
- data/app/views/alembic/flows/show.html.erb +17 -0
- data/app/views/alembic/flows/step.html.erb +27 -0
- data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
- data/app/views/alembic/manage/flows/edit.html.erb +29 -0
- data/app/views/alembic/manage/flows/index.html.erb +32 -0
- data/app/views/alembic/manage/flows/show.html.erb +26 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
- data/app/views/alembic/manage/pages/index.html.erb +20 -0
- data/app/views/alembic/manage/pages/show.html.erb +11 -0
- data/app/views/alembic/manage/versions/index.html.erb +41 -0
- data/app/views/layouts/alembic/application.html.erb +17 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
- data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
- data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
- data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
- data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
- data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
- data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
- data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
- data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
- data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
- data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
- data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
- data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
- data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
- data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
- data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
- data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
- data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
- data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
- data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
- data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
- data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
- data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
- data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
- data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
- data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
- data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
- data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
- data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
- data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
- data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
- data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
- data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
- data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
- data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
- data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
- data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
- data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
- data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
- data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
- data/lib/alembic/engine.rb +43 -0
- data/lib/alembic/leftover_stylesheet.rb +14 -0
- data/lib/alembic/version.rb +3 -0
- data/lib/alembic.rb +34 -0
- data/lib/tasks/alembic_tasks.rake +4 -0
- metadata +252 -0
|
@@ -0,0 +1,183 @@
|
|
|
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
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Manage
|
|
3
|
+
class FlowsController < BaseController
|
|
4
|
+
include DrawsCanvas
|
|
5
|
+
def index
|
|
6
|
+
@diagnostics = ordered_diagnostics
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
@diagnostic = Flow::Definition.new(create_params)
|
|
11
|
+
|
|
12
|
+
if @diagnostic.save
|
|
13
|
+
redirect_to manage_flow_path(@diagnostic), notice: "Flow created."
|
|
14
|
+
else
|
|
15
|
+
@diagnostics = ordered_diagnostics
|
|
16
|
+
render :index, status: :unprocessable_entity
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show
|
|
21
|
+
@diagnostic = Flow::Definition.find(params[:id])
|
|
22
|
+
@canvas = canvas_payload(@diagnostic)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def edit
|
|
26
|
+
@diagnostic = Flow::Definition.find(params[:id])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update
|
|
30
|
+
@diagnostic = Flow::Definition.find(params[:id])
|
|
31
|
+
@diagnostic.update!(diagnostic_params)
|
|
32
|
+
redirect_to manage_flow_path(@diagnostic), notice: "Saved."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy
|
|
36
|
+
Flow::Definition.find(params[:id]).destroy!
|
|
37
|
+
redirect_to manage_flows_path, notice: "Flow removed."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def ordered_diagnostics
|
|
43
|
+
Flow::Definition.order(:slug)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_params
|
|
47
|
+
params.require(:flow).permit(:slug, :kind)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def diagnostic_params
|
|
51
|
+
params.require(:flow).permit(:title, :summary, :start_label, :persists)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "ks_blocks/layout_endpoints"
|
|
2
|
+
|
|
3
|
+
module Alembic
|
|
4
|
+
module Manage
|
|
5
|
+
class PageBlocksController < BaseController
|
|
6
|
+
include KsBlocks::LayoutEndpoints
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def block_layout_record
|
|
11
|
+
Page.find(params[:page_id])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def block_content(block)
|
|
15
|
+
render_to_string(partial: "alembic/manage/pages/block_content", locals: { block: block }, formats: [ :html ])
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Manage
|
|
3
|
+
class PagesController < BaseController
|
|
4
|
+
def index
|
|
5
|
+
@pages = Page.order(:name)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@page = Page.find(params[:id])
|
|
10
|
+
@payload = payload(@page)
|
|
11
|
+
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html
|
|
14
|
+
format.json { render json: @payload }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
page = Page.create!(page_params)
|
|
20
|
+
redirect_to manage_page_path(page)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def payload(page)
|
|
26
|
+
{
|
|
27
|
+
base: manage_page_path(page),
|
|
28
|
+
pages: manage_pages_path,
|
|
29
|
+
name: page.name
|
|
30
|
+
}.merge(page.layout_data)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def page_params
|
|
34
|
+
params.require(:page).permit(:name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Manage
|
|
3
|
+
class PreviewsController < ::Alembic::FlowsController
|
|
4
|
+
include AuthenticatesAdmin
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
@diagnostic = previewed
|
|
8
|
+
render template: "alembic/flows/show"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def flow_start_path(_slug)
|
|
14
|
+
alembic.manage_flow_preview_path(previewed)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def flow_step_path(_slug)
|
|
18
|
+
alembic.step_manage_flow_preview_path(previewed)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def previewing?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def flowing_definition(diagnostic)
|
|
26
|
+
diagnostic.document
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def previewed
|
|
30
|
+
@previewed ||= Flow::Definition.find(params[:flow_id])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def admit(_diagnostic)
|
|
34
|
+
previewed
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Manage
|
|
3
|
+
module AuthenticatesAdmin
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_action :authenticate_admin
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def authenticate_admin
|
|
13
|
+
return unless Alembic.admin_authentication_method
|
|
14
|
+
|
|
15
|
+
send(Alembic.admin_authentication_method)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|