easy_flow 0.11.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62d908e1b9ce4b3b8845a80302af79e28801fa9ec737cf3b09c0032f123d6137
4
- data.tar.gz: 5118f5f7f8a333facae18c4a61e725cdc099d1e97e69c0c26e0065aa427e621b
3
+ metadata.gz: a271be372fa760d850d21b7c3e90719fe8aef78d378b07c249896fd9f69a6ef4
4
+ data.tar.gz: e5e0130c5bc885bd7865e938998a8ebec35549e07c90428764409cce2a4ee2e8
5
5
  SHA512:
6
- metadata.gz: 6cd630296d7d273e0992399e0678dcc655872f95cd7555bb8064f2bb37bf516e2dfcdddbe114551601dc085a0d3c97499f71a20ee1a7b7e993adc1d0aa3c4596
7
- data.tar.gz: e796ff7990e86671ef6c49f17ee36b45c97fc064c9efdf335c07dfa2e1b63a8c07ee126d41881636171a019c5f77a1213069a1ccef310783331ed14ed19f4bdf
6
+ metadata.gz: dc39e2bb8a0ce09d34d74d2e3c5635c8f7ea70094a7dfc83a4422b7254980e2a6c311c689ada061bd61cb7339d6a1d64e30438daa47c0a0d8f07a710a886fdc0
7
+ data.tar.gz: e7160a75079db724308f2dbbcf1d6263730188172b8198d037d9429713350086c87208f8803ede07084c92efb0993a0929988d3a50654411f2c643fc3606c681
@@ -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, value.is_a?(Array) ? value.compact_blank : value.to_s)
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,9 +1,20 @@
1
1
  module EasyFlow
2
2
  ChosenFlow = Data.define(:flow_id, :number) do
3
- def self.of(node)
3
+ def self.of(node, registry: EasyFlow.registry)
4
+ chosen = (registry.fetch(node.type).flow_chosen_by(node) if registry.registered?(node.type))
5
+ return worked_out(chosen) if chosen
6
+
4
7
  new(flow_id: node.config["flow"].presence, number: node.config["version"].presence)
5
8
  end
6
9
 
10
+ def self.worked_out(chosen)
11
+ new(flow_id: chosen[:flow].presence, number: chosen[:version].presence)
12
+ end
13
+
14
+ def name
15
+ [ flow&.title, ("version #{number}" if number) ].compact.join(", ").presence
16
+ end
17
+
7
18
  def flow
8
19
  Definition.find_by(id: flow_id) if flow_id
9
20
  end
@@ -13,10 +13,7 @@ module EasyFlow
13
13
 
14
14
  waits_until { |_node, _state| false }
15
15
 
16
- names_by do |node|
17
- chosen = ChosenFlow.of(node)
18
- [ chosen.flow&.title, ("version #{chosen.number}" if chosen.number) ].compact.join(", ").presence
19
- end
16
+ names_by { |node| ChosenFlow.of(node).name }
20
17
 
21
18
  def self.endings_of(node)
22
19
  version = ChosenFlow.of(node).version
@@ -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(&chooser)
78
+ @flow_chooser = chooser
79
+ end
80
+
73
81
  def names_by(field = nil, &naming)
74
82
  @naming_field = field
75
83
  @naming = naming
@@ -116,9 +124,9 @@ module EasyFlow
116
124
 
117
125
  def to_step_type
118
126
  StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
119
- ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, behaviour: @behaviour, routing: @routing,
127
+ ends_here: @ends_here, begins_here: @begins_here, starts_a_flow: @starts_a_flow, flow_chooser: @flow_chooser, behaviour: @behaviour, routing: @routing,
120
128
  display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
121
- outputs: @declared_outputs, answer_check: @answer_check, readiness: @readiness)
129
+ outputs: @declared_outputs, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness)
122
130
  end
123
131
 
124
132
  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, 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, 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
@@ -16,6 +16,7 @@ module EasyFlow
16
16
  @ends_here = ends_here
17
17
  @begins_here = begins_here
18
18
  @starts_a_flow = starts_a_flow
19
+ @flow_chooser = flow_chooser
19
20
  @behaviour = behaviour
20
21
  @routing = routing
21
22
  @display = display
@@ -24,6 +25,7 @@ module EasyFlow
24
25
  @naming = naming
25
26
  @outputs = outputs
26
27
  @answer_check = answer_check
28
+ @answer_labelling = answer_labelling
27
29
  @readiness = readiness
28
30
  end
29
31
 
