easy_flow 0.17.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 (33) 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/digest.rb +19 -11
  14. data/app/models/easy_flow/progress/kept.rb +4 -0
  15. data/app/models/easy_flow/progress/loose.rb +12 -4
  16. data/app/models/easy_flow/progress.rb +2 -2
  17. data/app/models/easy_flow/run.rb +19 -6
  18. data/app/models/easy_flow/runner.rb +11 -11
  19. data/app/models/easy_flow/settings.rb +5 -2
  20. data/app/models/easy_flow/step.rb +6 -2
  21. data/app/models/easy_flow/step_type/declaration.rb +13 -3
  22. data/app/models/easy_flow/step_type.rb +18 -5
  23. data/app/models/easy_flow/validator.rb +12 -1
  24. data/app/models/easy_flow/wait.rb +38 -0
  25. data/app/views/easy_flow/flows/step.html.erb +6 -0
  26. data/db/migrate/20261011130000_keep_step_visits_whole_in_easy_flow_runs.rb +23 -0
  27. data/lib/easy_flow/engine.rb +3 -0
  28. data/lib/easy_flow/version.rb +1 -1
  29. data/the_local/agents/easy_flow-develop.md +100 -20
  30. data/the_local/agents/easy_flow-info.md +22 -10
  31. data/the_local/agents/easy_flow-install.md +18 -5
  32. data/the_local/interface.yml +5 -0
  33. metadata +6 -1
@@ -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,7 +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
27
29
  EasyFlow::Count.register
30
+ EasyFlow::Wait.register
28
31
  EasyFlow::FlowStep.register
29
32
 
30
33
  EasyFlow.check(:unrouted_value)
@@ -1,3 +1,3 @@
1
1
  module EasyFlow
2
- VERSION = "0.17.0"
2
+ VERSION = "0.18.0"
3
3
  end