easy_flow 0.16.0 → 0.18.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_flow/canvas.js +1 -1
  3. data/app/controllers/easy_flow/flows_controller.rb +18 -4
  4. data/app/javascript/easy_flow/canvas/Canvas.jsx +1 -0
  5. data/app/javascript/easy_flow/canvas/Inspector.jsx +3 -1
  6. data/app/javascript/easy_flow/canvas/Records.jsx +8 -2
  7. data/app/models/easy_flow/all_or_any.rb +33 -0
  8. data/app/models/easy_flow/canvas.rb +17 -2
  9. data/app/models/easy_flow/compare.rb +2 -4
  10. data/app/models/easy_flow/compare_two_values.rb +32 -0
  11. data/app/models/easy_flow/completed_at.rb +34 -0
  12. data/app/models/easy_flow/condition.rb +3 -1
  13. data/app/models/easy_flow/count.rb +30 -0
  14. data/app/models/easy_flow/digest.rb +19 -11
  15. data/app/models/easy_flow/progress/kept.rb +4 -0
  16. data/app/models/easy_flow/progress/loose.rb +12 -4
  17. data/app/models/easy_flow/progress.rb +2 -2
  18. data/app/models/easy_flow/run.rb +19 -6
  19. data/app/models/easy_flow/runner.rb +11 -11
  20. data/app/models/easy_flow/settings.rb +5 -2
  21. data/app/models/easy_flow/step.rb +6 -2
  22. data/app/models/easy_flow/step_type/declaration.rb +13 -3
  23. data/app/models/easy_flow/step_type.rb +18 -5
  24. data/app/models/easy_flow/validator.rb +12 -1
  25. data/app/models/easy_flow/wait.rb +38 -0
  26. data/app/views/easy_flow/flows/step.html.erb +6 -0
  27. data/db/migrate/20261011130000_keep_step_visits_whole_in_easy_flow_runs.rb +23 -0
  28. data/lib/easy_flow/engine.rb +4 -0
  29. data/lib/easy_flow/version.rb +1 -1
  30. data/the_local/agents/easy_flow-develop.md +100 -20
  31. data/the_local/agents/easy_flow-info.md +25 -11
  32. data/the_local/agents/easy_flow-install.md +19 -5
  33. data/the_local/interface.yml +6 -0
  34. metadata +7 -1
@@ -15,11 +15,15 @@ module EasyFlow
15
15
  end
16
16
 
17
17
  def record(step_id, value)
18
- update!(recorded: recorded.merge(step_id => value))
18
+ update!(recorded: visits.merge(step_id.to_s => { "value" => value, "completed_at" => Time.current.utc.iso8601 }))
19
19
  end
20
20
 
21
21
  def recorded
22
- super.to_h.symbolize_keys
22
+ visits.to_h { |step_id, visit| [ step_id.to_sym, visit["value"] ] }
23
+ end
24
+
25
+ def completed_at
26
+ visits.select { |_step_id, visit| visit["completed_at"] }.to_h { |step_id, visit| [ step_id.to_sym, Time.zone.parse(visit["completed_at"]) ] }
23
27
  end
24
28
 
25
29
  def advance
@@ -31,11 +35,16 @@ module EasyFlow
31
35
  end
32
36
 
33
37
  def next_step(state)