@@ -45,6 +47,10 @@ module EasyFlow
45
47
  @answer_check&.call(node, value)
46
48
  end
47
49
 
50
+ def answer_label(node, value)
51
+ @answer_labelling&.call(node, value)
52
+ end
53
+
48
54
  def waits?
49
55
  @readiness.present?
50
56
  end
@@ -54,7 +60,7 @@ module EasyFlow
54
60
  end
55
61
 
56
62
  def name_of(node)
57
- @naming&.call(node).presence || node.config[naming_field.to_s].presence
63
+ @naming&.call(node).presence || node.config[naming_field.to_s].presence || worked_out_flow_name(node)
58
64
  end
59
65
 
60
66
  def acts?
@@ -84,5 +90,14 @@ module EasyFlow
84
90
  def starts_a_flow?
85
91
  @starts_a_flow
86
92
  end
93
+
94
+ def flow_chosen_by(node)
95
+ @flow_chooser&.call(node)
96
+ end
97
+
98
+ def worked_out_flow_name(node)
99
+ chosen = flow_chosen_by(node)
100
+ ChosenFlow.worked_out(chosen).name if chosen
101
+ end
87
102
  end
88
103
  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
@@ -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| %>
@@ -0,0 +1,5 @@
1
+ <fieldset>
2
+ <legend class="ks-section-title mb-5 text-xl font-semibold leading-snug"><%= step.text %></legend>
3
+
4
+ <%= file_field_tag "answers[#{step.id}]", accept: step.accepts.presence %>
5
+ </fieldset>
@@ -20,6 +20,7 @@ module EasyFlow
20
20
 
21
21
  EasyFlow::Steps::Question.register
22
22
  EasyFlow::Steps::Checklist.register
23
+ EasyFlow::Steps::FileUpload.register
23
24
  EasyFlow::Condition.register
24
25
  EasyFlow::Switch.register
25
26
  EasyFlow::Compare.register
@@ -1,3 +1,3 @@
1
1
  module EasyFlow
2
- VERSION = "0.11.0"
2
+ VERSION = "0.13.0"
3
3
  end
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, 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 eight: 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), 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. 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.
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,9 +82,10 @@ 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
- - `starts_a_flow` — when a stored run stops at a step of this type, a run of another flow is started. 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".
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`.
88
90
  - Once the step's `flow` setting names a flow, clicking the step takes the admin to that flow's page, and a `Settings` button on the step opens the step's own settings. Any other `:flow` setting the type declares is not followed by the click and is not read when the run is started.
89
91
  - A flow's page reached by clicking the step shows a `Back to <title>` button, or `Back to <slug>` when the flow it came from has no title, which returns the admin to the flow the step is in. The button is shown only when that flow belongs to the same host.
@@ -92,6 +94,37 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
92
94
  - The step's card shows `missing flow` while its `flow` setting is blank or names no existing flow, and `unrunnable version` while the flow exists but its `version` setting is blank, names no version of that flow, or names one that is a draft, retired or withdrawn.
93
95
  - The step's card shows `circular` while its `flow` setting names the flow the step sits in, or names a flow whose chosen version holds a `starts_a_flow` step that leads back to it the same way, through any number of flows. Each flow along the way is read at the version named in the `version` setting of the step that leads to it.
94
96
  - While `missing flow`, `unrunnable version` or `circular` shows, saving the flow as a new version and publishing it are refused. These checks are always made and are not among the optional checks set through `easy_flow-install`, so the type needs no `required:` or `check:` on those two settings for them.
97
+ - `chooses_flow { |node| { flow: ..., version: ... } }` — for a `starts_a_flow` type, works out the flow and version a step runs from the step's own settings instead of reading the settings named `flow` and `version`. Ask the developer which settings decide the flow and version, and how. Do not pick.
98
+ - The block is given the node only. `node.config` holds the admin's settings with string keys, without any setting declared with `kept_on:`.
99
+ - It returns a hash with the symbol keys `flow:`, the flow's id as an integer or a string, and `version:`, the version's number. A blank value under either key is treated as not chosen.
100
+ - When the block returns `nil`, the step falls back to the settings named `flow` and `version`.
101
+ - What it returns is used everywhere the settings named `flow` and `version` are used otherwise: the run started when a stored run stops at the step, the `missing flow`, `unrunnable version` and `circular` marks and the refusal to save or publish, and the flow's page opened by clicking the step on the canvas.
102
+ - The flow is found by its id alone, not through the host or owner. Ask the developer which flows the block may name, and look the flow up through the host inside the block when it must be limited to one host or owner.
103
+ - A step of a type with no `names_by`, or whose `names_by` gives a blank, is named after the flow and version the block returns, as that flow's title and the version number, such as `Onboarding, version 2`, or `version 2` alone when the flow has no title. That name is shown on the canvas and in `Waiting for <name>.` while a run waits at the step. When the block returns `nil`, the step is not named after the settings named `flow` and `version`, and falls back to its id on the canvas and its `step_name` in the waiting message. Ask the developer whether that name is what admins and visitors should read, and declare `names_by` when it is not.
104
+ - The block is called each time the canvas draws the step, each time the step's name is shown, each time the flow is saved as a version or published, and when a run stops at the step, so it must not be slow and must not change anything.
105
+ - A type that routes on the new run's output declares an `output` whose `values:` are the outputs the chosen flow can end with, or the canvas offers no connections to label.
106
+
107
+ ```ruby
108
+ module FlowSteps
109
+ class Offer
110
+ include EasyFlow::Step
111
+
112
+ step_name "Offer"
113
+
114
+ setting :plan, type: :select, options: %w[basic premium], required: true
115
+
116
+ starts_a_flow
117
+ waits_until { |_node, _state| false }
118
+ chooses_flow { |node| OfferFlow.for_plan(node.config["plan"]) }
119
+
120
+ def route(node, state)
121
+ state[node.id]
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ Here `OfferFlow.for_plan` is the app's own lookup, returning `{ flow: <id>, version: <number> }` or `nil`.
95
128
  - `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.
96
129
  - `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.
