easy_flow 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/easy_flow/flows_controller.rb +5 -4
- data/app/models/easy_flow/choice.rb +3 -3
- data/app/models/easy_flow/question_runner.rb +2 -0
- data/app/models/easy_flow/runner.rb +4 -4
- data/app/models/easy_flow/step_type.rb +4 -2
- data/app/models/easy_flow/steps/checklist.rb +24 -0
- data/app/models/easy_flow/steps/question.rb +2 -1
- data/app/views/easy_flow/flows/complete.html.erb +3 -3
- data/app/views/easy_flow/flows/step.html.erb +9 -3
- data/app/views/easy_flow/manage/flows/show.html.erb +3 -3
- data/app/views/easy_flow/manage/versions/index.html.erb +6 -6
- data/app/views/easy_flow/steps/_choosing.html.erb +2 -2
- data/app/views/easy_flow/steps/_ticking.html.erb +10 -0
- data/lib/easy_flow/engine.rb +1 -0
- data/lib/easy_flow/version.rb +1 -1
- data/the_local/agents/easy_flow-develop.md +45 -19
- data/the_local/agents/easy_flow-info.md +15 -6
- data/the_local/agents/easy_flow-install.md +19 -11
- data/the_local/interface.yml +8 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d808d59c87ec6f8b6eee1e9032bdab397bf4df8035e6248f24f1d404db59b8bb
|
|
4
|
+
data.tar.gz: ad2551396fcf8348fb87270287e31d29780514374877735e31fc50ffe8c63d77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d0a900d84901dab3444fc04eb9bd84cf12cad6d4eaa463d11c3ecb72e904f02ad48be7882e50965b275c920e7134023f298224c56f679b0f903fa74b8189271
|
|
7
|
+
data.tar.gz: 6c8b9bf2f4c7aa3581d9b445ce01ba203646e8023a7107266d6d6ab8e94e536b29c2af38ff0b4dd415745d80160da5a9c2906680f227f9674a21665edece86aa
|
|
@@ -15,7 +15,7 @@ module EasyFlow
|
|
|
15
15
|
@progress = progress
|
|
16
16
|
@guide.run(@progress)
|
|
17
17
|
@answers = @progress.recorded
|
|
18
|
-
@question = @guide.next_step(@answers)
|
|
18
|
+
@question = @guide.next_step(@answers, run: run)
|
|
19
19
|
@drawing = @guide.drawing_at(@answers)
|
|
20
20
|
flash.now[:alert] = @refused if @refused
|
|
21
21
|
@waiting = waiting_on(@question)
|
|
@@ -106,7 +106,8 @@ module EasyFlow
|
|
|
106
106
|
|
|
107
107
|
def submitted_answers
|
|
108
108
|
given = params.fetch(:answers, {})
|
|
109
|
-
|
|
109
|
+
keys = given.keys.select { |key| @guide.step(key) }
|
|
110
|
+
answers = given.permit(*keys, **keys.index_with { [] }).to_h.symbolize_keys.transform_values { |value| value.is_a?(Array) ? value.compact_blank : value }
|
|
110
111
|
return one_answer_back(answers) if params[:back].present?
|
|
111
112
|
|
|
112
113
|
key = params[:asked].to_s.to_sym
|
|
@@ -124,14 +125,14 @@ module EasyFlow
|
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
def record_submitted
|
|
127
|
-
id, value = params.fetch(:answers, {}).permit(*asked).to_h.first
|
|
128
|
+
id, value = params.fetch(:answers, {}).permit(*asked, **asked.index_with { [] }).to_h.first
|
|
128
129
|
id ||= params[:asked].presence_in(asked)
|
|
129
130
|
return if id.nil?
|
|
130
131
|
|
|
131
132
|
problem = answer_problem(runner_for(run.pinned_definition).step(id.to_s), value)
|
|
132
133
|
return flash[:alert] = problem if problem
|
|
133
134
|
|
|
134
|
-
progress.record(id, value.to_s)
|
|
135
|
+
progress.record(id, value.is_a?(Array) ? value.compact_blank : value.to_s)
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
def waiting_on(step)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module EasyFlow
|
|
2
|
-
Choice = Data.define(:value, :label, :hint) do
|
|
3
|
-
def initialize(value:, label: nil, hint: nil)
|
|
4
|
-
super(value: value, label: label.presence || value, hint: hint)
|
|
2
|
+
Choice = Data.define(:value, :label, :hint, :info) do
|
|
3
|
+
def initialize(value:, label: nil, hint: nil, info: nil)
|
|
4
|
+
super(value: value, label: label.presence || value, hint: hint, info: info)
|
|
5
5
|
end
|
|
6
6
|
end
|
|
7
7
|
end
|
|
@@ -5,6 +5,8 @@ module EasyFlow
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def choice_label(id, value)
|
|
8
|
+
return value.map { |ticked| choice_label(id, ticked) }.to_sentence if value.is_a?(Array)
|
|
9
|
+
|
|
8
10
|
chosen = Steps::Question.choices_in(step(id)).find { |choice| choice.value == value }
|
|
9
11
|
|
|
10
12
|
chosen&.label.presence || value
|
|
@@ -22,8 +22,8 @@ module EasyFlow
|
|
|
22
22
|
@digest.step(id.to_s)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def next_step(state)
|
|
26
|
-
shown(@digest.next_step(named(state)))
|
|
25
|
+
def next_step(state, run: nil)
|
|
26
|
+
shown(@digest.next_step(named(state)), run)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def run(progress)
|
|
@@ -66,10 +66,10 @@ module EasyFlow
|
|
|
66
66
|
acts?(node) ? @registry.fetch(node.type).process(node, state) : true
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
def shown(node)
|
|
69
|
+
def shown(node, run = nil)
|
|
70
70
|
return node unless node && @registry.registered?(node.type)
|
|
71
71
|
|
|
72
|
-
@registry.fetch(node.type).display_of(node) || node
|
|
72
|
+
@registry.fetch(node.type).display_of(node, run) || node
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def named(state)
|
|
@@ -26,8 +26,10 @@ module EasyFlow
|
|
|
26
26
|
@readiness = readiness
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def display_of(node)
|
|
30
|
-
@display
|
|
29
|
+
def display_of(node, run = nil)
|
|
30
|
+
return unless @display
|
|
31
|
+
|
|
32
|
+
@display.arity == 1 ? @display.call(node) : @display.call(node, run)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def process(node, state)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module EasyFlow
|
|
2
|
+
module Steps
|
|
3
|
+
class Checklist
|
|
4
|
+
include Step
|
|
5
|
+
|
|
6
|
+
step_name "Checklist"
|
|
7
|
+
|
|
8
|
+
setting :question, type: :string
|
|
9
|
+
setting :required, type: :boolean
|
|
10
|
+
setting :answers, type: :list, required: true do
|
|
11
|
+
setting :value, type: :string
|
|
12
|
+
setting :label, type: :string
|
|
13
|
+
setting :info, type: :string
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
names_by :question
|
|
17
|
+
awaits_input
|
|
18
|
+
drawn_by "easy_flow/steps/ticking"
|
|
19
|
+
answer_check { |node, value| "Tick at least one to go on." if node.config["required"] && Array(value).compact_blank.empty? }
|
|
20
|
+
|
|
21
|
+
displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"], choices: Question.choices_in(node)) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -13,6 +13,7 @@ module EasyFlow
|
|
|
13
13
|
setting :label, type: :string
|
|
14
14
|
setting :weight, type: :integer
|
|
15
15
|
setting :hint, type: :string
|
|
16
|
+
setting :info, type: :string
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
output :answer, type: :string, label: "Answer", values: ->(node) { Question.offered(node.config) }
|
|
@@ -44,7 +45,7 @@ module EasyFlow
|
|
|
44
45
|
def self.choice_from(answer)
|
|
45
46
|
return Choice.new(value: answer) unless answer.is_a?(Hash)
|
|
46
47
|
|
|
47
|
-
Choice.new(value: answer["value"], label: answer["label"], hint: answer["hint"])
|
|
48
|
+
Choice.new(value: answer["value"], label: answer["label"], hint: answer["hint"], info: answer["info"])
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def self.category_of(step)
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
<% end %>
|
|
11
11
|
|
|
12
12
|
<%= ui_section do %>
|
|
13
|
-
<ul class="divide-y divide-
|
|
13
|
+
<ul class="divide-y border-y divide-[var(--ks-color-divider)] border-[var(--ks-color-divider)] dark:divide-[var(--ks-color-divider-dark)] dark:border-[var(--ks-color-divider-dark)]">
|
|
14
14
|
<% @answered.each do |step, value| %>
|
|
15
15
|
<li data-answer="<%= step %>" class="flex items-baseline justify-between gap-6 py-3">
|
|
16
|
-
<span class="text-
|
|
17
|
-
<span class="shrink-0 font-medium text-
|
|
16
|
+
<span class="text-[var(--ks-color-text-muted)] dark:text-[var(--ks-color-text-muted-dark)]"><%= @guide.question_text(step) %></span>
|
|
17
|
+
<span class="shrink-0 font-medium text-[var(--ks-color-text)] dark:text-[var(--ks-color-text-dark)]"><%= @guide.choice_label(step, value) %></span>
|
|
18
18
|
</li>
|
|
19
19
|
<% end %>
|
|
20
20
|
</ul>
|
|
@@ -12,13 +12,19 @@
|
|
|
12
12
|
<% end %>
|
|
13
13
|
|
|
14
14
|
<% if @waiting %>
|
|
15
|
-
<p class="
|
|
15
|
+
<p class="ks-hint">Waiting for <%= @waiting %>.</p>
|
|
16
16
|
<% else %>
|
|
17
17
|
<%= form_with url: step_form[:url], method: step_form[:method], data: { turbo: false } do %>
|
|
18
18
|
<%= hidden_field_tag :asked, @question.id %>
|
|
19
19
|
<% if carries_answers? %>
|
|
20
20
|
<% @answers.each do |key, value| %>
|
|
21
|
-
|
|
21
|
+
<% if value.is_a?(Array) %>
|
|
22
|
+
<% (value.presence || [ "" ]).each do |ticked| %>
|
|
23
|
+
<%= hidden_field_tag "answers[#{key}][]", ticked, id: nil %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% else %>
|
|
26
|
+
<%= hidden_field_tag "answers[#{key}]", value %>
|
|
27
|
+
<% end %>
|
|
22
28
|
<% end %>
|
|
23
29
|
<% end %>
|
|
24
30
|
|
|
@@ -28,7 +34,7 @@
|
|
|
28
34
|
<%= ui_button(label: "Next", type: :submit) %>
|
|
29
35
|
|
|
30
36
|
<% if @answers.any? %>
|
|
31
|
-
<button type="submit" name="back" value="1" formnovalidate class="cursor-pointer
|
|
37
|
+
<button type="submit" name="back" value="1" formnovalidate class="ks-hint cursor-pointer underline underline-offset-2">← Back</button>
|
|
32
38
|
<% end %>
|
|
33
39
|
</div>
|
|
34
40
|
<% end %>
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
6
|
<div class="flex h-screen flex-col overflow-hidden">
|
|
7
|
-
<header class="flex shrink-0 items-center justify-between gap-4 border-b border-
|
|
7
|
+
<header class="flex shrink-0 items-center justify-between gap-4 border-b border-[var(--ks-color-divider)] dark:border-[var(--ks-color-divider-dark)] px-6 py-3">
|
|
8
8
|
<div>
|
|
9
|
-
<h1 class="text-lg font-semibold text-
|
|
10
|
-
<p class="text-sm text-
|
|
9
|
+
<h1 class="text-lg font-semibold text-[var(--ks-color-text)] dark:text-[var(--ks-color-text-dark)]" data-flow-heading><%= @flow.title.presence || @flow.slug %></h1>
|
|
10
|
+
<p class="text-sm text-[var(--ks-color-text-muted)] dark:text-[var(--ks-color-text-muted-dark)]"><%= @flow.slug %></p>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="flex items-center gap-2">
|
|
13
13
|
<button type="button" data-open-panel
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<%= ui_page_header(title: "History", subtitle: @flow.title.presence || @flow.slug) %>
|
|
7
7
|
|
|
8
8
|
<%= ui_section do %>
|
|
9
|
-
<ul class="divide-y divide-
|
|
9
|
+
<ul class="divide-y border-y divide-[var(--ks-color-divider)] border-[var(--ks-color-divider)] dark:divide-[var(--ks-color-divider-dark)] dark:border-[var(--ks-color-divider-dark)]">
|
|
10
10
|
<% @versions.each do |version| %>
|
|
11
11
|
<li data-version="<%= version.number %>" class="py-4">
|
|
12
12
|
<div class="flex items-baseline justify-between gap-4">
|
|
13
|
-
<span class="font-medium text-
|
|
14
|
-
<span class="text-sm text-
|
|
13
|
+
<span class="font-medium text-[var(--ks-color-text)] dark:text-[var(--ks-color-text-dark)]">Version <%= version.number %></span>
|
|
14
|
+
<span class="text-sm text-[var(--ks-color-text-muted)] dark:text-[var(--ks-color-text-muted-dark)]"><%= version.created_at.to_fs(:long) %></span>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<% if version == @flow.live_version %>
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
<% if version.changes.any? %>
|
|
22
22
|
<ul class="mt-2 space-y-0.5">
|
|
23
23
|
<% version.changes.each do |change| %>
|
|
24
|
-
<li data-captured class="text-sm text-
|
|
24
|
+
<li data-captured class="text-sm text-[var(--ks-color-text-muted)] dark:text-[var(--ks-color-text-muted-dark)]"><%= EasyFlow::Change.phrase(change) %></li>
|
|
25
25
|
<% end %>
|
|
26
26
|
</ul>
|
|
27
27
|
<% else %>
|
|
28
|
-
<p class="mt-2 text-sm text-
|
|
28
|
+
<p class="mt-2 text-sm text-[var(--ks-color-text-muted)] dark:text-[var(--ks-color-text-muted-dark)]">Nothing was recorded against this version.</p>
|
|
29
29
|
<% end %>
|
|
30
30
|
|
|
31
31
|
<% unless version == @versions.first %>
|
|
32
32
|
<%= button_to "Return the flow to this version",
|
|
33
33
|
flow_routes.return_manage_flow_version_path(@flow, version),
|
|
34
34
|
form: { data: { turbo: false } }, data: { return: true },
|
|
35
|
-
class: "
|
|
35
|
+
class: "ks-button ks-button-secondary ks-button-sm mt-3" %>
|
|
36
36
|
<% end %>
|
|
37
37
|
</li>
|
|
38
38
|
<% end %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<fieldset>
|
|
2
|
-
<legend class="mb-5 text-xl font-semibold leading-snug
|
|
2
|
+
<legend class="ks-section-title mb-5 text-xl font-semibold leading-snug"><%= step.text %></legend>
|
|
3
3
|
|
|
4
4
|
<div class="space-y-3">
|
|
5
5
|
<% step.choices.each do |choice| %>
|
|
6
|
-
<%= ui_radio_card(name: "answers[#{step.id}]", value: choice.value, label: choice.label.presence || choice.value, hint: choice.hint) %>
|
|
6
|
+
<%= ui_radio_card(name: "answers[#{step.id}]", value: choice.value, label: choice.label.presence || choice.value, hint: choice.hint, info: choice.info) %>
|
|
7
7
|
<% end %>
|
|
8
8
|
</div>
|
|
9
9
|
</fieldset>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<fieldset>
|
|
2
|
+
<legend class="ks-section-title mb-5 text-xl font-semibold leading-snug"><%= step.text %></legend>
|
|
3
|
+
|
|
4
|
+
<%= hidden_field_tag "answers[#{step.id}][]", "", id: nil %>
|
|
5
|
+
<div class="space-y-3">
|
|
6
|
+
<% step.choices.each do |choice| %>
|
|
7
|
+
<%= ui_checkbox_row(name: "answers[#{step.id}][]", value: choice.value, label: choice.label.presence || choice.value, info: choice.info) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
</fieldset>
|
data/lib/easy_flow/engine.rb
CHANGED
data/lib/easy_flow/version.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: easy_flow-develop
|
|
3
|
-
description: Use PROACTIVELY for adding a step type to easy_flow flows (a step that asks the visitor something, computes a value from earlier answers, picks the next branch, or holds the visitor until something outside the flow has happened), moving a paused run on once what it waits for has happened, refusing a blank or invalid answer to a step with a message, offering an admin a step setting whose options are read from the app's own records, serving a host's flows from the app's own controller and routes, acting when a visitor finishes a flow, and reading a run's recorded answers and question labels, including every visit's answer when a flow loops back to a question already asked — MUST BE USED instead of hand-rolling questionnaire steps, branching logic, number comparisons, hard-coded setting options, answer validation, polling or "come back later" pages, flow controllers or answer lookups.
|
|
3
|
+
description: Use PROACTIVELY for adding a step type to easy_flow flows (a step that asks the visitor something, takes several answers at once, computes a value from earlier answers, picks the next branch, or holds the visitor until something outside the flow has happened), showing a step differently depending on the visitor's stored run, branching on what a visitor ticked on a checklist, moving a paused run on once what it waits for has happened, refusing a blank or invalid answer to a step with a message, offering an admin a step setting whose options are read from the app's own records, serving a host's flows from the app's own controller and routes, acting when a visitor finishes a flow, and reading a run's recorded answers and question labels, including every answer ticked on a checklist and every visit's answer when a flow loops back to a question already asked — MUST BE USED instead of hand-rolling questionnaire steps, checkbox lists, branching logic, number comparisons, hard-coded setting options, answer validation, polling or "come back later" pages, flow controllers or answer lookups.
|
|
4
4
|
tools: Read, Write, Edit, Grep
|
|
5
5
|
scope: guided flows — versioned documents of steps and the connections between them, drawn on a canvas by an admin and run by a visitor one step at a time, with step types the host registers
|
|
6
6
|
---
|
|
@@ -9,7 +9,7 @@ This local follows the steps below exactly and invents none. Where a step names
|
|
|
9
9
|
|
|
10
10
|
## What easy_flow is
|
|
11
11
|
|
|
12
|
-
A Rails engine for flows an admin draws on a canvas and a visitor runs one step at a time. Each step is an instance of a step type. The engine ships
|
|
12
|
+
A Rails engine for flows an admin draws on a canvas and a visitor runs one step at a time. Each step is an instance of a step type. The engine ships seven: start (`:start`), end (`:terminal`), question (`:question`, a question with a list of answers of which the visitor picks one, which the admin can mark required so a blank answer is refused), checklist (`:checklist`, a question with a list of answers of which the visitor ticks any number, which the admin can mark required so a visitor who ticks nothing is refused), and three that pick a branch from an earlier answer with no code — condition (`:condition`, is or is not a chosen value), switch (`:switch`, follows the connection labelled with the answer) and compare (`:compare`, reads the answer as a number and checks it by more than, less than, at least or at most against an amount). On a question and on a checklist the admin can give each answer info, a longer explanation the visitor opens with an info button beside that answer. An admin may connect a step back to an earlier one, so a flow can ask the same question more than once, and each visit's answer is kept. Before writing a step type to ask for one answer or several, or to branch on an answer or a number, ask the developer whether an admin placing one of the built-in types on the canvas is enough. None of condition, switch or compare reads a checklist's answer, so branching on what a visitor ticked takes a step type of the app's own that defines `route`. Use this local when the app needs a step type of its own, needs a host's flows on its own pages with its own behaviour when a visitor finishes, or needs to read what a visitor answered. It assumes easy_flow is already installed and a host is declared; if not, hand off to `easy_flow-install` first.
|
|
13
13
|
|
|
14
14
|
## Interface
|
|
15
15
|
|
|
@@ -31,7 +31,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
31
31
|
- It only picks which connection to follow — define `route`.
|
|
32
32
|
- It holds the visitor until something outside the flow has happened, such as a payment arriving or a reviewer approving — declare `waits_until`.
|
|
33
33
|
A type may both `process` and `route`. A type that does none of the four is passed through without stopping when a visitor reaches it.
|
|
34
|
-
2. Create the class in the app, for example `app/models/flow_steps/rating.rb`. The class name, underscored, is the type's id (`Rating` becomes `:rating`), and that id is stored in every flow that uses it, so it must not change after admins start using the type. It must not be one of the built-in ids `start`, `terminal`, `question`, `condition`, `switch` or `compare`:
|
|
34
|
+
2. Create the class in the app, for example `app/models/flow_steps/rating.rb`. The class name, underscored, is the type's id (`Rating` becomes `:rating`), and that id is stored in every flow that uses it, so it must not change after admins start using the type. It must not be one of the built-in ids `start`, `terminal`, `question`, `checklist`, `condition`, `switch` or `compare`:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
37
|
module FlowSteps
|
|
@@ -81,10 +81,10 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
81
81
|
- `names_by :setting` or `names_by { |node| ... }` — what the step is called on the canvas, from a setting or computed.
|
|
82
82
|
- `awaits_input` — the visitor is shown this step and submits an answer to it.
|
|
83
83
|
- `waits_until { |node, state| ... }` — the run stops at this step until the block returns a truthy value. See step 8.
|
|
84
|
-
- `answer_check { |node, value| ... }` — checks a submitted answer before it is recorded. `value` is the submitted string, or `nil` when the input sent nothing. Return a message to refuse the answer, or `nil` to accept it. Without it every answer is accepted, including a blank one.
|
|
84
|
+
- `answer_check { |node, value| ... }` — checks a submitted answer before it is recorded. `value` is the submitted string, an array of strings when the input submits several values, or `nil` or `""` when the input sent nothing. An array may hold blank strings. Return a message to refuse the answer, or `nil` to accept it. Without it every answer is accepted, including a blank one.
|
|
85
85
|
- `ends_here` / `begins_here` — marks the type as an end or a start of a flow.
|
|
86
|
-
- `displays_by { |node| ... }` — builds the object handed to the step's partial as the local `step`. Without it the partial receives the node itself.
|
|
87
|
-
- `drawn_by "<partial>"` — the partial that draws this type for a visitor. Without it the host's default drawing is used, which expects `step.id`, `step.text` and `step.choices`, so a type with its own display shape needs its own partial.
|
|
86
|
+
- `displays_by { |node| ... }` or `displays_by { |node, run| ... }` — builds the object handed to the step's partial as the local `step`. Without it the partial receives the node itself. A block that takes two arguments is also given the visitor's stored `EasyFlow::Run`, which is `nil` when the flow keeps no stored run at this point and in an admin's preview, so the block must handle `nil`. The block is also called with a `nil` run once for each step already answered every time a step page is shown, to count progress, so it must not be slow and must not change anything.
|
|
87
|
+
- `drawn_by "<partial>"` — the partial that draws this type for a visitor. Without it the host's default drawing is used, which draws one answer to pick and expects `step.id`, `step.text` and `step.choices`, each choice answering `value`, `label`, `hint` and `info`, so a type with its own display shape needs its own partial.
|
|
88
88
|
6. For a type that computes or routes, define instance methods on the class (or `process { |node, state| ... }` / `route { |node, state| ... }` in the block form):
|
|
89
89
|
|
|
90
90
|
```ruby
|
|
@@ -98,10 +98,22 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
- `node` has `id`, `type` and `config`; `config` is a hash of the admin's settings with string keys.
|
|
101
|
-
- `state` is the answers recorded so far, keyed by step id as strings.
|
|
101
|
+
- `state` is the answers recorded so far, keyed by step id as strings. An answer is a string, or an array of strings for a step that takes several values, such as a checklist.
|
|
102
102
|
- `process` returns the value recorded under this step's id. It runs as soon as a visitor reaches the step, before the next step is shown.
|
|
103
103
|
- A type that declares more than one `output` returns a hash from `process`, keyed by each output's name as a string. A later compare step reads the output the admin picks from that hash.
|
|
104
|
-
- `route` returns the value that picks the connection to follow; it is compared as a string with the value each leaving connection is labelled with, so `false` follows the connection labelled `false`. Returning `nil` follows the first connection. A route that returns `true` or `false` declares `output :result, type: :boolean, values: [true, false]`.
|
|
104
|
+
- `route` returns the value that picks the connection to follow; it is compared as a string with the value each leaving connection is labelled with, so `false` follows the connection labelled `false`. Returning `nil` follows the first connection. A returned value that matches no connection's label ends the flow at that step. A route that returns `true` or `false` declares `output :result, type: :boolean, values: [true, false]`.
|
|
105
|
+
- To branch on a checklist, read its array and return one value. Ask the developer what decides the branch, such as one given answer being ticked, any of several, or how many were ticked:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
setting :step, type: :previous_step
|
|
109
|
+
setting :answer, type: :string, required: true
|
|
110
|
+
|
|
111
|
+
output :result, type: :boolean, values: [true, false]
|
|
112
|
+
|
|
113
|
+
def route(node, state)
|
|
114
|
+
Array(state[node.config["step"]]).include?(node.config["answer"])
|
|
115
|
+
end
|
|
116
|
+
```
|
|
105
117
|
- In a flow that loops, the first visit to a step is recorded under its id and each later visit under `<id>@<n>`, where `n` counts from 2. `route` is given the latest visit's answer under the plain step id. `process` and `waits_until` are given the answers as recorded, so the plain id holds the first visit's answer and later visits are under `<id>@2`, `<id>@3` and on. A `process` step reached again records its result under its own `<id>@<n>`. When a computing or waiting type reads an earlier step that can be asked again, ask the developer whether it should read the first visit or the latest.
|
|
106
118
|
7. For a type that awaits input, write the partial named in `drawn_by`, for example `app/views/flow_steps/_rating.html.erb`. It receives the display object as `step`. It renders only the input, since the page supplies the form, the Next button and the Back button. The input must be named `answers[<step id>]`, or the answer is not recorded:
|
|
107
119
|
|
|
@@ -112,11 +124,20 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
112
124
|
|
|
113
125
|
Name the input from `step.id`, never from a stored or hard-coded id. On a later visit to the step in a loop, the node's id is `<id>@<n>`, and the answer is only recorded against that visit when the input is named with it. A `displays_by` block must pass `node.id` through as the display object's `id` for the same reason.
|
|
114
126
|
|
|
115
|
-
|
|
127
|
+
Ask the developer whether the step takes one answer or several. An input named `answers[<step id>]` submits one value, recorded as the string the visitor submitted. Inputs named `answers[<step id>][]`, such as checkboxes, submit several, recorded as an array of strings with blank entries removed. For several, put a hidden blank field of the same name before the inputs, so that a visitor who ticks nothing records an empty array and not `""`:
|
|
128
|
+
|
|
129
|
+
```erb
|
|
130
|
+
<%= hidden_field_tag "answers[#{step.id}][]", "", id: nil %>
|
|
131
|
+
<% step.choices.each do |choice| %>
|
|
132
|
+
<%= check_box_tag "answers[#{step.id}][]", choice.value, false, id: nil %>
|
|
133
|
+
<% end %>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
An input that submits a hash, such as one named `answers[<step id>][key]`, is dropped and treated as blank.
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
A compare step reads a typed answer such as `"12"` as the number it spells, and an answer that is missing, is an array or is not a number makes it follow its `false` connection.
|
|
118
139
|
|
|
119
|
-
A blank answer is recorded as `""` and the visitor moves on, unless the type's `answer_check` refuses it. When the check returns a message, nothing is recorded and the same step is shown again with that message. This holds whether the flow keeps a stored run or carries its answers in the page, and the page supplies what it needs for both, so the partial adds nothing for it. Ask the developer whether the visitor may leave this step blank, and whether that is fixed for the type or chosen per step by the admin. For a per-step choice, declare a `:boolean` setting and read it in the check:
|
|
140
|
+
A blank single answer is recorded as `""` and the visitor moves on, unless the type's `answer_check` refuses it. For a type that takes several answers, refuse nothing ticked with `Array(value).compact_blank.empty?`, since `value.blank?` is false for an array holding only the hidden blank field. When the check returns a message, nothing is recorded and the same step is shown again with that message. This holds whether the flow keeps a stored run or carries its answers in the page, and the page supplies what it needs for both, so the partial adds nothing for it. Ask the developer whether the visitor may leave this step blank, and whether that is fixed for the type or chosen per step by the admin. For a per-step choice, declare a `:boolean` setting and read it in the check:
|
|
120
141
|
|
|
121
142
|
```ruby
|
|
122
143
|
setting :required, type: :boolean
|
|
@@ -178,7 +199,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
178
199
|
|
|
179
200
|
The controller inherits from the base controller set at install, uses the host's layout, visitor authorization and refusal methods, and only finds flows that belong to the named host. Pages it does not override use the engine's own views.
|
|
180
201
|
4. Override only the private methods the developer's answers call for:
|
|
181
|
-
- `finished(answers, run)` — called when the visitor reaches the end. `answers` is the answers on the path taken, keyed by step id as symbols. `run` is the stored `EasyFlow::Run`, or `nil` when the admin set the flow to save nothing. A flow the admin set to save on finish gets its run created at this point. It must render or redirect. Default: the engine's completion page.
|
|
202
|
+
- `finished(answers, run)` — called when the visitor reaches the end. `answers` is the answers on the path taken, keyed by step id as symbols, each a string or, for a checklist, an array of strings. `run` is the stored `EasyFlow::Run`, or `nil` when the admin set the flow to save nothing. A flow the admin set to save on finish gets its run created at this point. It must render or redirect. Default: the engine's completion page, which lists each answer on the path taken by its label, a checklist's answer as the labels of everything ticked joined into one sentence.
|
|
182
203
|
- `start_run(flow)` — creates the run when a visitor starts a flow the admin set to save each step. Call `super` and change the run it returns, for example to set its `owner` or `label`.
|
|
183
204
|
- `runner_for(definition)` — returns the runner used for each step. Return a subclass of `EasyFlow::QuestionRunner` to change what the step and completion pages read from it.
|
|
184
205
|
5. Visit `/<path>/<slug>` for a published flow and walk it to the end.
|
|
@@ -187,7 +208,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
187
208
|
|
|
188
209
|
1. Find runs scoped to one host so one host never sees another's: `EasyFlow::Run.joins(:flow).where(easy_flow_definitions: { host: "intake" })`.
|
|
189
210
|
2. Read from a run:
|
|
190
|
-
- `run.recorded` — the answers so far, a hash keyed by step id as symbols.
|
|
211
|
+
- `run.recorded` — the answers so far, a hash keyed by step id as symbols. A value is a string, or an array of strings for a checklist or any step that takes several answers, empty when nothing was ticked.
|
|
191
212
|
- `run.flow` — the flow it belongs to.
|
|
192
213
|
- `run.owner` — the optional record the run belongs to, polymorphic, set by the app. `run.label` and `run.status` are free string columns for the app's own use.
|
|
193
214
|
- `run.pinned_definition` — the flow document of the version the run started on.
|
|
@@ -203,15 +224,17 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
203
224
|
```
|
|
204
225
|
|
|
205
226
|
- `state_on_path(answers)` — only the answers on the path the visitor actually took, in the order they were given, dropping answers left behind by going back. In a flow that loops, it holds every visit: the first under the step id and each later one under `:"<id>@<n>"`, such as `:"job@2"`. Ask the developer whether each visit is shown as its own line or the visits to one question are grouped together; to group them, take the part of the key before `@`.
|
|
206
|
-
- `question_text(id)` — the text of a question step, given either its id or a visit key such as `"job@2"`.
|
|
207
|
-
- `choice_label(id, value)` — the label of
|
|
227
|
+
- `question_text(id)` — the text of a question or checklist step, given either its id or a visit key such as `"job@2"`. For a step of any other type it returns that step's `question` setting, or `nil` when it has none.
|
|
228
|
+
- `choice_label(id, value)` — the label of one chosen answer on a question or checklist, or the value itself when there is no label. It takes a visit key the same way as `question_text`. Handed a checklist's array, it returns one string, the label of each ticked value joined into a sentence such as `Email, Phone, and Post`, and an empty string when nothing was ticked. Ask the developer whether a checklist's answers are shown as that one sentence or each on its own; for each on its own, call it once per ticked value with `value.map { |ticked| runner.choice_label(step_id, ticked) }`.
|
|
208
229
|
- `step(id)` finds the step for either its id or a visit key.
|
|
209
|
-
- `
|
|
230
|
+
- `next_step(answers, run: nil)` — the next step to show, built by its type's `displays_by`. Pass `run:` when a type's display block takes the run.
|
|
231
|
+
- `steps_on_path(answers)` — the steps on the path taken, each built by its type's `displays_by` with a `nil` run.
|
|
232
|
+
- `steps`, `slug` and `headline` read the rest of the document.
|
|
210
233
|
|
|
211
234
|
## Conventions
|
|
212
235
|
|
|
213
236
|
- A step type's id comes from its class name or the id passed to `EasyFlow.step`, and it is stored in flow documents, so renaming the class or id breaks every flow that uses it.
|
|
214
|
-
- Never give a step type one of the built-in ids: `start`, `terminal`, `question`, `condition`, `switch`, `compare`.
|
|
237
|
+
- Never give a step type one of the built-in ids: `start`, `terminal`, `question`, `checklist`, `condition`, `switch`, `compare`.
|
|
215
238
|
- Register class-based step types inside `to_prepare`. A type registered anywhere else is lost on code reload in development.
|
|
216
239
|
- Registering a type does not put it in the palette of a host that names a list of offered step types without the type's id. Which step types a host offers is set through `easy_flow-install`, not here.
|
|
217
240
|
- A type that declares `ends_here` is in every host's palette, and a type that declares `begins_here` is in no host's palette.
|
|
@@ -221,8 +244,11 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
221
244
|
- Always look flows and runs up through a host.
|
|
222
245
|
- In a flow that loops, a later visit's answer is keyed `<id>@<n>`, so code that reads answers never assumes one answer per step id. A step id may not contain `@`, and a flow with one is refused when published.
|
|
223
246
|
- A loop that comes back to a step with no answer given since the last visit to it ends the flow there, so every loop needs a step that awaits input.
|
|
224
|
-
- A step's input field is named `answers[<step.id>]`
|
|
225
|
-
- An answer
|
|
247
|
+
- A step's input field is named `answers[<step.id>]` for one value or `answers[<step.id>][]` for several. Any other name is ignored, and a hash is treated as blank.
|
|
248
|
+
- An answer is a string or an array of strings, so code that reads answers never assumes a string. A checklist's answer is always an array.
|
|
249
|
+
- Condition, switch and compare do not branch on an array answer. Branching on one takes an app step type that defines `route`.
|
|
250
|
+
- A `displays_by` block that takes the run handles a `nil` run.
|
|
251
|
+
- An answer the type's `answer_check` refuses is never recorded, and the visitor is shown the same step with the check's message. An answer no check refuses is recorded, a blank single one as `""`.
|
|
226
252
|
- A `waits_until` block is given only the step and the answers recorded so far, so what it waits for must be findable from those.
|
|
227
253
|
- A type that waits never also declares `awaits_input`.
|
|
228
254
|
- `run.advance` only moves a stored run, and never finishes it; the visitor's next page load does.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: easy_flow-info
|
|
3
|
-
description: Use to learn what easy_flow offers — guided flows an admin draws on a canvas and a visitor runs one step at a time, versioned documents of steps and connections, flows that loop back to ask a step again, hosts and the step types each offers its admins, runs, the built-in step types including questions that can require an answer and branching on an answer or comparing a number, step types the host app registers, the checks they make on an answer, and steps that hold a run until something outside the flow has happened, and the settings an admin fills in, including picks whose options come from the app's data.
|
|
3
|
+
description: Use to learn what easy_flow offers — guided flows an admin draws on a canvas and a visitor runs one step at a time, versioned documents of steps and connections, flows that loop back to ask a step again, hosts and the step types each offers its admins, runs, the built-in step types including questions and checklists that can require an answer and explain each answer behind an info button, and branching on an answer or comparing a number, step types the host app registers, the checks they make on an answer, what a step shows a visitor and how that can depend on the visitor's run, and steps that hold a run until something outside the flow has happened, and the settings an admin fills in, including picks whose options come from the app's data.
|
|
4
4
|
tools: Read
|
|
5
5
|
scope: guided flows — versioned documents of steps and the connections between them, drawn on a canvas by an admin and run by a visitor one step at a time, with step types the host registers
|
|
6
6
|
---
|
|
@@ -11,7 +11,7 @@ This local explains easy_flow and the words it uses. It makes no changes and giv
|
|
|
11
11
|
|
|
12
12
|
easy_flow is a Rails engine for guided, branching flows. An admin builds a flow on a canvas by placing steps and connecting them, previews it, and publishes it. A visitor then walks the published flow one step at a time, and each answer decides which step comes next.
|
|
13
13
|
|
|
14
|
-
Reach for it when an app needs a questionnaire, an intake form, or any decision path that an admin should be able to change without a deploy. The engine ships a start step, an end step, a question step, and three steps that pick a branch from earlier answers. Anything else a flow needs to ask, do, or wait for is a step type the app declares itself.
|
|
14
|
+
Reach for it when an app needs a questionnaire, an intake form, or any decision path that an admin should be able to change without a deploy. The engine ships a start step, an end step, a question step, a checklist step, and three steps that pick a branch from earlier answers. Anything else a flow needs to ask, do, or wait for is a step type the app declares itself.
|
|
15
15
|
|
|
16
16
|
## Interface
|
|
17
17
|
|
|
@@ -23,9 +23,11 @@ easy_flow declares no commands of its own for this local. Its surface is split b
|
|
|
23
23
|
## How to use it
|
|
24
24
|
|
|
25
25
|
- To get easy_flow running in an app, or to change how it is configured, including which step types a host's admins can add, use **easy_flow-install**.
|
|
26
|
-
- To add a step type, give a step type its own rule for refusing an answer, hold a run until something outside the flow has happened, put a flow on the app's own pages, or act on what a visitor answered, use **easy_flow-develop**.
|
|
26
|
+
- To add a step type, give a step type its own rule for refusing an answer, make what a step shows depend on the visitor's run, hold a run until something outside the flow has happened, put a flow on the app's own pages, or act on what a visitor answered, use **easy_flow-develop**.
|
|
27
27
|
- To branch a flow on an answer or on a number the visitor gave, no code is needed: an admin places one of the built-in branching steps on the canvas.
|
|
28
|
-
- To make a visitor answer a question before going on, no code is needed: an admin marks that
|
|
28
|
+
- To make a visitor answer a question or tick something on a checklist before going on, no code is needed: an admin marks that step as required on the canvas.
|
|
29
|
+
- To let a visitor tick several answers on one step, no code is needed: an admin places a checklist step on the canvas.
|
|
30
|
+
- To explain an answer to the visitor, no code is needed: an admin fills in that answer's info on a question or checklist step.
|
|
29
31
|
- To ask a step again, such as repeating a question until the visitor says they are done, no code is needed: an admin connects a later step back to an earlier one on the canvas.
|
|
30
32
|
- If the question is only what a word below means, this page is the answer.
|
|
31
33
|
|
|
@@ -37,13 +39,19 @@ easy_flow declares no commands of its own for this local. Its surface is split b
|
|
|
37
39
|
- **Canvas** — the admin screen where steps are added, configured, moved, removed and connected, with undo and redo. Publishing happens there.
|
|
38
40
|
- **Preview** — an admin walking the current flow without starting a stored run.
|
|
39
41
|
- **Step type** — what a kind of step is: its display name, its settings (the fields an admin fills in), its outputs (the values it records), and whether it waits for the visitor, acts on its own, or waits for something outside the flow. A step that acts on its own may record a value worked out from the answers so far, pick which connection to follow, or both.
|
|
42
|
+
- **Display** — what a step type hands the visitor's page to draw a step, such as its question text and its answers. A step type may build its display from the step alone, or from the step and the visitor's stored run, so the same step can show something different to each visitor. There is no stored run in an admin's preview or in a flow that carries its answers in the page, and the display is then built without one.
|
|
40
43
|
- **Answer check** — a rule a step type may carry that looks at a visitor's answer and, when the answer is not acceptable, returns the message the visitor sees. A step type with no such rule accepts every answer.
|
|
41
44
|
- **Setting** — one field an admin fills in when configuring a step on the canvas. It is text, a whole number, a decimal, a yes or no, a pick from a list, several picks from a list, an earlier step, an earlier step's output, or a list of entries that each hold their own fields. A setting can be required, limited to a number of picks, or checked by a rule the step type supplies.
|
|
42
45
|
- **Options** — the values a pick-from-a-list setting offers. They are either a fixed list written into the step type, or a lookup the app provides that is read each time the canvas or a save asks for them, so they can come from the app's own data. A saved value that is not among the options is refused.
|
|
43
46
|
- **Output** — a value a step records into the run, with a type and a label. Later steps read outputs, and a setting that points at an earlier step offers that step's known values on the canvas.
|
|
44
47
|
- **Start** and **End** — the built-in step types that begin and finish a flow.
|
|
45
|
-
- **Question** — the built-in step type that asks the visitor: a question text, an optional category, a yes-or-no required setting, and a list of answers, each with a value, a label
|
|
48
|
+
- **Question** — the built-in step type that asks the visitor: a question text, an optional category, a yes-or-no required setting, and a list of answers, each with a value, a label, a weight, a hint and an info. The visitor picks one answer, and that answer's value is recorded.
|
|
49
|
+
- **Checklist** — the built-in step type that asks the visitor to tick any number of answers: a question text, a yes-or-no required setting, and a list of answers, each with a value, a label and an info. The list of ticked values is recorded, and a checklist left with nothing ticked records an empty list.
|
|
50
|
+
- **Hint** — a short line of text shown under an answer's label on a question step.
|
|
51
|
+
- **Info** — a longer explanation of one answer on a question or checklist step. The visitor opens it with an info button beside that answer, and an answer with no info has no button.
|
|
52
|
+
- **Read-back** — the list a visitor sees when a flow finishes: each question or checklist asked, beside the label of the answer given. A checklist's ticked answers are read back as their labels joined into one phrase, such as "Email, Phone and Post", and an answer with no label is read back as its value.
|
|
46
53
|
- **Required question** — a question step the admin has marked required. A blank answer to it is refused with the message "Fill this in to go on."
|
|
54
|
+
- **Required checklist** — a checklist step the admin has marked required. Submitting it with nothing ticked is refused with the message "Tick at least one to go on."
|
|
47
55
|
- **Condition** — a built-in branching step that checks whether an earlier step's answer is, or is not, a chosen value, and follows the true or the false connection.
|
|
48
56
|
- **Switch** — a built-in branching step that follows the connection labelled with an earlier step's answer.
|
|
49
57
|
- **Compare** — a built-in branching step that reads an earlier step's answer as a number and checks it against an amount by more than, less than, at least or at most, then follows the true or the false connection. When the earlier step recorded several outputs, the admin picks which one. An answer that is missing or is not a number decides false.
|
|
@@ -53,11 +61,12 @@ easy_flow declares no commands of its own for this local. Its surface is split b
|
|
|
53
61
|
- **Offered step types** — the list of step types a host names for its admins to add. A host that names no list offers every registered step type. The list only narrows the palette.
|
|
54
62
|
- **Palette** — the step types an admin can add on a host's canvas. It never holds the start step, always holds the end step, and otherwise holds the step types the host offers. A step already in a flow whose type the host does not offer is still drawn and run.
|
|
55
63
|
- **Run** — one visitor's pass through a flow. A run is pinned to the version that was live when it started, so publishing a new version does not change a run already under way. It records each answer by visit, and going back discards the last answer on the path.
|
|
64
|
+
- **Stored run** and **carried answers** — the two ways a visitor's progress is held. A stored run is saved by the app and found again by its id. A flow that keeps nothing carries the answers given so far in the page itself and sends them along with each step.
|
|
56
65
|
- **Answers** — the recorded state of a run, keyed by visit. The next step is always worked out from these answers and the connections in the pinned version.
|
|
57
66
|
- **Loop** — a connection that leads back to a step the run has already passed, so that step is visited again. Branching steps read the most recent answer to a step, so a loop ends when an answer sends the run down a different connection.
|
|
58
67
|
- **Visit** — one time a run reaches a step. The first visit is keyed by the step id alone, and each later visit is keyed by the step id, an `@`, and the visit number, such as `size@2`. Every visit's answer is kept, and a finished run lists each one.
|
|
59
68
|
- **Stopped loop** — a run that comes back to a step with no new answer given since its last visit there ends at that point rather than cycling forever. Only answers the visitor gives count, so a loop made only of steps that act on their own stops the first time it comes round.
|
|
60
69
|
- **Reserved `@`** — a step id may not contain `@`, since that mark numbers later visits. A flow holding such a step id is reported as invalid.
|
|
61
70
|
- **Refused answer** — an answer the step's answer check turned down. Nothing is recorded for that step, and the visitor is shown the same step again with the check's message. This works the same whether the flow keeps a stored run or carries its answers in the page.
|
|
62
|
-
- **Blank answer** — a step the visitor leaves blank. On a step whose answer check does not refuse it, such as a question that is not required, the blank is recorded and the visitor moves on to the next step.
|
|
71
|
+
- **Blank answer** — a step the visitor leaves blank. On a step whose answer check does not refuse it, such as a question or checklist that is not required, the blank is recorded and the visitor moves on to the next step. This works the same whether the flow keeps a stored run or carries its answers in the page, and every value ticked on a checklist is carried either way.
|
|
63
72
|
- **Checks** — warnings on a flow's shape: an answer value no connection routes, a path nothing follows, and a step that leads nowhere. The engine turns all three on by default. These are separate from answer checks, which look at what a visitor submits.
|
|
@@ -24,16 +24,17 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
24
24
|
## How to use it
|
|
25
25
|
|
|
26
26
|
1. Confirm the app has Keystone UI installed (`keystone_ui` in the Gemfile). easy_flow's controllers use its helpers and it is not pulled in by easy_flow. If it is missing, stop and hand off to the `keystone_ui-install` local before continuing.
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
-
5. Ask the developer which
|
|
27
|
+
2. Read the `keystone_ui` version in the app's `Gemfile.lock`. easy_flow's visitor pages pass each answer's info text to Keystone UI's radio cards and checkbox rows, and easy_flow is built against `keystone_ui` 0.27.0. If the app's version is older, ask the developer whether to run `bundle update keystone_ui` before continuing.
|
|
28
|
+
3. Add `gem "easy_flow"` to the host's `Gemfile` and run `bundle install`.
|
|
29
|
+
4. Run `bin/rails easy_flow:install:migrations`, then `bin/rails db:migrate`. This writes four migrations into `db/migrate` and updates `db/schema.rb`.
|
|
30
|
+
5. Ask the developer which hosts the app needs. A host is one part of the app that owns its own set of flows, and one host never sees another's flows. Ask for each host's name and the path it is served under.
|
|
31
|
+
6. Ask the developer which controller the engine should inherit from. Choosing `"ApplicationController"` gives the engine the app's own authentication methods and helpers. Create `config/initializers/easy_flow.rb` and set it on the first line:
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
34
|
EasyFlow.base_controller = "ApplicationController"
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
7. In the same initializer, declare each host. Every setting is optional. Ask the developer for each value and leave out any they do not want:
|
|
37
38
|
|
|
38
39
|
```ruby
|
|
39
40
|
EasyFlow.host(:console) do |host|
|
|
@@ -42,7 +43,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
42
43
|
host.admin_authentication_method = :authenticate_admin!
|
|
43
44
|
host.visitor_authorization_method = :easy_flow_visitor_permitted?
|
|
44
45
|
host.refusal_method = :refuse_flow
|
|
45
|
-
host.offers = %i[question condition switch compare]
|
|
46
|
+
host.offers = %i[question checklist condition switch compare]
|
|
46
47
|
end
|
|
47
48
|
```
|
|
48
49
|
|
|
@@ -51,10 +52,10 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
51
52
|
- `admin_authentication_method` — a method on the base controller, called with no arguments before every admin page. With none set, the admin pages are open to anyone.
|
|
52
53
|
- `visitor_authorization_method` — a method on the base controller, called with the flow, returning true when the visitor may run it. With none set, every visitor is refused.
|
|
53
54
|
- `refusal_method` — a method on the base controller, called with the refusal error when a visitor is refused or a flow is unpublished or withdrawn. With none set, the response is `404 Not Found`.
|
|
54
|
-
- `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `condition`, `switch` and `compare`. A step type the app declares is named by the id passed to `EasyFlow.step`, or after its class when it is a step class, so `Steps::Notify` is `notify`. The End step is offered whether it is listed or not, and the Start step is never offered. Ask the developer which step types each host should offer.
|
|
55
|
+
- `offers` — the step types this host's admins can add from the canvas palette, as a list of step type names. With none set, every registered step type is offered. The engine's own names are `question`, `checklist`, `condition`, `switch` and `compare`. A step type the app declares is named by the id passed to `EasyFlow.step`, or after its class when it is a step class, so `Steps::Notify` is `notify`. The End step is offered whether it is listed or not, and the Start step is never offered. Ask the developer which step types each host should offer.
|
|
55
56
|
|
|
56
57
|
Each method named here must exist on the base controller. Ask the developer to point at it or write it. Do not invent its logic.
|
|
57
|
-
|
|
58
|
+
8. Mount the engine in `config/routes.rb`, once per host. The host name in `defaults` must match a name declared in step 7. When there is more than one mount, give each an `as:` name:
|
|
58
59
|
|
|
59
60
|
```ruby
|
|
60
61
|
mount EasyFlow::Engine => "/flows", defaults: { easy_flow_host: "flows" }
|
|
@@ -62,19 +63,26 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
A mount whose host name is not declared serves no flows, and its admin pages return `404 Not Found`.
|
|
65
|
-
|
|
66
|
+
9. Ask the developer whether visitors' steps should be drawn with the engine's own partial or with one from the app. The engine's own draws a question as one radio card per answer, each with its hint and an info button when the answer has info text. For the app's own, create a partial, for example `app/views/steps/_step.html.erb`, and add to the initializer:
|
|
66
67
|
|
|
67
68
|
```ruby
|
|
68
69
|
EasyFlow.draws_with("steps/step")
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
- The partial receives the step as the local `step`. `step.text` is the question, and `step.choices` lists its answers, each with a `value`, `label`, `hint` and `info`.
|
|
73
|
+
- It is rendered inside the engine's form, so it draws only the fields.
|
|
74
|
+
- The visitor's answer must be submitted as `answers[<%= step.id %>]`. Always build the field name from `step.id` and never from a fixed id. When a flow loops back and asks a question again, `step.id` names that visit, so each visit's answer is stored apart.
|
|
75
|
+
- It replaces the engine's drawing of every step whose type names no partial, so it draws each answer's hint and info itself or they are not shown.
|
|
76
|
+
- It is not used for a checklist step. A checklist is always drawn by the engine as one checkbox per answer, each with an info button when the answer has info text, and the visitor may tick several.
|
|
77
|
+
10. Leave `EasyFlow.check` out unless the developer asks for it. The engine already turns on `:unrouted_value`, `:unfollowed_path` and `:dead_end` at boot. Any other name raises `EasyFlow::UnknownCheck` when the app boots.
|
|
78
|
+
11. Start the server and open `<mount path>/manage/flows` for each host.
|
|
73
79
|
|
|
74
80
|
## Conventions
|
|
75
81
|
|
|
76
82
|
- After install, check that `<mount path>/manage/flows` shows the flow list and that a new flow opens on the canvas. If the canvas is blank, check that the admin layout calls `yield :head`.
|
|
77
83
|
- After publishing a flow, check that `<mount path>/<slug>` shows it to a visitor who passes the host's visitor authorization method.
|
|
84
|
+
- After publishing a flow with a question or a checklist whose answer has info text, check that the visitor's page shows an info button on that answer. If it does not, compare the app's `keystone_ui` version with the one in step 2.
|
|
85
|
+
- After publishing a flow with a checklist, check that a visitor can tick several answers and go on, and that a checklist marked required refuses to go on with nothing ticked.
|
|
78
86
|
- After upgrading easy_flow, run `bin/rails easy_flow:install:migrations` again and then `bin/rails db:migrate`. Only migrations the app does not already have are copied.
|
|
79
87
|
- After setting a host's `offers`, check that the canvas palette on that host's `<mount path>/manage/flows` lists only those step types and End.
|
|
80
88
|
- Taking a step type off a host's `offers` removes it from the palette only. Steps of that type already in the host's flows stay in them and keep running.
|
data/the_local/interface.yml
CHANGED
|
@@ -30,11 +30,19 @@ sources:
|
|
|
30
30
|
- app/models/easy_flow/step.rb
|
|
31
31
|
- app/models/easy_flow/step_type/declaration.rb
|
|
32
32
|
- app/models/easy_flow/steps/question.rb
|
|
33
|
+
- app/models/easy_flow/steps/checklist.rb
|
|
34
|
+
- app/models/easy_flow/step_type.rb
|
|
35
|
+
- app/models/easy_flow/choice.rb
|
|
33
36
|
- app/models/easy_flow/run.rb
|
|
34
37
|
- app/models/easy_flow/runner.rb
|
|
35
38
|
- app/models/easy_flow/digest.rb
|
|
36
39
|
- app/models/easy_flow/validator.rb
|
|
37
40
|
- app/models/easy_flow/question_runner.rb
|
|
41
|
+
- app/views/easy_flow/flows/step.html.erb
|
|
42
|
+
- app/views/easy_flow/flows/complete.html.erb
|
|
43
|
+
- app/views/easy_flow/steps/_choosing.html.erb
|
|
44
|
+
- app/views/easy_flow/steps/_ticking.html.erb
|
|
45
|
+
- app/controllers/easy_flow/manage/previews_controller.rb
|
|
38
46
|
- config/routes.rb
|
|
39
47
|
- db/migrate/20260924120000_create_easy_flow_definitions.rb
|
|
40
48
|
- db/migrate/20260924120100_create_easy_flow_versions.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_flow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- app/models/easy_flow/step.rb
|
|
115
115
|
- app/models/easy_flow/step_type.rb
|
|
116
116
|
- app/models/easy_flow/step_type/declaration.rb
|
|
117
|
+
- app/models/easy_flow/steps/checklist.rb
|
|
117
118
|
- app/models/easy_flow/steps/question.rb
|
|
118
119
|
- app/models/easy_flow/switch.rb
|
|
119
120
|
- app/models/easy_flow/terminal.rb
|
|
@@ -129,6 +130,7 @@ files:
|
|
|
129
130
|
- app/views/easy_flow/manage/flows/show.html.erb
|
|
130
131
|
- app/views/easy_flow/manage/versions/index.html.erb
|
|
131
132
|
- app/views/easy_flow/steps/_choosing.html.erb
|
|
133
|
+
- app/views/easy_flow/steps/_ticking.html.erb
|
|
132
134
|
- app/views/layouts/easy_flow/application.html.erb
|
|
133
135
|
- config/routes.rb
|
|
134
136
|
- db/migrate/20260924120000_create_easy_flow_definitions.rb
|