easy_flow 0.12.0 → 0.14.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 +8 -1
- data/app/models/easy_flow/chosen_flow.rb +5 -2
- data/app/models/easy_flow/progress/kept.rb +9 -2
- data/app/models/easy_flow/question_runner.rb +10 -0
- data/app/models/easy_flow/step_type/declaration.rb +10 -2
- data/app/models/easy_flow/step_type.rb +16 -2
- data/app/models/easy_flow/steps/file_upload.rb +39 -0
- data/app/models/easy_flow/validator.rb +1 -1
- data/app/views/easy_flow/flows/step.html.erb +1 -1
- data/app/views/easy_flow/steps/_uploading.html.erb +5 -0
- data/lib/easy_flow/engine.rb +1 -0
- data/lib/easy_flow/version.rb +1 -1
- data/lib/easy_flow.rb +8 -0
- data/the_local/agents/easy_flow-develop.md +19 -8
- data/the_local/agents/easy_flow-info.md +10 -7
- data/the_local/agents/easy_flow-install.md +23 -8
- data/the_local/interface.yml +4 -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: 48b43c9ecd7f02841d19f9fd4f10f42ea49092641bd998fb5750d6b61d9d330f
|
|
4
|
+
data.tar.gz: f6b12a0a88c69f17a8aa596dad74c310a936060babb7f76d0f0a7f0f486435fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8e4a6145d9ba8309fc633274e280d96409978b18f8a8df957add151423d9b75d1365b606efd444ae6a91d9563c27c9a1bfe0355f3e1e946020fb34d03c963d1
|
|
7
|
+
data.tar.gz: 48f5729ef10aa4b053ecf20583c91edbe7cf0f749e4623f85b84733ca6e2663c6e115df0f269a3e987f31d38ff4e4289840b4dc6856785d8611ab194e9d94281
|
|
@@ -132,7 +132,14 @@ module EasyFlow
|
|
|
132
132
|
problem = answer_problem(runner_for(run.pinned_definition).step(id.to_s), value)
|
|
133
133
|
return flash[:alert] = problem if problem
|
|
134
134
|
|
|
135
|
-
progress.record(id,
|
|
135
|
+
progress.record(id, recorded(value))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def recorded(value)
|
|
139
|
+
return value.compact_blank.map { |each| recorded(each) } if value.is_a?(Array)
|
|
140
|
+
return EasyFlow.keep_file(value) if value.respond_to?(:original_filename)
|
|
141
|
+
|
|
142
|
+
value.to_s
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
def waiting_on(step)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
module EasyFlow
|
|
2
2
|
ChosenFlow = Data.define(:flow_id, :number) do
|
|
3
|
-
def self.of(node, registry: EasyFlow.registry)
|
|
4
|
-
|
|
3
|
+
def self.of(node, registry: EasyFlow.registry, run: nil)
|
|
4
|
+
step_type = registry.fetch(node.type) if registry.registered?(node.type)
|
|
5
|
+
return worked_out((run && step_type.flow_chosen_from_run(node, run)) || {}) if step_type&.chooses_flow_from_run?
|
|
6
|
+
|
|
7
|
+
chosen = step_type&.flow_chosen_by(node)
|
|
5
8
|
return worked_out(chosen) if chosen
|
|
6
9
|
|
|
7
10
|
new(flow_id: node.config["flow"].presence, number: node.config["version"].presence)
|
|
@@ -20,7 +20,8 @@ module EasyFlow
|
|
|
20
20
|
def start_inner(node)
|
|
21
21
|
return if @run.inner_runs.exists?(parent_step: node.id)
|
|
22
22
|
|
|
23
|
-
chosen = ChosenFlow.of(node)
|
|
23
|
+
chosen = ChosenFlow.of(node, run: @run)
|
|
24
|
+
raise InnerFlowError, "The step #{node.id} chose no flow to run" if chosen.flow.nil? && EasyFlow.registry.fetch(node.type).chooses_flow_from_run?
|
|
24
25
|
raise InnerFlowError, "The Flow step #{node.id} names a version that cannot run" if chosen.version&.out_of_service?
|
|
25
26
|
Run.start(chosen.flow || raise(ActiveRecord::RecordNotFound), version: chosen.version)
|
|
26
27
|
.tap { |inner| inner.update!(parent_run: @run, parent_step: node.id, owner: @run.owner) }.advance
|
|
@@ -30,10 +31,16 @@ module EasyFlow
|
|
|
30
31
|
parent = @run.parent_run or return
|
|
31
32
|
return if parent.recorded.key?(@run.parent_step.to_sym)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
ending = @run.output || ("ended" if chose_from_run?(parent))
|
|
35
|
+
parent.record(@run.parent_step.to_sym, ending) if ending
|
|
34
36
|
AdvanceParentJob.perform_later(parent, @run.parent_step)
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def chose_from_run?(parent)
|
|
40
|
+
type = parent.pinned_steps.dig(@run.parent_step, "type")
|
|
41
|
+
EasyFlow.registry.registered?(type) && EasyFlow.registry.fetch(type).chooses_flow_from_run?
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
def finish(_state)
|
|
38
45
|
@run
|
|
39
46
|
end
|
|
@@ -7,9 +7,19 @@ module EasyFlow
|
|
|
7
7
|
def choice_label(id, value)
|
|
8
8
|
return value.map { |ticked| choice_label(id, ticked) }.to_sentence if value.is_a?(Array)
|
|
9
9
|
|
|
10
|
+
labelled = labelled_by_step_type(id, value)
|
|
11
|
+
return labelled if labelled.present?
|
|
12
|
+
|
|
10
13
|
chosen = Steps::Question.choices_in(step(id)).find { |choice| choice.value == value }
|
|
11
14
|
|
|
12
15
|
chosen&.label.presence || value
|
|
13
16
|
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def labelled_by_step_type(id, value)
|
|
21
|
+
node = step(id)
|
|
22
|
+
EasyFlow.registry.fetch(node.type).answer_label(node, value) if node && EasyFlow.registry.registered?(node.type)
|
|
23
|
+
end
|
|
14
24
|
end
|
|
15
25
|
end
|
|
@@ -46,6 +46,10 @@ module EasyFlow
|
|
|
46
46
|
@answer_check = check
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def labels_answer_by(&labelling)
|
|
50
|
+
@answer_labelling = labelling
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
def waits_until(&readiness)
|
|
50
54
|
@readiness = readiness
|
|
51
55
|
end
|
|
@@ -70,6 +74,10 @@ module EasyFlow
|
|
|
70
74
|
@starts_a_flow = true
|
|
71
75
|
end
|
|
72
76
|
|
|
77
|
+
def chooses_flow_from_run(&chooser)
|
|
78
|
+
@run_chooser = chooser
|
|
79
|
+
end
|
|
80
|
+
|
|
73
81
|
def chooses_flow(&chooser)
|
|
74
82
|
@flow_chooser = chooser
|
|
75
83
|
end
|
|
@@ -120,9 +128,9 @@ module EasyFlow
|
|
|
120
128
|
|
|
121
129
|
def to_step_type
|
|
122
130
|
StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
|
|
123
|
-
ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, flow_chooser: @flow_chooser, behaviour: @behaviour, routing: @routing,
|
|
131
|
+
ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, flow_chooser: @flow_chooser, run_chooser: @run_chooser, behaviour: @behaviour, routing: @routing,
|
|
124
132
|
display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
|
|
125
|
-
outputs: @declared_outputs, answer_check: @answer_check, readiness: @readiness)
|
|
133
|
+
outputs: @declared_outputs, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness)
|
|
126
134
|
end
|
|
127
135
|
|
|
128
136
|
def settings
|
|
@@ -7,8 +7,8 @@ module EasyFlow
|
|
|
7
7
|
attr_reader :id, :step_name, :settings, :naming_field, :naming, :outputs, :drawn_by
|
|
8
8
|
|
|
9
9
|
def initialize(id:, step_name:, settings:, awaits_input:, behaviour:, routing:,
|
|
10
|
-
ends_here: false, begins_here: false, starts_a_flow: false, flow_chooser: nil, display: nil, drawn_by: nil,
|
|
11
|
-
naming_field: nil, naming: nil, outputs: [], answer_check: nil, readiness: nil)
|
|
10
|
+
ends_here: false, begins_here: false, starts_a_flow: false, flow_chooser: nil, run_chooser: nil, display: nil, drawn_by: nil,
|
|
11
|
+
naming_field: nil, naming: nil, outputs: [], answer_check: nil, answer_labelling: nil, readiness: nil)
|
|
12
12
|
@id = id
|
|
13
13
|
@step_name = step_name
|
|
14
14
|
@settings = settings
|
|
@@ -17,6 +17,7 @@ module EasyFlow
|
|
|
17
17
|
@begins_here = begins_here
|
|
18
18
|
@starts_a_flow = starts_a_flow
|
|
19
19
|
@flow_chooser = flow_chooser
|
|
20
|
+
@run_chooser = run_chooser
|
|
20
21
|
@behaviour = behaviour
|
|
21
22
|
@routing = routing
|
|
22
23
|
@display = display
|
|
@@ -25,6 +26,7 @@ module EasyFlow
|
|
|
25
26
|
@naming = naming
|
|
26
27
|
@outputs = outputs
|
|
27
28
|
@answer_check = answer_check
|
|
29
|
+
@answer_labelling = answer_labelling
|
|
28
30
|
@readiness = readiness
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -46,6 +48,10 @@ module EasyFlow
|
|
|
46
48
|
@answer_check&.call(node, value)
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def answer_label(node, value)
|
|
52
|
+
@answer_labelling&.call(node, value)
|
|
53
|
+
end
|
|
54
|
+
|
|
49
55
|
def waits?
|
|
50
56
|
@readiness.present?
|
|
51
57
|
end
|
|
@@ -86,6 +92,14 @@ module EasyFlow
|
|
|
86
92
|
@starts_a_flow
|
|
87
93
|
end
|
|
88
94
|
|
|
95
|
+
def chooses_flow_from_run?
|
|
96
|
+
!@run_chooser.nil?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def flow_chosen_from_run(node, run)
|
|
100
|
+
@run_chooser&.call(node, run)
|
|
101
|
+
end
|
|
102
|
+
|
|
89
103
|
def flow_chosen_by(node)
|
|
90
104
|
@flow_chooser&.call(node)
|
|
91
105
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module EasyFlow
|
|
2
|
+
module Steps
|
|
3
|
+
class FileUpload
|
|
4
|
+
include Step
|
|
5
|
+
|
|
6
|
+
Asked = Data.define(:id, :text, :accepts)
|
|
7
|
+
|
|
8
|
+
step_name "File upload"
|
|
9
|
+
|
|
10
|
+
setting :question, type: :string
|
|
11
|
+
setting :accepts, type: :string, label: "Accepted file types"
|
|
12
|
+
setting :required, type: :boolean
|
|
13
|
+
|
|
14
|
+
names_by :question
|
|
15
|
+
awaits_input
|
|
16
|
+
drawn_by "easy_flow/steps/uploading"
|
|
17
|
+
answer_check { |node, value| FileUpload.problem_with(node, value) }
|
|
18
|
+
labels_answer_by { |_node, reference| EasyFlow.file_store&.name_of(reference) }
|
|
19
|
+
|
|
20
|
+
displays_by { |node| Asked.new(id: node.id.to_sym, text: node.config["question"], accepts: node.config["accepts"]) }
|
|
21
|
+
|
|
22
|
+
def self.problem_with(node, value)
|
|
23
|
+
return "Choose a file to go on." if node.config["required"] && value.blank?
|
|
24
|
+
return if value.blank?
|
|
25
|
+
return "A file can only be uploaded in a flow that saves its runs." unless value.respond_to?(:original_filename)
|
|
26
|
+
|
|
27
|
+
kinds = accepted_kinds(node)
|
|
28
|
+
return if kinds.empty?
|
|
29
|
+
return if kinds.include?(File.extname(value.original_filename).downcase)
|
|
30
|
+
|
|
31
|
+
"Choose a #{kinds.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')} file."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.accepted_kinds(node)
|
|
35
|
+
node.config["accepts"].to_s.split(",").map { |kind| kind.strip.downcase }.compact_blank
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -170,7 +170,7 @@ module EasyFlow
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def starting_a_flow(document = @document)
|
|
173
|
-
document.nodes.select { |node| @registry.registered?(node.type) && @registry.fetch(node.type).starts_a_flow? }
|
|
173
|
+
document.nodes.select { |node| @registry.registered?(node.type) && @registry.fetch(node.type).starts_a_flow? && !@registry.fetch(node.type).chooses_flow_from_run? }
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def missing_settings
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<% if @waiting %>
|
|
15
15
|
<p class="ks-hint">Waiting for <%= @waiting %>.</p>
|
|
16
16
|
<% else %>
|
|
17
|
-
<%= form_with url: step_form[:url], method: step_form[:method], data: { turbo: false } do %>
|
|
17
|
+
<%= form_with url: step_form[:url], method: step_form[:method], multipart: true, data: { turbo: false } do %>
|
|
18
18
|
<%= hidden_field_tag :asked, @question.id %>
|
|
19
19
|
<% if carries_answers? %>
|
|
20
20
|
<% @answers.each do |key, value| %>
|
data/lib/easy_flow/engine.rb
CHANGED
data/lib/easy_flow/version.rb
CHANGED
data/lib/easy_flow.rb
CHANGED
|
@@ -21,11 +21,13 @@ module EasyFlow
|
|
|
21
21
|
class OutOfService < StandardError; end
|
|
22
22
|
class InnerFlowError < StandardError; end
|
|
23
23
|
class Withdrawn < StandardError; end
|
|
24
|
+
class NoFileStore < StandardError; end
|
|
24
25
|
|
|
25
26
|
DRAWING = "easy_flow/steps/choosing".freeze
|
|
26
27
|
|
|
27
28
|
class << self
|
|
28
29
|
attr_writer :base_controller
|
|
30
|
+
attr_accessor :file_store
|
|
29
31
|
|
|
30
32
|
def base_controller
|
|
31
33
|
@base_controller || "ActionController::Base"
|
|
@@ -61,6 +63,12 @@ module EasyFlow
|
|
|
61
63
|
@registry ||= Registry.new
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def keep_file(file)
|
|
67
|
+
raise NoFileStore, "set EasyFlow.file_store to a store that keeps an uploaded file and returns a reference to it" unless file_store
|
|
68
|
+
|
|
69
|
+
file_store.keep(file)
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
def check(name)
|
|
65
73
|
raise UnknownCheck, "#{name} is not one of #{OPTIONAL_CHECKS.join(', ')}" unless OPTIONAL_CHECKS.include?(name)
|
|
66
74
|
|
|
@@ -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, 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, letting an admin pick another flow of the same host in a step's settings, adding a step type that starts another flow and works out which flow and version to run from its own settings, letting an admin edit a value stored on one of the app's own records from a step's settings on the canvas, serving a host's flows from the app's own controller and routes, starting a visitor's run on a published version of a flow other than the live one, acting when a visitor finishes a flow, acting on the output a visitor's flow ended with, keeping a flow from being published while a step that starts another flow names a flow or version that cannot run or leads back to the flow it sits in, finding the run of another flow that a Flow step started, the run a waiting run is waiting on, and the run that started it, sending a visitor back to the run that started a flow once that flow ends, handling a run whose starting run was removed with its flow, handling a Flow step that cannot start its flow or whose flow ended with no output, 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, copying a record's value into a flow and back, 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, letting an admin pick another flow of the same host in a step's settings, adding a step type that starts another flow and works out which flow and version to run from its own settings, letting an admin edit a value stored on one of the app's own records from a step's settings on the canvas, serving a host's flows from the app's own controller and routes, starting a visitor's run on a published version of a flow other than the live one, acting when a visitor finishes a flow, acting on the output a visitor's flow ended with, keeping a flow from being published while a step that starts another flow names a flow or version that cannot run or leads back to the flow it sits in, finding the run of another flow that a Flow step started, the run a waiting run is waiting on, and the run that started it, sending a visitor back to the run that started a flow once that flow ends, handling a run whose starting run was removed with its flow, handling a Flow step that cannot start its flow or whose flow ended with no output, taking one file or several from a visitor at a step, showing a step's recorded answer by a label the step type works out, 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, file upload steps, branching logic, number comparisons, hard-coded setting options, copying a record's value into a flow and back, 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,12 +9,13 @@ 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 nine: start (`:start`), end (`:terminal`, whose one setting is the output an admin writes for a flow that ends there), 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), File upload (`:file_upload`, a question the visitor answers by choosing one file, with an admin setting listing the accepted file types as comma-separated extensions such as `.pdf, .png`, which the admin can mark required so a visitor who chooses no file is refused, and whose recorded answer is the reference the host's file store returned for the file), 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) — and Flow (`:flow_step`, names another flow of the same host and the number of one of its versions, with one connection for each different output that version's End steps write, and named on the canvas by that flow's title and the version number, such as `Intake, version 2`). When a stored run stops at a Flow step, a run of the named flow is started on the named version, and the first run waits on the Flow step. When the second run ends with an output, that output is recorded in the first run at the Flow step, and the first run moves on along the connection labelled with that output. When the second run ends with no output, nothing is recorded and the first run stays waiting at the Flow step. 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. A File upload step takes a file only in a flow the admin set to save each step. In a flow set to save on finish or to save nothing, a visitor who chooses a file is shown `A file can only be uploaded in a flow that saves its runs.` and cannot go on with it. Before writing a step type to ask for one answer or several, to take a file, 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
|
|
|
16
16
|
- `EasyFlow::Step` — a module a class includes to declare a step type with class-level words, registered with `.register`.
|
|
17
17
|
- `EasyFlow.step` — declares and registers a step type in one call from a block of the same words, for a type small enough not to need its own class.
|
|
18
|
+
- `labels_answer_by` — a word in a step type's declaration giving the label a run's recorded answer to a step of that type is shown by, on the completion page and from `EasyFlow::QuestionRunner#choice_label`.
|
|
18
19
|
- `EasyFlow::FlowsController` — the visitor controller; the app subclasses it to serve one host's flows from its own routes and to change what happens at the start, on each step and at the finish.
|
|
19
20
|
- `hosted_by` — class method on a `FlowsController` subclass naming the host whose flows it serves.
|
|
20
21
|
- `routed_by` — class method on a `FlowsController` subclass naming the prefix of the app's own route names that the controller redirects and links to.
|
|
@@ -31,7 +32,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
31
32
|
- It only picks which connection to follow — define `route`.
|
|
32
33
|
- It holds the visitor until something outside the flow has happened, such as a payment arriving or a reviewer approving — declare `waits_until`.
|
|
33
34
|
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`, `checklist`, `condition`, `switch`, `compare` or `flow_step`:
|
|
35
|
+
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`, `file_upload`, `condition`, `switch`, `compare` or `flow_step`:
|
|
35
36
|
|
|
36
37
|
```ruby
|
|
37
38
|
module FlowSteps
|
|
@@ -81,7 +82,8 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
81
82
|
- `names_by :setting` or `names_by { |node| ... }` — what the step is called on the canvas, from a setting or computed.
|
|
82
83
|
- `awaits_input` — the visitor is shown this step and submits an answer to it.
|
|
83
84
|
- `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, 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
|
+
- `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. For a file input, see step 7.
|
|
86
|
+
- `labels_answer_by { |node, value| ... }` — the label a recorded answer to a step of this type is shown by. `value` is one recorded answer, and for an array answer the block is called once per entry. Return the label, or `nil` or `""` to fall back to the label of the question answer with that value, and then to the value itself. The block is called on every completion page and every `choice_label` call, so it must not be slow and must not change anything. Ask the developer what a visitor or admin reading the answer should see in place of the stored value.
|
|
85
87
|
- `ends_here` / `begins_here` — marks the type as an end or a start of a flow. A run that ends on a step of an `ends_here` type gives that step's `output` setting as `run.output`, so a type of the app's own that ends a flow declares `setting :output, type: :string` for the admin to fill in, or gives no output.
|
|
86
88
|
- `starts_a_flow` — when a stored run stops at a step of this type, a run of another flow is started. Without `chooses_flow`, the type must declare `setting :flow, type: :flow` and `setting :version, type: :integer`, since the new run is started on that flow's version with that number. The new run is only started at a step the run stops at, so the type must also declare `waits_until` or `awaits_input`. When the new run ends, its output is recorded under this step's id, so a type that should follow a connection per output defines `route` returning `state[node.id]` and declares an `output` whose `values:` are the outputs the named flow can end with. Before writing such a type, ask the developer whether the built-in Flow step is enough. See "Follow a run into the flow a Flow step started".
|
|
87
89
|
- On the canvas, a step of the type is marked `Runs a flow`.
|
|
@@ -175,6 +177,13 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
175
177
|
|
|
176
178
|
An input that submits a hash, such as one named `answers[<step id>][key]`, is dropped and treated as blank.
|
|
177
179
|
|
|
180
|
+
Before writing a type that takes a file, ask the developer whether the built-in File upload step is enough, since it takes one file. For a type of the app's own that takes a file, ask the developer whether the step takes one file or several:
|
|
181
|
+
- For one file, name the file input `answers[<step id>]`, such as `file_field_tag "answers[#{step.id}]"`. The page's form already sends files.
|
|
182
|
+
- For several, name the input `answers[<step id>][]`, such as `file_field_tag "answers[#{step.id}][]", multiple: true`, and put the hidden blank field of the same name before it, as for checkboxes, so that a visitor who chooses none records an empty array and not `""`.
|
|
183
|
+
- In a flow the admin set to save each step, `answer_check` is given the uploaded file, which responds to `original_filename`, or `nil` when the visitor chose none. For an input that sends several, it is given the array as sent, which may hold the hidden blank string beside the files, so check each entry that responds to `original_filename`. Once no check refuses the answer, each file is handed to the host's file store, set through `easy_flow-install`, and the string reference the store returns is recorded in its place: one reference for one file, or an array of references in the order the files were sent, with blank entries removed. When the host has no file store, `EasyFlow::NoFileStore` is raised on the visitor's submit and nothing is recorded. This holds for any step a file is sent to in a stored run, whatever its type.
|
|
184
|
+
- In a flow set to save on finish or to save nothing, no file reaches the check, and it is given a string, an array of strings, or `nil` instead. Refuse an answer that does not respond to `original_filename`, as the built-in step does with `A file can only be uploaded in a flow that saves its runs.`, or that string is recorded as the answer.
|
|
185
|
+
- Declare `labels_answer_by` to show the recorded reference by a name a reader knows, such as the file's name read back from the app's store. For an array of references the block is called once per reference.
|
|
186
|
+
|
|
178
187
|
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.
|
|
179
188
|
|
|
180
189
|
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:
|
|
@@ -261,7 +270,7 @@ Use this when an admin should edit a value that belongs to one of the app's own
|
|
|
261
270
|
|
|
262
271
|
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.
|
|
263
272
|
4. Override only the private methods the developer's answers call for:
|
|
264
|
-
- `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. To act on which way the flow ended, read `run.output`; when `run` is `nil` there is no output to read. 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.
|
|
273
|
+
- `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. To act on which way the flow ended, read `run.output`; when `run` is `nil` there is no output to read. 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, and a File upload step's answer as the name the host's file store gives the file.
|
|
265
274
|
- `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`. To start the run on a version other than the live one, return `EasyFlow::Run.start(flow, version: ...)` instead of calling `super`; see "Start a run on a chosen version".
|
|
266
275
|
- `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.
|
|
267
276
|
5. Visit `/<path>/<slug>` for a published flow and walk it to the end.
|
|
@@ -348,7 +357,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
|
|
|
348
357
|
|
|
349
358
|
1. Find runs scoped to one host so one host never sees another's: `EasyFlow::Run.joins(:flow).where(easy_flow_definitions: { host: "intake" })`.
|
|
350
359
|
2. Read from a run:
|
|
351
|
-
- `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.
|
|
360
|
+
- `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. A File upload step's value is the reference the host's file store returned, not the file, and a step of the app's own that took several files holds an array of references, so the app reads each file back from its own store.
|
|
352
361
|
- `run.flow` — the flow it belongs to.
|
|
353
362
|
- `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.
|
|
354
363
|
- `run.parent_run` and `run.parent_step` — the run whose Flow step started this one and that step's id, both `nil` for a run a visitor or the app started. Once this run ends with an output, `run.parent_run.recorded` holds that `output` under that step id. When it ends with none, nothing is recorded there. When an admin removed the flow the parent run belonged to, `parent_run` is `nil` and `parent_step` still holds the step id.
|
|
@@ -369,7 +378,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
|
|
|
369
378
|
|
|
370
379
|
- `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 `@`.
|
|
371
380
|
- `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.
|
|
372
|
-
- `choice_label(id, value)` — the label of one chosen answer on a question or checklist, or the value itself
|
|
381
|
+
- `choice_label(id, value)` — the label the step's type gives the answer with `labels_answer_by`, or else the label of one chosen answer on a question or checklist, or else the value itself. For a File upload step it is the name the host's file store gives the recorded reference, or the reference itself when the store gives none. 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) }`.
|
|
373
382
|
- `step(id)` finds the step for either its id or a visit key.
|
|
374
383
|
- `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.
|
|
375
384
|
- `steps_on_path(answers)` — the steps on the path taken, each built by its type's `displays_by` with a `nil` run.
|
|
@@ -378,7 +387,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
|
|
|
378
387
|
## Conventions
|
|
379
388
|
|
|
380
389
|
- 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.
|
|
381
|
-
- Never give a step type one of the built-in ids: `start`, `terminal`, `question`, `checklist`, `condition`, `switch`, `compare`, `flow_step`.
|
|
390
|
+
- Never give a step type one of the built-in ids: `start`, `terminal`, `question`, `checklist`, `file_upload`, `condition`, `switch`, `compare`, `flow_step`.
|
|
382
391
|
- Register class-based step types inside `to_prepare`. A type registered anywhere else is lost on code reload in development.
|
|
383
392
|
- 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.
|
|
384
393
|
- 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.
|
|
@@ -401,6 +410,8 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
|
|
|
401
410
|
- 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.
|
|
402
411
|
- Condition, switch and compare do not branch on an array answer. Branching on one takes an app step type that defines `route`.
|
|
403
412
|
- A `displays_by` block that takes the run handles a `nil` run.
|
|
413
|
+
- A file is taken only in a flow that keeps a stored run, only from an input named `answers[<step.id>]` or, for several files, `answers[<step.id>][]`, and only when the host has a file store, and the run records the store's reference for each file, never the file.
|
|
414
|
+
- A `labels_answer_by` block changes only how an answer is shown, never what is recorded.
|
|
404
415
|
- 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 `""`.
|
|
405
416
|
- 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.
|
|
406
417
|
- A type that waits never also declares `awaits_input`.
|
|
@@ -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 and the published version each starts on, and the output a flow gives when it ends, 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, Flow steps that name another flow, open its canvas when their card is clicked with a button back to the flow it was opened from, start a run of it for the same owner when a stored run reaches them, are flagged when the flow or version they name cannot run or leads back to the flow they sit in, let the waiting run name that inner run, move that run on along the output it ends with, and fail with an error naming the step when its version was taken out of service or its inner run ended with no output, removing a flow and why it is refused while a waiting run depends on it, step types the host app registers, including ones that start another flow and work out which flow and version to run from their own settings and are named after that flow, 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 and settings kept on one of the app's own records.
|
|
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 and the published version each starts on, and the output a flow gives when it ends, the built-in step types including questions and checklists that can require an answer and explain each answer behind an info button, file upload steps that hand a visitor's file to the app's file store and read it back by its name, and branching on an answer or comparing a number, Flow steps that name another flow, open its canvas when their card is clicked with a button back to the flow it was opened from, start a run of it for the same owner when a stored run reaches them, are flagged when the flow or version they name cannot run or leads back to the flow they sit in, let the waiting run name that inner run, move that run on along the output it ends with, and fail with an error naming the step when its version was taken out of service or its inner run ended with no output, removing a flow and why it is refused while a waiting run depends on it, step types the host app registers, including ones that start another flow and work out which flow and version to run from their own settings and are named after that flow, 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 and settings kept on one of the app's own records.
|
|
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,19 +11,20 @@ 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, a checklist step, three steps that pick a branch from earlier answers, and a Flow step that names another flow. 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, a file upload step, three steps that pick a branch from earlier answers, and a Flow step that names another flow. 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
|
|
|
18
18
|
easy_flow declares no commands of its own for this local. Its surface is split between the other two:
|
|
19
19
|
|
|
20
|
-
- **easy_flow-install** owns putting the engine into an app: its migrations, mounting it, choosing the controller it inherits from, naming hosts and the step types each one offers, choosing the default step drawing, the flow checks, and the admin pages.
|
|
21
|
-
- **easy_flow-develop** owns building on it: declaring and registering step types, including ones that wait, ones that start another flow, and ones with settings kept on the app's own records, moving a run on once what it waits for has happened, serving a host's flows from the app's own controllers and routes, and starting a run on a chosen published version, reading a visitor's run, its answers, the inner run it is waiting on and the output its flow ended with.
|
|
20
|
+
- **easy_flow-install** owns putting the engine into an app: its migrations, mounting it, choosing the controller it inherits from, naming hosts and the step types each one offers, choosing the default step drawing, setting the file store that keeps uploaded files, the flow checks, and the admin pages.
|
|
21
|
+
- **easy_flow-develop** owns building on it: declaring and registering step types, including ones that read an answer back under a label of their own, ones that wait, ones that start another flow, and ones with settings kept on the app's own records, moving a run on once what it waits for has happened, serving a host's flows from the app's own controllers and routes, and starting a run on a chosen published version, reading a visitor's run, its answers, the inner run it is waiting on and the output its flow ended with.
|
|
22
22
|
|
|
23
23
|
## How to use it
|
|
24
24
|
|
|
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
|
|
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 and where uploaded files are kept, use **easy_flow-install**.
|
|
26
|
+
- To ask a visitor for a file, an admin places a file upload step on the canvas, and the app must first have a file store set through **easy_flow-install**.
|
|
27
|
+
- To add a step type, give a step type its own rule for refusing an answer, give it its own way to read an answer back, make what a step shows depend on the visitor's run, hold a run until something outside the flow has happened, add a step type of the app's own that starts another flow, keep a step's setting on one of the app's own records, put a flow on the app's own pages, start a run on a published version other than the live one, act on what a visitor answered, or act on the output a visitor's flow ended with, use **easy_flow-develop**.
|
|
27
28
|
- To give a flow a different output for each way it can end, no code is needed: an admin fills in the output on each End step on the canvas.
|
|
28
29
|
- 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.
|
|
29
30
|
- 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.
|
|
@@ -53,9 +54,11 @@ easy_flow declares no commands of its own for this local. Its surface is split b
|
|
|
53
54
|
- **Flow output** — the text an admin writes into the output setting of an End step, given as the result of a stored run that has reached that step. A flow may hold several End steps with a different output on each, so the output says which way the flow ended. A run that has not reached an End step, or that ended on one whose output was left blank, gives no output.
|
|
54
55
|
- **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.
|
|
55
56
|
- **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.
|
|
57
|
+
- **File upload** — the built-in step type that asks the visitor for one file: a question text, the accepted file types, and a yes-or-no required setting. The accepted file types are a comma-separated list of extensions, such as `.pdf, .png`, and an empty list accepts any file. A file whose extension is not on the list is refused with a message naming the accepted types, such as "Choose a .pdf or .png file.", and is not handed to the file store. A required file upload left empty is refused with "Choose a file to go on." A file can only be uploaded in a flow that keeps a stored run, so a flow that carries its answers in the page refuses one with "A file can only be uploaded in a flow that saves its runs."
|
|
58
|
+
- **File store** — the object the app sets to keep uploaded files. It takes an accepted file, keeps it, and returns a reference, and that reference is what the run records as the step's answer. When a step's answer is sent as a list holding several files, such as from a step type of the app's own, each file is handed to the store and the run records the list of references. It also gives the name of the file a reference points to, which is what the read-back shows. When a file is accepted and the app has set no file store, the step fails with an error saying a file store must be set.
|
|
56
59
|
- **Hint** — a short line of text shown under an answer's label on a question step.
|
|
57
60
|
- **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.
|
|
58
|
-
- **Read-back** — the list a visitor sees when a flow finishes: each question or
|
|
61
|
+
- **Read-back** — the list a visitor sees when a flow finishes: each question, checklist or file upload asked, beside the label of the answer given. A step type may give its own label for an answer, and a file upload's answer is read back as the name the file store gives for it. 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.
|
|
59
62
|
- **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."
|
|
60
63
|
- **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."
|
|
61
64
|
- **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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: easy_flow-install
|
|
3
|
-
description: Use to hook easy_flow into a project — copying and running its migrations, mounting the engine for each host, setting the controller it inherits from, declaring hosts with their layouts, access methods, the record that owns their flows and the step types each offers, choosing the default step drawing, turning on optional checks, giving the app a job queue that moves a run on once the flow its Flow step, or its own step type that starts a flow, started ends, and reaching the admin pages.
|
|
3
|
+
description: Use to hook easy_flow into a project — copying and running its migrations, mounting the engine for each host, setting the controller it inherits from, declaring hosts with their layouts, access methods, the record that owns their flows and the step types each offers, choosing the default step drawing, setting the file store that keeps files visitors upload, turning on optional checks, giving the app a job queue that moves a run on once the flow its Flow step, or its own step type that starts a flow, started ends, and reaching the admin pages.
|
|
4
4
|
tools: Bash, Read, Edit
|
|
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
|
---
|
|
@@ -18,6 +18,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
18
18
|
- `EasyFlow.base_controller=` — the name, as a string, of the host controller every engine controller inherits from. Defaults to `"ActionController::Base"`.
|
|
19
19
|
- `EasyFlow.host` — declares a named host with its visitor layout, admin layout, admin authentication method, visitor authorization method, refusal method, owner method, and the step types its admins can add on the canvas.
|
|
20
20
|
- `EasyFlow.draws_with` — the partial that draws a visitor's step when the step's type names none. Defaults to the engine's own `easy_flow/steps/choosing`.
|
|
21
|
+
- `EasyFlow.file_store` — the object that keeps a file a visitor uploads at a File upload step and names it again later. Defaults to none, and with none set an uploaded file is refused with `EasyFlow::NoFileStore`.
|
|
21
22
|
- `EasyFlow.check` — turns on an optional flow check: `:unrouted_value`, `:unfollowed_path` or `:dead_end`.
|
|
22
23
|
- `/manage/flows` — the admin pages, under each mount path, where flows are listed, created, drawn on the canvas, previewed and published.
|
|
23
24
|
|
|
@@ -44,7 +45,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
44
45
|
host.visitor_authorization_method = :easy_flow_visitor_permitted?
|
|
45
46
|
host.refusal_method = :refuse_flow
|
|
46
47
|
host.owner_method = :current_account
|
|
47
|
-
host.offers = %i[question checklist condition switch compare flow_step]
|
|
48
|
+
host.offers = %i[question checklist file_upload condition switch compare flow_step]
|
|
48
49
|
end
|
|
49
50
|
```
|
|
50
51
|
|
|
@@ -54,7 +55,7 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
54
55
|
- `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.
|
|
55
56
|
- `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`.
|
|
56
57
|
- `owner_method` — a method on the base controller, called with no arguments, returning the record whose flows the current request works with, such as the signed-in account. When set, the admin pages list, create and edit only that record's flows, a visitor runs only that record's flows, and two records may each hold a flow with the same slug. With none set, every flow in the host is shared by everyone who reaches it. Ask the developer whether each host's flows belong to one record or are shared.
|
|
57
|
-
- `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`, `compare` and `flow_step`, the
|
|
58
|
+
- `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`, `file_upload`, `condition`, `switch`, `compare` and `flow_step`, `file_upload` being the File upload step, which needs the file store from step 10, and `flow_step` being the Flow step, which names one of the same host's flows. A step type the app declares is named by the id it was declared with, 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.
|
|
58
59
|
|
|
59
60
|
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.
|
|
60
61
|
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:
|
|
@@ -76,9 +77,20 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
76
77
|
- 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.
|
|
77
78
|
- 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.
|
|
78
79
|
- 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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
- It is not used for a File upload step. A File upload step is always drawn by the engine as one file field.
|
|
81
|
+
10. Ask the developer whether any host will offer the File upload step, and if so, where the app keeps uploaded files. The engine keeps no file itself and has no default store. Ask the developer to point at the app's store or write one. Do not pick a storage service for them. Set it in the initializer:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
EasyFlow.file_store = UploadedFiles.new
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- The store answers `keep(file)`, called with the uploaded file, which responds to `original_filename`. It keeps the file and returns a string reference to it, and that reference is what the run records as the step's answer.
|
|
88
|
+
- When several files are sent for one step, `keep` is called once for each file, and the run records the list of references it returned, in the order the files were sent.
|
|
89
|
+
- The store answers `name_of(reference)`, called with a reference `keep` returned. It returns the name the visitor's completion page shows for that answer. When it returns nothing, the page shows the reference itself.
|
|
90
|
+
- With no store set, a visitor who submits a file at any step of a stored run gets the app's error page, raised as `EasyFlow::NoFileStore`.
|
|
91
|
+
11. 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.
|
|
92
|
+
12. Check that the app loads Active Job (`require "rails/all"` or `require "active_job/railtie"` in `config/application.rb`) and has a queue adapter that runs jobs from the `default` queue. When a run started by a Flow step, or by a step type of the app's own that starts another flow, ends, the engine records its output in the run it was started from and queues a job on `default` that moves that run on. If no adapter runs that queue, the run that holds the step stays at it. If `config.active_job.queue_adapter` is not set for an environment, ask the developer which adapter to use there.
|
|
93
|
+
13. Start the server and open `<mount path>/manage/flows` for each host.
|
|
82
94
|
|
|
83
95
|
## Conventions
|
|
84
96
|
|
|
@@ -96,12 +108,15 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
|
|
|
96
108
|
- After install, check that a Flow step whose chosen flow exists but whose version is not set, does not exist, or was never published or has been retired or withdrawn shows "unrunnable version" in red on its canvas card, and that saving a version or publishing is refused until it names a live or superseded version. This check is always on and is not turned on with `EasyFlow.check`.
|
|
97
109
|
- After install, check that a Flow step whose chosen flow is the flow it sits in, or whose chosen flow leads back to it through the Flow steps of other flows, shows "circular" in red on its canvas card, and that saving a version or publishing is refused until it is fixed. This check is always on and is not turned on with `EasyFlow.check`.
|
|
98
110
|
- When a stored run reaches a Flow step, the engine starts a run of the chosen flow and records the run it was started from. That record needs the migration that adds a parent run to the runs table, so an app upgrading from a version without it runs `bin/rails easy_flow:install:migrations` and `bin/rails db:migrate` before publishing a flow with a Flow step.
|
|
99
|
-
- After publishing a flow with a Flow step, check that when a visitor finishes the chosen flow, the run that holds the Flow step moves on along the connection named after the output that flow ended with. If it stays at the Flow step, check the queue adapter from step
|
|
100
|
-
- When a chosen flow ends at an End step with no output filled in, the engine records nothing at the Flow step, and the job queued on `default` fails with `EasyFlow::InnerFlowError` naming the Flow step. The run that holds the Flow step stays at it. Check that the queue adapter from step
|
|
111
|
+
- After publishing a flow with a Flow step, check that when a visitor finishes the chosen flow, the run that holds the Flow step moves on along the connection named after the output that flow ended with. If it stays at the Flow step, check the queue adapter from step 12.
|
|
112
|
+
- When a chosen flow ends at an End step with no output filled in, the engine records nothing at the Flow step, and the job queued on `default` fails with `EasyFlow::InnerFlowError` naming the Flow step. The run that holds the Flow step stays at it. Check that the queue adapter from step 12 keeps or reports failed jobs, and that every End step of a flow chosen on a Flow step has an output.
|
|
101
113
|
- When a run reaches a Flow step whose chosen version was retired or withdrawn after the flow was published, the engine raises `EasyFlow::InnerFlowError` naming the Flow step and starts no run. The engine does not turn this error into a refusal, so the visitor gets the app's error page. Check that the app's error reporting captures it.
|
|
102
114
|
- After install, check that removing a flow from `<mount path>/manage/flows` shows "Flow removed." and takes the flow off the list. Removing a flow deletes its versions and its runs, and needs no setting in the initializer.
|
|
103
115
|
- After publishing a flow with a Flow step, check that while a run of it is stopped at that Flow step, removing the chosen flow is refused and the flow list shows "This flow cannot be removed while" followed by the waiting flow's title, or its slug when it has no title, and "waits on one of its runs". If no message is shown, check that the admin layout shows `flash[:alert]`.
|
|
104
116
|
- A step type of the app's own that starts another flow is treated as a Flow step by every check above: the "missing flow", "unrunnable version" and "circular" flags, the refusal to remove a flow a waiting run depends on, and the job on `default`. It runs the flow and version its own settings name, or the ones its declaration works out from its other settings, and needs no setting in the initializer beyond being listed in a host's `offers` when that host sets `offers`.
|
|
117
|
+
- After setting `EasyFlow.file_store`, publish a flow with a File upload step whose "What this keeps of a run" detail is "Every answer as it is given", upload a file as a visitor, and check that the completion page shows the name the store gives it.
|
|
118
|
+
- A File upload step takes a file only in a flow that keeps every answer as it is given. In a flow that keeps nothing or keeps the run only once it finishes, the visitor is told "A file can only be uploaded in a flow that saves its runs." and cannot go on with a file chosen.
|
|
119
|
+
- A File upload step's "Accepted file types" setting is a comma-separated list of extensions, such as `.pdf, .png`. A file of another kind is refused with a message naming the accepted kinds and is never passed to the file store.
|
|
105
120
|
- 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.
|
|
106
121
|
- 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.
|
|
107
122
|
- The initializer runs once at boot, so a change to it needs a server restart.
|
data/the_local/interface.yml
CHANGED
|
@@ -6,12 +6,14 @@ install:
|
|
|
6
6
|
- EasyFlow.base_controller=
|
|
7
7
|
- EasyFlow.host
|
|
8
8
|
- EasyFlow.draws_with
|
|
9
|
+
- EasyFlow.file_store
|
|
9
10
|
- EasyFlow.check
|
|
10
11
|
- /manage/flows
|
|
11
12
|
|
|
12
13
|
develop:
|
|
13
14
|
- EasyFlow::Step
|
|
14
15
|
- EasyFlow.step
|
|
16
|
+
- labels_answer_by
|
|
15
17
|
- EasyFlow::FlowsController
|
|
16
18
|
- hosted_by
|
|
17
19
|
- routed_by
|
|
@@ -35,6 +37,7 @@ sources:
|
|
|
35
37
|
- app/models/easy_flow/terminal.rb
|
|
36
38
|
- app/models/easy_flow/steps/question.rb
|
|
37
39
|
- app/models/easy_flow/steps/checklist.rb
|
|
40
|
+
- app/models/easy_flow/steps/file_upload.rb
|
|
38
41
|
- app/models/easy_flow/step_type.rb
|
|
39
42
|
- app/models/easy_flow/choice.rb
|
|
40
43
|
- app/jobs/easy_flow/advance_parent_job.rb
|
|
@@ -48,6 +51,7 @@ sources:
|
|
|
48
51
|
- app/views/easy_flow/flows/complete.html.erb
|
|
49
52
|
- app/views/easy_flow/steps/_choosing.html.erb
|
|
50
53
|
- app/views/easy_flow/steps/_ticking.html.erb
|
|
54
|
+
- app/views/easy_flow/steps/_uploading.html.erb
|
|
51
55
|
- app/controllers/easy_flow/manage/previews_controller.rb
|
|
52
56
|
- config/routes.rb
|
|
53
57
|
- db/migrate/20260924120000_create_easy_flow_definitions.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.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- app/models/easy_flow/step_type.rb
|
|
119
119
|
- app/models/easy_flow/step_type/declaration.rb
|
|
120
120
|
- app/models/easy_flow/steps/checklist.rb
|
|
121
|
+
- app/models/easy_flow/steps/file_upload.rb
|
|
121
122
|
- app/models/easy_flow/steps/question.rb
|
|
122
123
|
- app/models/easy_flow/switch.rb
|
|
123
124
|
- app/models/easy_flow/terminal.rb
|
|
@@ -135,6 +136,7 @@ files:
|
|
|
135
136
|
- app/views/easy_flow/manage/versions/index.html.erb
|
|
136
137
|
- app/views/easy_flow/steps/_choosing.html.erb
|
|
137
138
|
- app/views/easy_flow/steps/_ticking.html.erb
|
|
139
|
+
- app/views/easy_flow/steps/_uploading.html.erb
|
|
138
140
|
- app/views/layouts/easy_flow/application.html.erb
|
|
139
141
|
- config/routes.rb
|
|
140
142
|
- db/migrate/20260924120000_create_easy_flow_definitions.rb
|