97
130
  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):
@@ -144,6 +177,13 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
144
177
 
145
178
  An input that submits a hash, such as one named `answers[<step id>][key]`, is dropped and treated as blank.
146
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
+
147
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.
148
188
 
149
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:
@@ -170,13 +210,13 @@ A Rails engine for flows an admin draws on a canvas and a visitor runs one step
170
210
 
171
211
  - The block is given `node` and `state` only, the same as `process`, and is not given the run or its owner. Ask the developer what the step waits for, and how the block finds it from the step's settings and the answers recorded before it. If it cannot be found from those, stop and ask, since the block has nothing else to read.
172
212
  - The block is called each time the visitor loads the step page, and each time the app calls `run.advance`. Keep it to a single lookup.
173
- - While the block returns a falsy value, the visitor is shown `Waiting for <name>.` and no form, no Next button and no Back button. `<name>` is what `names_by` gives, or the `step_name` when there is none, so give the type a name a visitor can read.
213
+ - While the block returns a falsy value, the visitor is shown `Waiting for <name>.` and no form, no Next button and no Back button. `<name>` is what `names_by` gives, or for a type that declares `chooses_flow` the chosen flow's title and version, or the `step_name` when there is neither, so give the type a name a visitor can read.
174
214
  - Once the block returns a truthy value, `true` is recorded under the step's id, or the value `process` returns when the type also defines `process`, and the run goes on to the next step.
175
215
  - Do not declare `awaits_input` on a type that waits. The waiting message replaces the form, so the input is never shown.
176
216
  - The page does not reload itself. Ask the developer how the run should move on when what it waits for happens:
177
217
  - The visitor reloads the page. This needs nothing more, and works whether or not the flow keeps a stored run.
178
218
  - The app calls `run.advance` where the event is handled, such as a webhook or a job. See "Read a run and its answers". This needs a stored run, so it only applies to a flow the admin set to save each step.
179
- 9. Restart the server, open a flow on the canvas of each host that should offer the type, and check the type is in the palette, its settings show, and a preview walks through it. On a host that names a list of offered step types without the type's id, check the type is not in the palette. For a type that declares `starts_a_flow`, check the step is marked `Runs a flow`, that once a flow is picked in its settings clicking the step goes to that flow's page, that the `Back to` button on that page returns to the flow the step is in, and that its `Settings` button opens its settings. Also check that the step shows `missing flow` while no flow is picked, `unrunnable version` while the version number names no live or replaced version, `circular` while it names the flow it sits in, and that publishing is refused until each is fixed.
219
+ 9. Restart the server, open a flow on the canvas of each host that should offer the type, and check the type is in the palette, its settings show, and a preview walks through it. On a host that names a list of offered step types without the type's id, check the type is not in the palette. For a type that declares `starts_a_flow`, check the step is marked `Runs a flow`, that once a flow is picked in its settings clicking the step goes to that flow's page, that the `Back to` button on that page returns to the flow the step is in, and that its `Settings` button opens its settings. Also check that the step shows `missing flow` while no flow is picked, `unrunnable version` while the version number names no live or replaced version, `circular` while it names the flow it sits in, and that publishing is refused until each is fixed. For a type that declares `chooses_flow`, set the settings the block reads and check that clicking the step goes to the flow the block names, that a type with no `names_by` is named on the canvas by that flow's title and version, and that a run stopping at the step starts a run of that flow on that version.
180
220
 
