flowengine-rails 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/.claude/branch-name.sh +49 -0
- data/.claude/commands/create-pr.md +93 -0
- data/.claude/commands/stash-unstaged.md +21 -0
- data/.claude/commands/unstash-unstaged.md +15 -0
- data/.claude/settings.json +72 -0
- data/.rubocop_todo.yml +17 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +153 -0
- data/LICENSE.txt +21 -0
- data/README.md +294 -0
- data/Rakefile +43 -0
- data/app/assets/javascripts/flow_engine/embed.js +44 -0
- data/app/assets/javascripts/flow_engine/progress_controller.js +17 -0
- data/app/assets/javascripts/flow_engine/step_controller.js +22 -0
- data/app/assets/stylesheets/flow_engine/application.css +569 -0
- data/app/controllers/flow_engine/admin/definitions_controller.rb +95 -0
- data/app/controllers/flow_engine/application_controller.rb +41 -0
- data/app/controllers/flow_engine/sessions_controller.rb +91 -0
- data/app/helpers/flow_engine/sessions_helper.rb +20 -0
- data/app/models/flow_engine/application_record.rb +8 -0
- data/app/models/flow_engine/flow_definition.rb +75 -0
- data/app/models/flow_engine/flow_session.rb +98 -0
- data/app/views/flow_engine/admin/definitions/_form.html.erb +27 -0
- data/app/views/flow_engine/admin/definitions/edit.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/index.html.erb +41 -0
- data/app/views/flow_engine/admin/definitions/mermaid.html.erb +9 -0
- data/app/views/flow_engine/admin/definitions/new.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/show.html.erb +25 -0
- data/app/views/flow_engine/sessions/completed.html.erb +34 -0
- data/app/views/flow_engine/sessions/new.html.erb +17 -0
- data/app/views/flow_engine/sessions/show.html.erb +26 -0
- data/app/views/flow_engine/sessions/steps/_boolean.html.erb +10 -0
- data/app/views/flow_engine/sessions/steps/_display.html.erb +4 -0
- data/app/views/flow_engine/sessions/steps/_multi_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_number.html.erb +3 -0
- data/app/views/flow_engine/sessions/steps/_number_matrix.html.erb +13 -0
- data/app/views/flow_engine/sessions/steps/_single_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_text.html.erb +11 -0
- data/app/views/flow_engine/sessions/steps/_unknown.html.erb +4 -0
- data/app/views/layouts/flow_engine/application.html.erb +26 -0
- data/app/views/layouts/flow_engine/embed.html.erb +30 -0
- data/config/routes.rb +22 -0
- data/db/migrate/01_create_flow_engine_definitions.rb +18 -0
- data/db/migrate/02_create_flow_engine_sessions.rb +18 -0
- data/exe/flowengine-rails +4 -0
- data/justfile +49 -0
- data/lefthook.yml +16 -0
- data/lib/flowengine/rails/configuration.rb +23 -0
- data/lib/flowengine/rails/dsl_loader.rb +35 -0
- data/lib/flowengine/rails/engine.rb +26 -0
- data/lib/flowengine/rails/version.rb +7 -0
- data/lib/flowengine/rails.rb +27 -0
- data/lib/generators/flow_engine/flow/flow_generator.rb +29 -0
- data/lib/generators/flow_engine/flow/templates/flow_definition.rb.tt +27 -0
- data/lib/generators/flow_engine/flow/templates/seed_task.rake.tt +22 -0
- data/lib/generators/flow_engine/install/install_generator.rb +34 -0
- data/lib/generators/flow_engine/install/templates/initializer.rb +25 -0
- data/log/.gitkeep +0 -0
- data/sig/flowengine/rails.rbs +6 -0
- metadata +164 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlowEngine
|
|
4
|
+
class SessionsController < ApplicationController
|
|
5
|
+
before_action :set_session, only: %i[show update completed abandon]
|
|
6
|
+
|
|
7
|
+
def new
|
|
8
|
+
@definitions = FlowEngine::FlowDefinition.active.order(:name)
|
|
9
|
+
@definition_id = params[:definition_id]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
definition = FlowEngine::FlowDefinition.find(params[:definition_id])
|
|
14
|
+
engine = FlowEngine::Engine.new(definition.parsed_definition)
|
|
15
|
+
state = engine.to_state
|
|
16
|
+
|
|
17
|
+
session = FlowEngine::FlowSession.create!(
|
|
18
|
+
flow_definition: definition,
|
|
19
|
+
current_step_id: state[:current_step_id]&.to_s,
|
|
20
|
+
answers: {},
|
|
21
|
+
history: state[:history].map(&:to_s),
|
|
22
|
+
status: "in_progress"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
redirect_to session_path(session, embed_params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def show
|
|
29
|
+
if @flow_session.completed?
|
|
30
|
+
redirect_to completed_session_path(@flow_session, embed_params)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@node = @flow_session.current_node
|
|
35
|
+
@progress = @flow_session.progress_percentage
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update
|
|
39
|
+
unless @flow_session.in_progress?
|
|
40
|
+
redirect_to session_path(@flow_session, embed_params)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
answer_value = parse_answer
|
|
45
|
+
@flow_session.advance!(answer_value)
|
|
46
|
+
|
|
47
|
+
if @flow_session.completed?
|
|
48
|
+
redirect_to completed_session_path(@flow_session, embed_params)
|
|
49
|
+
else
|
|
50
|
+
redirect_to session_path(@flow_session, embed_params)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def completed
|
|
55
|
+
@result = @flow_session.result_json
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def abandon
|
|
59
|
+
@flow_session.abandon!
|
|
60
|
+
redirect_to new_session_path(embed_params), notice: "Session abandoned."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def set_session
|
|
66
|
+
@flow_session = FlowEngine::FlowSession.find(params[:id])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def embed_params
|
|
70
|
+
embed_mode? ? { embed: "true" } : {}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_answer
|
|
74
|
+
node = @flow_session.current_node
|
|
75
|
+
return params[:answer] unless node
|
|
76
|
+
|
|
77
|
+
case node.type
|
|
78
|
+
when :multi_select
|
|
79
|
+
params[:answer_values] || []
|
|
80
|
+
when :number_matrix
|
|
81
|
+
(params[:answer_fields] || {}).to_unsafe_h.transform_values(&:to_i)
|
|
82
|
+
when :number
|
|
83
|
+
params[:answer].to_i
|
|
84
|
+
when :boolean
|
|
85
|
+
params[:answer] == "true"
|
|
86
|
+
else
|
|
87
|
+
params[:answer]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlowEngine
|
|
4
|
+
module SessionsHelper
|
|
5
|
+
STEP_PARTIALS = {
|
|
6
|
+
multi_select: "flow_engine/sessions/steps/multi_select",
|
|
7
|
+
single_select: "flow_engine/sessions/steps/single_select",
|
|
8
|
+
number_matrix: "flow_engine/sessions/steps/number_matrix",
|
|
9
|
+
text: "flow_engine/sessions/steps/text",
|
|
10
|
+
number: "flow_engine/sessions/steps/number",
|
|
11
|
+
boolean: "flow_engine/sessions/steps/boolean",
|
|
12
|
+
display: "flow_engine/sessions/steps/display"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def render_step(node, form)
|
|
16
|
+
partial = STEP_PARTIALS.fetch(node.type, "flow_engine/sessions/steps/unknown")
|
|
17
|
+
render partial: partial, locals: { node: node, form: form }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlowEngine
|
|
4
|
+
class FlowDefinition < ApplicationRecord
|
|
5
|
+
self.table_name = "flow_engine_definitions"
|
|
6
|
+
|
|
7
|
+
has_many :flow_sessions, dependent: :restrict_with_error, foreign_key: :definition_id, inverse_of: :flow_definition
|
|
8
|
+
|
|
9
|
+
validates :name, presence: true
|
|
10
|
+
validates :version, presence: true,
|
|
11
|
+
numericality: { only_integer: true, greater_than: 0 },
|
|
12
|
+
uniqueness: { scope: :name }
|
|
13
|
+
validates :dsl, presence: true
|
|
14
|
+
validate :dsl_must_parse
|
|
15
|
+
|
|
16
|
+
attr_accessor :skip_auto_version
|
|
17
|
+
|
|
18
|
+
before_validation :auto_increment_version, on: :create, unless: :skip_auto_version
|
|
19
|
+
|
|
20
|
+
scope :active, -> { where(active: true) }
|
|
21
|
+
scope :by_name, ->(name) { where(name: name) }
|
|
22
|
+
scope :latest_version, ->(name) { by_name(name).order(version: :desc).first }
|
|
23
|
+
|
|
24
|
+
def parsed_definition
|
|
25
|
+
@parsed_definition ||= FlowEngine::Rails::DslLoader.load(dsl, cache_key: cache_key_for_dsl)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def activate!
|
|
29
|
+
transaction do
|
|
30
|
+
self.class.where(name: name, active: true).where.not(id: id).update_all(active: false)
|
|
31
|
+
update!(active: true)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def deactivate!
|
|
36
|
+
update!(active: false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def readonly?
|
|
40
|
+
flow_sessions.exists? && persisted? && !new_record?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mermaid_diagram
|
|
44
|
+
exporter = FlowEngine::Graph::MermaidExporter.new(parsed_definition)
|
|
45
|
+
exporter.export
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def step_ids
|
|
49
|
+
parsed_definition.step_ids
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def total_steps
|
|
53
|
+
step_ids.size
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def cache_key_for_dsl
|
|
59
|
+
"definition:#{id}:v#{version}" if persisted?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def auto_increment_version
|
|
63
|
+
max_version = self.class.where(name: name).maximum(:version) || 0
|
|
64
|
+
self.version = max_version + 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def dsl_must_parse
|
|
68
|
+
return if dsl.blank?
|
|
69
|
+
|
|
70
|
+
FlowEngine::Rails::DslLoader.load(dsl)
|
|
71
|
+
rescue FlowEngine::Errors::Error => e
|
|
72
|
+
errors.add(:dsl, "is invalid: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlowEngine
|
|
4
|
+
class FlowSession < ApplicationRecord
|
|
5
|
+
self.table_name = "flow_engine_sessions"
|
|
6
|
+
|
|
7
|
+
belongs_to :flow_definition, foreign_key: :definition_id, inverse_of: :flow_sessions
|
|
8
|
+
|
|
9
|
+
validates :status, presence: true, inclusion: { in: %w[in_progress completed abandoned] }
|
|
10
|
+
|
|
11
|
+
scope :in_progress, -> { where(status: "in_progress") }
|
|
12
|
+
scope :completed, -> { where(status: "completed") }
|
|
13
|
+
scope :abandoned, -> { where(status: "abandoned") }
|
|
14
|
+
|
|
15
|
+
def engine
|
|
16
|
+
state = {
|
|
17
|
+
current_step_id: current_step_id,
|
|
18
|
+
answers: answers || {},
|
|
19
|
+
history: history || []
|
|
20
|
+
}
|
|
21
|
+
FlowEngine::Engine.from_state(flow_definition.parsed_definition, state)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def advance!(answer_value)
|
|
25
|
+
eng = engine
|
|
26
|
+
eng.answer(answer_value)
|
|
27
|
+
persist_engine_state!(eng)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def abandon!
|
|
31
|
+
update!(status: "abandoned")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def in_progress?
|
|
35
|
+
status == "in_progress"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def completed?
|
|
39
|
+
status == "completed"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def abandoned?
|
|
43
|
+
status == "abandoned"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def finished?
|
|
47
|
+
completed? || abandoned?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def current_node
|
|
51
|
+
return nil if current_step_id.blank?
|
|
52
|
+
|
|
53
|
+
flow_definition.parsed_definition.step(current_step_id.to_sym)
|
|
54
|
+
rescue FlowEngine::UnknownStepError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def progress_percentage
|
|
59
|
+
total = flow_definition.total_steps
|
|
60
|
+
return 0 if total.zero?
|
|
61
|
+
|
|
62
|
+
completed_count = (history || []).size
|
|
63
|
+
[(completed_count.to_f / total * 100).round, 100].min
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def result_json
|
|
67
|
+
{
|
|
68
|
+
definition_name: flow_definition.name,
|
|
69
|
+
definition_version: flow_definition.version,
|
|
70
|
+
answers: answers,
|
|
71
|
+
history: history,
|
|
72
|
+
status: status,
|
|
73
|
+
completed_at: updated_at&.iso8601
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def persist_engine_state!(eng)
|
|
80
|
+
state = eng.to_state
|
|
81
|
+
new_status = eng.finished? ? "completed" : "in_progress"
|
|
82
|
+
|
|
83
|
+
update!(
|
|
84
|
+
current_step_id: state[:current_step_id]&.to_s,
|
|
85
|
+
answers: state[:answers].transform_keys(&:to_s),
|
|
86
|
+
history: state[:history].map(&:to_s),
|
|
87
|
+
status: new_status
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
fire_completion_callback if new_status == "completed"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def fire_completion_callback
|
|
94
|
+
callback = FlowEngine::Rails.configuration.on_session_complete
|
|
95
|
+
callback&.call(self)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%= form_with model: [:admin, definition], url: url, method: method, local: true do |form| %>
|
|
2
|
+
<% if definition.errors.any? %>
|
|
3
|
+
<div class="fe-errors">
|
|
4
|
+
<h3><%= pluralize(definition.errors.count, "error") %> prevented this definition from being saved:</h3>
|
|
5
|
+
<ul>
|
|
6
|
+
<% definition.errors.full_messages.each do |msg| %>
|
|
7
|
+
<li><%= msg %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="fe-form-group">
|
|
14
|
+
<%= form.label :name, class: "fe-label" %>
|
|
15
|
+
<%= form.text_field :name, class: "fe-input" %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="fe-form-group">
|
|
19
|
+
<%= form.label :dsl, "DSL Definition", class: "fe-label" %>
|
|
20
|
+
<%= form.text_area :dsl, rows: 20, class: "fe-input fe-input--textarea fe-input--code" %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="fe-form-group">
|
|
24
|
+
<%= form.submit class: "fe-btn fe-btn--primary" %>
|
|
25
|
+
<%= link_to "Cancel", flow_engine.admin_definitions_path, class: "fe-btn" %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<h1>Flow Definitions</h1>
|
|
2
|
+
|
|
3
|
+
<%= link_to "New Definition", flow_engine.new_admin_definition_path, class: "fe-btn fe-btn--primary" %>
|
|
4
|
+
|
|
5
|
+
<% @grouped.each do |name, versions| %>
|
|
6
|
+
<div class="fe-definition-group">
|
|
7
|
+
<h2><%= name %></h2>
|
|
8
|
+
<table class="fe-table">
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th>Version</th>
|
|
12
|
+
<th>Active</th>
|
|
13
|
+
<th>Sessions</th>
|
|
14
|
+
<th>Created</th>
|
|
15
|
+
<th>Actions</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<% versions.each do |definition| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td>v<%= definition.version %></td>
|
|
22
|
+
<td><%= definition.active? ? "Yes" : "No" %></td>
|
|
23
|
+
<td><%= definition.flow_sessions.count %></td>
|
|
24
|
+
<td><%= definition.created_at.strftime("%Y-%m-%d %H:%M") %></td>
|
|
25
|
+
<td>
|
|
26
|
+
<%= link_to "View", flow_engine.admin_definition_path(definition) %>
|
|
27
|
+
<% unless definition.readonly? %>
|
|
28
|
+
| <%= link_to "Edit", flow_engine.edit_admin_definition_path(definition) %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% if definition.active? %>
|
|
31
|
+
| <%= link_to "Deactivate", flow_engine.deactivate_admin_definition_path(definition), data: { turbo_method: :post } %>
|
|
32
|
+
<% else %>
|
|
33
|
+
| <%= link_to "Activate", flow_engine.activate_admin_definition_path(definition), data: { turbo_method: :post } %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<h1>Mermaid Diagram: <%= @definition.name %> v<%= @definition.version %></h1>
|
|
2
|
+
|
|
3
|
+
<div class="fe-mermaid">
|
|
4
|
+
<pre class="fe-code"><code><%= @diagram %></code></pre>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<%= link_to "Back", flow_engine.admin_definition_path(@definition), class: "fe-btn" %>
|
|
9
|
+
</p>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<h1><%= @definition.name %> <small>v<%= @definition.version %></small></h1>
|
|
2
|
+
|
|
3
|
+
<div class="fe-definition-detail">
|
|
4
|
+
<p><strong>Status:</strong> <%= @definition.active? ? "Active" : "Inactive" %></p>
|
|
5
|
+
<p><strong>Sessions:</strong> <%= @definition.flow_sessions.count %></p>
|
|
6
|
+
<p><strong>Steps:</strong> <%= @definition.total_steps %></p>
|
|
7
|
+
|
|
8
|
+
<div class="fe-definition-detail__actions">
|
|
9
|
+
<% if @definition.active? %>
|
|
10
|
+
<%= link_to "Deactivate", flow_engine.deactivate_admin_definition_path(@definition), method: :post, data: { turbo_method: :post }, class: "fe-btn" %>
|
|
11
|
+
<% else %>
|
|
12
|
+
<%= link_to "Activate", flow_engine.activate_admin_definition_path(@definition), method: :post, data: { turbo_method: :post }, class: "fe-btn fe-btn--primary" %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<% unless @definition.readonly? %>
|
|
16
|
+
<%= link_to "Edit", flow_engine.edit_admin_definition_path(@definition), class: "fe-btn" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<%= link_to "Mermaid Diagram", flow_engine.mermaid_admin_definition_path(@definition), class: "fe-btn" %>
|
|
20
|
+
<%= link_to "Back", flow_engine.admin_definitions_path, class: "fe-btn" %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<h2>DSL Source</h2>
|
|
24
|
+
<pre class="fe-code"><code><%= @definition.dsl %></code></pre>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<div class="fe-completed">
|
|
2
|
+
<h1>Flow Complete</h1>
|
|
3
|
+
|
|
4
|
+
<div class="fe-completed__summary">
|
|
5
|
+
<h2><%= @flow_session.flow_definition.name %> — Results</h2>
|
|
6
|
+
|
|
7
|
+
<table class="fe-table">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Step</th>
|
|
11
|
+
<th>Answer</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% (@flow_session.answers || {}).each do |step_id, answer| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td><%= step_id %></td>
|
|
18
|
+
<td><%= answer.is_a?(Hash) || answer.is_a?(Array) ? answer.to_json : answer %></td>
|
|
19
|
+
</tr>
|
|
20
|
+
<% end %>
|
|
21
|
+
</tbody>
|
|
22
|
+
</table>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<% if embed_mode? %>
|
|
26
|
+
<script>
|
|
27
|
+
window.parent.postMessage({
|
|
28
|
+
type: 'flowengine:completed',
|
|
29
|
+
sessionId: '<%= @flow_session.id %>',
|
|
30
|
+
answers: <%= raw @result.to_json %>
|
|
31
|
+
}, '*');
|
|
32
|
+
</script>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<h1>Start a New Flow</h1>
|
|
2
|
+
|
|
3
|
+
<% if @definitions.any? %>
|
|
4
|
+
<div class="fe-definition-list">
|
|
5
|
+
<% @definitions.each do |definition| %>
|
|
6
|
+
<div class="fe-definition-card">
|
|
7
|
+
<h2><%= definition.name %> <small>v<%= definition.version %></small></h2>
|
|
8
|
+
<%= link_to "Start",
|
|
9
|
+
flow_engine.sessions_path(definition_id: definition.id, **(@embed_mode ? { embed: "true" } : {})),
|
|
10
|
+
class: "fe-btn fe-btn--primary",
|
|
11
|
+
data: { turbo_method: :post } %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<% else %>
|
|
16
|
+
<p>No active flow definitions available.</p>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="fe-session" data-controller="step">
|
|
2
|
+
<div class="fe-progress" data-controller="progress" data-progress-value="<%= @progress %>">
|
|
3
|
+
<div class="fe-progress__bar" style="width: <%= @progress %>%"></div>
|
|
4
|
+
</div>
|
|
5
|
+
<span class="fe-progress__text">Step <%= @progress %>% complete</span>
|
|
6
|
+
|
|
7
|
+
<% if @node %>
|
|
8
|
+
<div class="fe-step">
|
|
9
|
+
<h2 class="fe-step__question"><%= @node.question %></h2>
|
|
10
|
+
|
|
11
|
+
<%= form_with url: flow_engine.session_path(@flow_session, **embed_mode? ? { embed: "true" } : {}),
|
|
12
|
+
method: :patch,
|
|
13
|
+
local: true,
|
|
14
|
+
data: { step_target: "form" } do |form| %>
|
|
15
|
+
|
|
16
|
+
<%= render_step(@node, form) %>
|
|
17
|
+
|
|
18
|
+
<div class="fe-step__actions">
|
|
19
|
+
<%= form.submit "Continue", class: "fe-btn fe-btn--primary", data: { step_target: "submit" } %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% else %>
|
|
24
|
+
<p>This session has no current step.</p>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="fe-field fe-field--boolean">
|
|
2
|
+
<label class="fe-radio">
|
|
3
|
+
<input type="radio" name="answer" value="true">
|
|
4
|
+
<span>Yes</span>
|
|
5
|
+
</label>
|
|
6
|
+
<label class="fe-radio">
|
|
7
|
+
<input type="radio" name="answer" value="false">
|
|
8
|
+
<span>No</span>
|
|
9
|
+
</label>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="fe-field fe-field--number-matrix">
|
|
2
|
+
<% (node.fields || []).each do |field_name| %>
|
|
3
|
+
<div class="fe-matrix-row">
|
|
4
|
+
<label class="fe-matrix-row__label" for="answer_fields_<%= field_name %>"><%= field_name %></label>
|
|
5
|
+
<input type="number"
|
|
6
|
+
name="answer_fields[<%= field_name %>]"
|
|
7
|
+
id="answer_fields_<%= field_name %>"
|
|
8
|
+
value="0"
|
|
9
|
+
min="0"
|
|
10
|
+
class="fe-input fe-input--number">
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="fe-field fe-field--text">
|
|
2
|
+
<textarea name="answer"
|
|
3
|
+
rows="6"
|
|
4
|
+
class="fe-input fe-input--textarea"
|
|
5
|
+
placeholder="<%= node.decorations&.dig(:placeholder) || "Type your answer here..." %>"
|
|
6
|
+
<% if node.decorations&.dig(:maxlength) %>maxlength="<%= node.decorations[:maxlength] %>"<% end %>
|
|
7
|
+
></textarea>
|
|
8
|
+
<% if node.decorations&.dig(:hint) %>
|
|
9
|
+
<p class="fe-field__hint"><%= node.decorations[:hint] %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>FlowEngine</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap">
|
|
12
|
+
<link rel="stylesheet" href="<%= flow_engine.root_path %>assets/flow_engine/application.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div class="fe-container">
|
|
16
|
+
<% if flash[:notice] %>
|
|
17
|
+
<div class="fe-flash fe-flash--notice"><%= flash[:notice] %></div>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% if flash[:alert] %>
|
|
20
|
+
<div class="fe-flash fe-flash--alert"><%= flash[:alert] %></div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<%= yield %>
|
|
24
|
+
</div>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap">
|
|
10
|
+
<link rel="stylesheet" href="<%= flow_engine.root_path %>assets/flow_engine/application.css">
|
|
11
|
+
<style>
|
|
12
|
+
body { margin: 0; padding: 0; background: transparent; }
|
|
13
|
+
.fe-container { --fe-max-width: 100%; padding: var(--fe-space-md); }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="fe-container">
|
|
18
|
+
<%= yield %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
function postResize() {
|
|
23
|
+
var height = document.body.scrollHeight;
|
|
24
|
+
window.parent.postMessage({ type: 'flowengine:resize', height: height }, '*');
|
|
25
|
+
}
|
|
26
|
+
window.addEventListener('load', postResize);
|
|
27
|
+
new MutationObserver(postResize).observe(document.body, { childList: true, subtree: true });
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FlowEngine::Rails::Engine.routes.draw do
|
|
4
|
+
resources :sessions, only: %i[new create show update] do
|
|
5
|
+
member do
|
|
6
|
+
get :completed
|
|
7
|
+
patch :abandon
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
namespace :admin do
|
|
12
|
+
resources :definitions do
|
|
13
|
+
member do
|
|
14
|
+
post :activate
|
|
15
|
+
post :deactivate
|
|
16
|
+
get :mermaid
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
root to: "sessions#new"
|
|
22
|
+
end
|