34
- digest.next_step(state.transform_keys(&:to_s))
38
+ digest.next_step(state.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
35
39
  end
36
40
 
37
41
  def output
38
- digest.output(recorded.transform_keys(&:to_s))
42
+ digest.output(recorded.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
43
+ end
44
+
45
+ def held_until
46
+ stopped_at = next_step(recorded)
47
+ Wait.held_until(digest.step(stopped_at.id), CompletedAt.latest(completed_at)) if stopped_at&.type == "wait"
39
48
  end
40
49
 
41
50
  def waiting_on
@@ -44,7 +53,7 @@ module EasyFlow
44
53
  end
45
54
 
46
55
  def walked(state)
47
- digest.state_on_path(state.transform_keys(&:to_s))
56
+ digest.state_on_path(state.transform_keys(&:to_s), completed_at.transform_keys(&:to_s))
48
57
  end
49
58
 
50
59
  def digest
@@ -53,7 +62,7 @@ module EasyFlow
53
62
 
54
63
  def discard_last
55
64
  last = walked(recorded).keys.map(&:to_sym).last
56
- update!(recorded: recorded.except(last)) if last
65
+ update!(recorded: visits.except(last.to_s)) if last
57
66
  end
58
67
 
59
68
  def pinned_steps
@@ -62,6 +71,10 @@ module EasyFlow
62
71
 
63
72
  private
64
73
 
74
+ def visits
75
+ read_attribute(:recorded).to_h
76
+ end
77
+
65
78
  def started_on_a_published_version
66
79
  errors.add(:definition_version, "must be a published version of the flow") unless definition_version&.flow_id == flow_id && (definition_version.live? || definition_version.superseded?)
67
80
  end
@@ -23,13 +23,13 @@ module EasyFlow
23
23
  @digest.step(id.to_s)
24
24
  end
25
25
 
26
- def next_step(state, run: nil)
27
- node = @digest.next_step(named(state))
26
+ def next_step(state, run: nil, times: {})
27
+ node = @digest.next_step(named(state), named(times))
28
28
  narrowed(shown(node, run), node, state)
29
29
  end
30
30
 
31
31
  def run(progress)
32
- while (node = @digest.next_step(named(progress.recorded))) && goes_on?(node, named(progress.recorded))
32
+ while (node = @digest.next_step(named(progress.recorded), named(progress.completed_at))) && goes_on?(node, named(progress.recorded), CompletedAt.latest(named(progress.completed_at)))
33
33
  progress.record(node.id, result_of(@digest.step(node.id), named(progress.recorded)))
34
34
  end
35
35
  progress.start_inner(node) if node && starts_a_flow?(node)
@@ -41,12 +41,12 @@ module EasyFlow
41
41
  node.present? && decided?(node, state)
42
42
  end
43
43
 
44
- def drawing_at(state)
45
- Drawing.of(@digest.next_step(named(state)), @registry)
44
+ def drawing_at(state, times: {})
45
+ Drawing.of(@digest.next_step(named(state), named(times)), @registry)
46
46
  end
47
47
 
48
- def state_on_path(state)
49
- @digest.state_on_path(named(state)).symbolize_keys
48
+ def state_on_path(state, times: {})
49
+ @digest.state_on_path(named(state), named(times)).symbolize_keys
50
50
  end
51
51
 
52
52
  def questions_left(state)
@@ -67,8 +67,8 @@ module EasyFlow
67
67
  @registry.registered?(node.type) && @registry.fetch(node.type).acts?
68
68
  end
69
69
 
70
- def goes_on?(node, state)
71
- acts?(node) || ready?(node, state) || decided?(node, state)
70
+ def goes_on?(node, state, times = {})
71
+ acts?(node) || ready?(node, state, times) || decided?(node, state)
72
72
  end
73
73
 
74
74
  def allowed(node, state)
@@ -87,8 +87,8 @@ module EasyFlow
87
87
  !choices.nil? && choices.size <= 1
88
88
  end
89
89
 
90
- def ready?(node, state)
91
- @registry.registered?(node.type) && @registry.fetch(node.type).waits? && @registry.fetch(node.type).ready?(node, state)
90
+ def ready?(node, state, times = {})
91
+ @registry.registered?(node.type) && @registry.fetch(node.type).waits? && @registry.fetch(node.type).ready?(node, state, times)
92
92
  end
93
93
 
94
94
  def result_of(node, state)
@@ -1,13 +1,16 @@
1
1
  module EasyFlow
2
2
  class Settings
3
- attr_reader :fields, :labels, :record_fields, :record_labels, :drawn_from, :outputs_of
3
+ attr_reader :fields, :labels, :record_fields, :record_labels, :record_choices, :record_outputs_of, :record_required, :drawn_from, :outputs_of
4
4
 
5
- def initialize(fields: {}, labels: {}, record_fields: {}, record_labels: {}, choices: {},
5
+ def initialize(fields: {}, labels: {}, record_fields: {}, record_labels: {}, record_choices: {}, record_outputs_of: {}, record_required: {}, choices: {},
6
6
  limits: {}, checks: {}, required: [], drawn_from: {}, outputs_of: {}, kept: {})
7
7
  @fields = fields
8
8
  @labels = labels
9
9
  @record_fields = record_fields
10
10
  @record_labels = record_labels
11
+ @record_choices = record_choices
12
+ @record_outputs_of = record_outputs_of
13
+ @record_required = record_required
11
14
  @choices = choices
12
15
  @limits = limits
13
16
  @checks = checks
@@ -24,8 +24,12 @@ module EasyFlow
24
24
  end
25
25
 
26
26
  def behaviours
27
- BEHAVIOURS.select { |word| method_defined?(word) }
28
- .map { |word| [ word, [], {}, ->(node, state) { new.public_send(word, node, state) } ] }
27
+ BEHAVIOURS.select { |word| method_defined?(word) }.map { |word| [ word, [], {}, behaviour(word) ] }
28
+ end
29
+
30
+ def behaviour(word)
31
+ asks_for_times = instance_method(word).arity.abs >= 3
32
+ ->(node, state, times = {}) { asks_for_times ? new.public_send(word, node, state, times) : new.public_send(word, node, state) }
29
33
  end
30
34
 
31
35
  def register(registry = EasyFlow.registry)
@@ -8,6 +8,9 @@ module EasyFlow
8
8
  @labels = {}
9
9
  @record_fields = {}
10
10
  @record_labels = {}
11
+ @record_choices = {}
12
+ @record_outputs_of = {}
13
+ @record_required = {}
11
14
  @choices = {}
12
15
  @limits = {}
13
16
  @checks = {}
@@ -26,6 +29,10 @@ module EasyFlow
26
29
  @declared_outputs += [ Output.new(name: name, type: type, label: label, values: values, from: from) ]
27
30
  end
28
31
 
32
+ def outputs_named_by(setting)
33
+ @outputs_named_by = setting
34
+ end
35
+
29
36
  def displays_by(&display)
30
37
  @display = display
31
38
  end
@@ -107,7 +114,7 @@ module EasyFlow
107
114
 
108
115
  protected
109
116
 
110
- attr_reader :fields, :labels
117
+ attr_reader :fields, :labels, :choices, :outputs_of, :required
111
118
 
112
119
  private
113
120
 
@@ -122,6 +129,9 @@ module EasyFlow
122
129
  declared = Declaration.new(:entry).tap { |entry| entry.instance_eval(&entries) }
123
130
  @record_fields[name] = declared.fields
124
131
  @record_labels[name] = declared.labels
132
+ @record_choices[name] = declared.choices
133
+ @record_outputs_of[name] = declared.outputs_of
134
+ @record_required[name] = declared.required
125
135
  end
126
136
 
127
137
  public
@@ -130,12 +140,12 @@ module EasyFlow
130
140
  StepType.new(id: @id, step_name: @step_name, settings: settings, awaits_input: @awaits_input,
131
141
  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,
132
142
  display: @display, drawn_by: @drawn_by, naming_field: @naming_field, naming: @naming,
133
- outputs: @declared_outputs, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness)
143
+ outputs: @declared_outputs, outputs_named_by: @outputs_named_by, answer_check: @answer_check, answer_labelling: @answer_labelling, readiness: @readiness)
134
144
  end
135
145
 
136
146
  def settings
137
147
  Settings.new(fields: @fields, labels: @labels, record_fields: @record_fields,
138
- record_labels: @record_labels, choices: @choices, limits: @limits, checks: @checks,
148
+ record_labels: @record_labels, record_choices: @record_choices, record_outputs_of: @record_outputs_of, record_required: @record_required, choices: @choices, limits: @limits, checks: @checks,
139
149
  required: @required, drawn_from: @drawn_from, outputs_of: @outputs_of, kept: @kept)
140
150
  end
141
151
  end
@@ -8,7 +8,7 @@ module EasyFlow
8
8
 
9
9
  def initialize(id:, step_name:, settings:, awaits_input:, behaviour:, routing:,
10
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)
11
+ naming_field: nil, naming: nil, outputs: [], outputs_named_by: nil, answer_check: nil, answer_labelling: nil, readiness: nil)
12
12
  @id = id
13
13
  @step_name = step_name
14
14
  @settings = settings
@@ -25,6 +25,7 @@ module EasyFlow
25
25
  @naming_field = naming_field
26
26
  @naming = naming
27
27
  @outputs = outputs
28
+ @outputs_named_by = outputs_named_by
28
29
  @answer_check = answer_check
29
30
  @answer_labelling = answer_labelling
30
31
  @readiness = readiness
@@ -40,8 +41,8 @@ module EasyFlow
40
41
  @behaviour&.call(node, state)
41
42
  end
42
43
 
43
- def route(node, state)
44
- @routing&.call(node, state)
44
+ def route(node, state, times = {})
45
+ @routing&.call(node, state, times)
45
46
  end
46
47
 
47
48
  def answer_problem(node, value)
@@ -56,8 +57,8 @@ module EasyFlow
56
57
  @readiness.present?
57
58
  end
58
59
 
59
- def ready?(node, state)
60
- @readiness.call(node, state)
60
+ def ready?(node, state, times = {})
61
+ @readiness.call(node, state, times)
61
62
  end
62
63
 
63
64
  def name_of(node)
@@ -72,6 +73,10 @@ module EasyFlow
72
73
  @routing.present?
73
74
  end
74
75
 
76
+ def outputs_for(node)
77
+ outputs + named_outputs(node)
78
+ end
79
+
75
80
  def values_of(name, node)
76
81
  outputs.find { |output| output.name.to_s == name.to_s }&.values_for(node).to_a
77
82
  end
@@ -108,5 +113,13 @@ module EasyFlow
108
113
  chosen = flow_chosen_by(node)
109
114
  ChosenFlow.worked_out(chosen).name if chosen
110
115
  end
116
+
117
+ private
118
+
119
+ def named_outputs(node)
120
+ return [] unless @outputs_named_by
121
+
122
+ Array(node.config[@outputs_named_by.to_s]).filter_map { |entry| entry["name"].presence }.map { |name| Output.new(name: name) }
123
+ end
111
124
  end
112
125
  end
@@ -176,7 +176,18 @@ module EasyFlow
176
176
  def missing_settings
177
177
  @document.nodes.flat_map do |node|
178
178
  required_of(node).reject { |name| node.config[name.to_s].present? }
179
- .map { |name| Violation.new(node: node.id, problem: :missing_setting, detail: name.to_s) }
179
+ .map { |name| Violation.new(node: node.id, problem: :missing_setting, detail: name.to_s) } + missing_entry_settings(node)
180
+ end
181
+ end
182
+
183
+ def missing_entry_settings(node)
184
+ return [] unless @registry.registered?(node.type)
185
+
186
+ @registry.fetch(node.type).settings.record_required.flat_map do |list, required|
187
+ Array(node.config[list.to_s]).each_with_index.flat_map do |entry, index|
188
+ required.reject { |name| entry.to_h[name.to_s].present? }
189
+ .map { |name| Violation.new(node: node.id, problem: :missing_setting, detail: "#{list} #{index + 1} #{name}") }
190
+ end
180
191
  end
181
192
  end
182
193
 
@@ -0,0 +1,38 @@
1
+ module EasyFlow
2
+ class Wait
3
+ include Step
4
+
5
+ step_name "Wait"
6
+
7
+ setting :step, type: :previous_step, label: "Counting from", required: true
8
+ setting :minutes, type: :integer, label: "Minutes after it"
9
+ setting :time_of_day, type: :string, label: "Next time of day after it, as HH:MM"
10
+ setting :date, type: :string, label: "Date, as YYYY-MM-DD"
11
+
12
+ names_by { |node| Wait.named(node.config) }
13
+
14
+ waits_until { |node, _state, times| Wait.held_until(node, times)&.then { |held| held <= Time.current } || false }
15
+
16
+ def self.named(config)
17
+ return config["date"] if config["date"].present?
18
+
19
+ waited = config["time_of_day"].presence || ("#{config["minutes"]} minutes" if config["minutes"].present?)
20
+ "#{waited} after #{config["step"]}" if waited
21
+ end
22
+
23
+ def self.held_until(node, times)
24
+ from = times[node.config["step"]]
25
+ return Time.zone.parse(node.config["date"]) if node.config["date"].present?
26
+ return if from.nil?
27
+ return next_time_of_day(from, node.config["time_of_day"]) if node.config["time_of_day"].present?
28
+
29
+ from + node.config["minutes"].to_i.minutes
30
+ end
31
+
32
+ def self.next_time_of_day(from, time_of_day)
33
+ hour, minute = time_of_day.split(":").map(&:to_i)
34
+ same_day = from.in_time_zone.change(hour: hour, min: minute)
35
+ same_day > from ? same_day : same_day.tomorrow
36
+ end
37
+ end
38
+ end
@@ -13,6 +13,9 @@
13
13
 
14
14
  <% if @waiting %>
15
15
  <p class="ks-hint">Waiting for <%= @waiting %>.</p>
16
+ <% if carries_answers? %>
17
+ <div class="mt-4"><%= ui_button(label: "Check again", href: "#{flow_step_path(@guide.slug)}?#{{ answers: @answers, completed_at: @progress.completed_at.transform_values(&:iso8601) }.to_query}", variant: :secondary) %></div>
18
+ <% end %>
16
19
  <% else %>
17
20
  <%= form_with url: step_form[:url], method: step_form[:method], multipart: true, data: { turbo: false } do %>
18
21
  <%= hidden_field_tag :asked, @question.id %>
@@ -25,6 +28,9 @@
25
28
  <% else %>
26
29
  <%= hidden_field_tag "answers[#{key}]", value %>
27
30
  <% end %>
31
+ <% if (answered = @progress.completed_at[key]) %>
32
+ <%= hidden_field_tag "completed_at[#{key}]", answered.iso8601, id: nil %>
33
+ <% end %>
28
34
  <% end %>
29
35
  <% end %>
30
36
 
@@ -0,0 +1,23 @@
1
+ class KeepStepVisitsWholeInEasyFlowRuns < ActiveRecord::Migration[8.1]
2
+ class Run < ActiveRecord::Base
3
+ self.table_name = "easy_flow_runs"
4
+ end
5
+
6
+ def up
7
+ Run.find_each do |run|
8
+ run.update_columns(recorded: run.recorded.to_h.transform_values { |value| whole?(value) ? value : { "value" => value } })
9
+ end
10
+ end
11
+
12
+ def down
13
+ Run.find_each do |run|
14
+ run.update_columns(recorded: run.recorded.to_h.transform_values { |value| whole?(value) ? value["value"] : value })
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def whole?(value)
21
+ value.is_a?(Hash) && value.key?("value") && (value.keys - %w[value completed_at]).empty?
22
+ end
23
+ end
@@ -24,6 +24,10 @@ module EasyFlow
24
24
  EasyFlow::Condition.register
25
25
  EasyFlow::Switch.register
26
26
  EasyFlow::Compare.register
27
+ EasyFlow::CompareTwoValues.register
28
+ EasyFlow::AllOrAny.register
29
+ EasyFlow::Count.register
30
+ EasyFlow::Wait.register
27
31
  EasyFlow::FlowStep.register
28
32
 
29
33
  EasyFlow.check(:unrouted_value)
@@ -1,3 +1,3 @@
1
1
  module EasyFlow
2
- VERSION = "0.16.0"
2
+ VERSION = "0.18.0"
3
3
  end