181
221
  ### Keep a setting on one of the app's records
182
222
 
@@ -230,7 +270,7 @@ Use this when an admin should edit a value that belongs to one of the app's own
230
270
 
231
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.
232
272
  4. Override only the private methods the developer's answers call for:
233
- - `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.
234
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".
235
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.
236
276
  5. Visit `/<path>/<slug>` for a published flow and walk it to the end.
@@ -261,7 +301,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
261
301
 
262
302
  1. This needs the engine's migrations installed, which `easy_flow-install` does.
263
303
  2. What holds when a stored run stops at a Flow step, whether on the visitor's page load or on `run.advance`:
264
- - A run of the flow named in the step's settings is started on the version with the number in its settings. The flow is found by its id alone and not through the host, since a `:flow` setting is not checked on save.
304
+ - A run of the flow named in the step's settings is started on the version with the number in its settings, or of the flow and version its type's `chooses_flow` block returns. The flow is found by its id alone and not through the host, since a `:flow` setting is not checked on save.
265
305
  - The new run's `parent_run` is the run that reached the step, and its `parent_step` is the step's id as a string, or the visit key such as `"sub@2"` on a later visit in a loop.
266
306
  - The new run is moved on at once past every step that acts without the visitor, the same as `run.advance`. When it stops at a Flow step of its own, that starts a further run the same way.
267
307
  - It is started once per visit to the step, so loading the page again or calling `advance` again starts no second one.
@@ -317,7 +357,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
317
357
 
318
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" })`.
319
359
  2. Read from a run:
320
- - `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.
321
361
  - `run.flow` — the flow it belongs to.
322
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.
323
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.
@@ -338,7 +378,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
338
378
 
339
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 `@`.
340
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.
341
- - `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) }`.
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) }`.
342
382
  - `step(id)` finds the step for either its id or a visit key.
343
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.
344
384
  - `steps_on_path(answers)` — the steps on the path taken, each built by its type's `displays_by` with a `nil` run.
@@ -347,7 +387,7 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
347
387
  ## Conventions
348
388
 
349
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.
350
- - 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`.
351
391
  - Register class-based step types inside `to_prepare`. A type registered anywhere else is lost on code reload in development.
352
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.
353
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.
@@ -355,7 +395,9 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
355
395
  - A setting declared with `kept_on:` lives only on the app's record, written when the admin saves the step, so a step's code never finds it in `node.config`.
356
396
  - A `kept_on:` lambda is given only the step's settings, so it cannot limit which records an admin reaches by admin, account or host.
357
397
  - A `:flow` setting holds a flow's id as a string and is not checked against the flows offered, so code reading it looks the flow up through the host.
358
- - A `starts_a_flow` type reads the flow and version to run from the settings named `flow` and `version` and no others.
398
+ - A `starts_a_flow` type reads the flow and version to run from the settings named `flow` and `version`, unless it declares `chooses_flow` and that block returns a choice.
399
+ - A `chooses_flow` block is given only the step, finds the flow by id with no host or owner scope, and is called on every canvas draw, name shown, save and publish, so it limits the flows it names itself and changes nothing.
400
+ - A step of a `chooses_flow` type with no `names_by` is named by the chosen flow's title and version number.
359
401
  - A flow holding a `starts_a_flow` step whose `flow` names no existing flow, or whose `version` names no live or replaced version of it, or whose `flow` leads back to the flow the step sits in directly or through other flows, cannot be saved as a version or published, and these checks cannot be turned off.
360
402
  - On the canvas, clicking a step of a `starts_a_flow` type whose `flow` setting names a flow goes to that flow's page, which has a `Back to` button to the flow the step is in, and the step's settings open from its `Settings` button.
361
403
  - A type that routes declares the values its output takes, or the canvas offers no connections to label.
@@ -368,6 +410,8 @@ Use this when a visitor's run reaches a Flow step and the app must take the visi
368
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.
369
411
  - Condition, switch and compare do not branch on an array answer. Branching on one takes an app step type that defines `route`.
370
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.
371
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 `""`.
372
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.
373
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, 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 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 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, 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**.
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 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.
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.
@@ -64,6 +67,7 @@ easy_flow declares no commands of its own for this local. Its surface is split b
64
67
  - **Flow step** — a built-in step that names another flow and the number of one of its versions. The admin picks the flow from the same host's flows, listed by title as the admin's list of flows shows them. The step has one connection for each different output the End steps of that version write, and none when no flow or version is picked or no End step there has an output. On the canvas its card is named after the flow's title and the version, such as "Onboarding, version 2", and is marked "Runs a flow". Once a flow is picked, clicking the card opens that flow's canvas, and the card's Settings button opens the step's own settings instead. A canvas opened from a Flow step's card shows a "Back to" button named after the title of the flow it was opened from, or its slug when it has no title, which returns to that flow's canvas. The button only appears when that flow belongs to the same host, and to the same owner when the host has one. While the admin is connecting steps, clicking the card picks it as the connection's target. A run that reaches a Flow step stops there. When the run is a stored run, it starts one inner run of the named flow on the picked version. A run that carries its answers in the page, and an admin's preview, stop at the Flow step, start no run, and never move past it.
65
68
  - **Unrunnable Flow step** — a Flow step whose picked flow or version cannot be run. Its card shows "missing flow" when no flow is picked or the picked flow no longer exists, and "unrunnable version" when the picked flow has no version with that number or that version is a draft, retired or withdrawn. These are always on, unlike the optional checks below, and the flow holding such a step cannot be saved as a new version or published until it is fixed.
66
69
  - **Circular Flow step** — a Flow step that leads back to the flow it sits in. That is either a Flow step naming its own flow, or one naming a flow whose picked version holds a Flow step that leads back the same way, through any number of flows. Each flow along the way is read at the version its Flow step picked. Its card shows "circular", and like an unrunnable Flow step this is always on and keeps the flow from being saved as a new version or published until it is fixed.
70
+ - **Flow-starting step type** — a step type of the app's own that starts another flow the way the Flow step does. By default it runs the flow and version named in its settings called flow and version. It may instead carry a rule that works out the flow and version from its other settings, and that choice is then used everywhere a Flow step's choice is: starting the inner run, the unrunnable and circular checks, and the canvas link to that flow. When the step type gives the step no name of its own, the step is named after the flow and version that rule works out, the same way a Flow step is, such as "Onboarding, version 2", on its canvas card and in the "Waiting for" line a visitor sees. Everything said below about a Flow step's inner run and handing back applies to it too.
67
71
  - **Inner run** — the run a Flow step starts. It records which run started it and at which step, and that run holds at most one inner run for each Flow step, however often it is moved on. It belongs to the same owner as the run that started it. When the picked version has been retired or withdrawn, no inner run is started and the run that reached the Flow step fails with an error naming that step. While the run that started it is stopped at the Flow step, that run can name the inner run it is waiting on, and it names none when it is stopped at any other step or has finished. Its steps that act on their own run as soon as it starts, and it stops at its first step that asks for input or waits.
68
72
  - **Handing back** — what happens when an inner run ends. Its flow output is recorded in the run that started it, as that run's answer at the Flow step, and a background job then moves that run on along the connection labelled with the output. An inner run that ended with no output records nothing in the run that started it, and the background job then fails with an error naming the Flow step, so that run does not move on. An output is handed back only once, so an inner run moved on again after it ended changes nothing. Because the move happens in a background job, the app must run its jobs for a run to go past a Flow step.
69
73
  - **Waiting step** — a step whose type carries a rule saying whether what it waits for has happened, such as a payment clearing or a document being signed. A run that reaches it stops there, and the visitor sees "Waiting for" followed by the step's name, with no form to submit. Once the rule says it has happened, the run moves past the step the next time the visitor's page is loaded or the app moves the run on, and the step is recorded as `true`.
@@ -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 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 last 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
+ - `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
- 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.
80
- 11. 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 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 Flow step stays at it. If `config.active_job.queue_adapter` is not set for an environment, ask the developer which adapter to use there.
81
- 12. Start the server and open `<mount path>/manage/flows` for each host.
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,11 +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 11.
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 11 keeps or reports failed jobs, and that every End step of a flow chosen on a Flow step has an output.
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]`.
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.
104
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.
105
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.
106
122
  - The initializer runs once at boot, so a change to it needs a server restart.
@@ -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.11.0
4
+ version: 0.13.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