diff --git a/lib/trailblazer/workflow.rb b/lib/trailblazer/workflow.rb index ebda1df..756bdc0 100644 --- a/lib/trailblazer/workflow.rb +++ b/lib/trailblazer/workflow.rb @@ -18,3 +18,4 @@ module Workflow require "trailblazer/workflow/test/plan" require "trailblazer/workflow/discovery" +require "trailblazer/workflow/discovery/present" diff --git a/lib/trailblazer/workflow/collaboration.rb b/lib/trailblazer/workflow/collaboration.rb index 011426a..13399d3 100644 --- a/lib/trailblazer/workflow/collaboration.rb +++ b/lib/trailblazer/workflow/collaboration.rb @@ -40,7 +40,7 @@ def to_a # DISCUSS: is lane_positions same as Configuration? class Positions def initialize(positions) - @positions = positions + @positions = positions.sort { |a, b| a.activity.object_id <=> b.activity.object_id } # TODO: allow other orders? @activity_to_task = positions.collect { |position| [position.activity, position.task] }.to_h freeze @@ -69,7 +69,7 @@ def collect(&block) .collect(&block) end - def ==(b) + def ==(b) # DISCUSS: needed? eql?(b) end @@ -78,7 +78,7 @@ def eql?(b) end def hash - @positions.collect { |position| position.hash }.sort.join("").to_i + @positions.flat_map { |position| position.to_a }.hash end end diff --git a/lib/trailblazer/workflow/discovery.rb b/lib/trailblazer/workflow/discovery.rb index 05e7101..a2d79f9 100644 --- a/lib/trailblazer/workflow/discovery.rb +++ b/lib/trailblazer/workflow/discovery.rb @@ -22,7 +22,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po original_lanes = collaboration.to_h[:lanes] # this dictates the order within created Positions. - collaboration, message_flow, start_position, initial_lane_positions, original_activity_2_stub_activity, original_task_2_stub_task = stub_tasks_for(collaboration, message_flow: message_flow, start_position: start_position, initial_lane_positions: initial_lane_positions) + collaboration, message_flow, start_position, initial_lane_positions, activity_2_stub, original_task_2_stub_task = stub_tasks_for(collaboration, message_flow: message_flow, start_position: start_position, initial_lane_positions: initial_lane_positions) # pp collaboration.to_h[:lanes][:ui].to_h # raise @@ -67,7 +67,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po discovered_state = discovered_state.merge( stubbed_positions_before: [lane_positions, start_position], - positions_before: [unstub_positions(original_activity_2_stub_activity, original_task_2_stub_task, lane_positions, lanes: original_lanes), *unstub_positions(original_activity_2_stub_activity, original_task_2_stub_task, [start_position], lanes: Hash.new(0))] + positions_before: [unstub_positions(activity_2_stub, original_task_2_stub_task, lane_positions, lanes: original_lanes), *unstub_positions(activity_2_stub, original_task_2_stub_task, [start_position], lanes: Hash.new(0))] ) discovered_state = discovered_state.merge(ctx_before: [ctx.inspect]) @@ -95,7 +95,7 @@ def call(collaboration, start_position:, run_multiple_times: {}, initial_lane_po suspend_configuration = configuration discovered_state = discovered_state.merge( stubbed_suspend_configuration: suspend_configuration, - suspend_configuration: unstub_configuration(original_activity_2_stub_activity, configuration, lanes: original_lanes) + suspend_configuration: unstub_configuration(activity_2_stub, configuration, lanes: original_lanes) ) # figure out possible next resumes/catchs: @@ -178,35 +178,35 @@ def stub_tasks_for(collaboration, ignore_class: Trailblazer::Activity::End, mess lane = Activity.new(Activity::Schema.new(new_circuit, activity.to_h[:outputs], new_nodes, activity.to_h[:config])) # FIXME: breaking taskWrap here (which is no problem, actually). - # [lane_id, lane, replaced_tasks] [[lane_id, lane], replaced_tasks] end + # DISCUSS: interestingly, we don't need this map as all stubbed tasks are user tasks, and positions always reference suspends or catch events, which arent' stubbed. original_task_2_stub_task = collected.inject({}) { |memo, (_, replaced_tasks)| memo.merge(replaced_tasks) } stubbed_lanes = collected.collect { |lane, _| lane }.to_h - original_activity_2_stub_activity = collaboration.to_h[:lanes].collect { |lane_id, activity| [activity, stubbed_lanes[lane_id]] }.to_h + activity_2_stub = collaboration.to_h[:lanes].collect { |lane_id, activity| [activity, stubbed_lanes[lane_id]] }.to_h - new_message_flow = message_flow.collect { |throw_evt, (activity, catch_evt)| [throw_evt, [original_activity_2_stub_activity[activity], catch_evt]] }.to_h + new_message_flow = message_flow.collect { |throw_evt, (activity, catch_evt)| [throw_evt, [activity_2_stub[activity], catch_evt]] }.to_h - new_start_position = Collaboration::Position.new(original_activity_2_stub_activity.fetch(start_position.activity), start_position.task) + new_start_position = Collaboration::Position.new(activity_2_stub.fetch(start_position.activity), start_position.task) new_initial_lane_positions = initial_lane_positions.collect do |position| # TODO: make lane_positions {Position} instances, too. - Collaboration::Position.new(original_activity_2_stub_activity[position[0]], position[1]) + Collaboration::Position.new(activity_2_stub[position[0]], position[1]) end new_initial_lane_positions = Collaboration::Positions.new(new_initial_lane_positions) - return Collaboration::Schema.new(lanes: stubbed_lanes, message_flow: new_message_flow), new_message_flow, new_start_position, new_initial_lane_positions, original_activity_2_stub_activity, original_task_2_stub_task + return Collaboration::Schema.new(lanes: stubbed_lanes, message_flow: new_message_flow), new_message_flow, new_start_position, new_initial_lane_positions, activity_2_stub, original_task_2_stub_task end # Get the original lane activity and tasks for a {Positions} set from the stubbed ones. - def unstub_positions(original_activity_2_stub_activity, original_task_2_stub_task, positions, lanes: {}) + def unstub_positions(activity_2_stub, original_task_2_stub_task, positions, lanes: {}) real_positions = positions.to_a.collect do |position| Collaboration::Position.new( - original_activity_2_stub_activity.invert.fetch(position.activity), + activity_2_stub.invert.fetch(position.activity), position.task # since the task will always be a suspend, a resume or terminus, we can safely use the stubbed one, which is identical to the original. ) end.sort { |a, b| lanes.values.index(a.activity) <=> lanes.values.index(b.activity) } @@ -214,11 +214,10 @@ def unstub_positions(original_activity_2_stub_activity, original_task_2_stub_tas Collaboration::Positions.new(real_positions) end - def unstub_configuration(original_activity_2_stub_activity, configuration, lanes:) + def unstub_configuration(activity_2_stub, configuration, lanes:) + real_lane_positions = unstub_positions(activity_2_stub, nil, configuration.lane_positions, lanes: lanes) - real_lane_positions = unstub_positions(original_activity_2_stub_activity, nil, configuration.lane_positions, lanes: lanes) - - real_last_lane = original_activity_2_stub_activity[configuration.last_lane] + real_last_lane = activity_2_stub[configuration.last_lane] Collaboration::Configuration.new( **configuration.to_h, diff --git a/lib/trailblazer/workflow/discovery/present.rb b/lib/trailblazer/workflow/discovery/present.rb new file mode 100644 index 0000000..3f4619a --- /dev/null +++ b/lib/trailblazer/workflow/discovery/present.rb @@ -0,0 +1,116 @@ +module Trailblazer + module Workflow + module Discovery + # Rendering-specific code using {Discovery:states}. + module Present + module_function + + # Find the next connected task, usually outgoing from a catch event. + def label_for_next_task(activity, catch_event) + task_after_catch = activity.to_h[:circuit].to_h[:map][catch_event][Trailblazer::Activity::Right] + + Trailblazer::Activity::Introspect.Nodes(activity, task: task_after_catch).data[:label] || task_after_catch + end + + + def readable_name_for_catch_event(activity, catch_event, lanes_cfg: {}) + envelope_icon = "(✉)➔" # TODO: implement {envelope_icon} flag. + envelope_icon = "▶" + + lane_options = lane_options_for(activity, catch_event, lanes_cfg: lanes_cfg) + lane_name = lane_options[:label] + lane_label = lane_options[:icon] #if lane_icons.key?(lane_name) # TODO: handle default! + + event_label = label_for_next_task(activity, catch_event) + + "#{lane_label} #{envelope_icon}#{event_label}" + end + + # Compute real catch events from the ID for a particular resume. + def resumes_from_suspend(activity, suspend) + suspend.to_h["resumes"].collect do |catch_event_id| + _catch_event = Trailblazer::Activity::Introspect.Nodes(activity, id: catch_event_id).task + end + end + + def lane_options_for(activity, task, lanes_cfg:) + lanes_cfg.values.find { |options| options[:activity] == activity } or raise + end + + # Each row represents a configuration of suspends aka "state". + # The state knows its possible resume events. + # does the state know which state fields belong to it? + # + # TODO: move that to separate module {StateTable.call}. + def render_cli_state_table(discovered_states, lanes_cfg:) + # raise discovery_state_table.inspect + start_position_to_catch = {} + + # Key by lane_positions, which represent a state. + # State (lane_positions) => [events (start position)] + states = {} + + # Collect the invoked start positions per Positions configuration. + # This implies the possible "catch events" per configuration. + discovered_states.each do |row| + positions_before, start_position = row[:positions_before] + + # raise positions_before.inspect + # puts positions_before.to_a.collect { |p| + # # puts "@@@@@ #{p.inspect}" + # next if p.task.to_h["resumes"].nil? + # resumes_from_suspend(*p).collect { |catch_event| readable_name_for_catch_event(p.activity, catch_event, lanes_cfg: lanes_cfg) } + + # }.inspect + + events = states[positions_before] + events = [] if events.nil? + + events << start_position + + states[positions_before] = events + end + + # render + cli_rows = states.flat_map do |configuration, catch_events| + suggested_state_name = suggested_state_name_for(catch_events) + + suggested_state_name = "⛊ #{suggested_state_name}" + .inspect + + + # triggerable_events = events + # .collect { |event_position| readable_name_for_catch_event(event_position, lanes_cfg: lanes_cfg).inspect } + # .uniq + # .join(", ") + + + Hash[ + "state name", + suggested_state_name, + + # "triggerable events", + # triggerable_events + ] + end + + Hirb::Helpers::Table.render(cli_rows, fields: [ + "state name", + "triggerable events", + # *lane_ids, + ], + max_width: 186, + ) # 186 for laptop 13" + end + + # TODO: move to StateTable + def suggested_state_name_for(catch_events) + catch_events + .collect { |event_position| label_for_next_task(*event_position.to_a) } + .uniq + .join("/") + end + end + end + end +end diff --git a/lib/trailblazer/workflow/state/discovery.rb b/lib/trailblazer/workflow/state/discovery.rb index f41ec33..0a7191a 100644 --- a/lib/trailblazer/workflow/state/discovery.rb +++ b/lib/trailblazer/workflow/state/discovery.rb @@ -75,73 +75,9 @@ def self.generate_state_table(discovery_states, lanes:) state_table end - # Each row represents a configuration of suspends aka "state". - # The state knows its possible resume events. - # does the state know which state fields belong to it? - def self.render_cli_state_table(discovery_state_table) - start_position_to_catch = {} - # Key by lane_positions, which represent a state. - # State (lane_positions) => [events (start position)] - states = {} - discovery_state_table.each do |row| - configuration = row[:lane_positions] - events = states[configuration] - events = [] if events.nil? - - events << row[:start_position] - - states[configuration] = events - end - - - # render - cli_rows = states.flat_map do |configuration, events| - suggested_state_name = events - .collect { |event| event[:comment][1] } - .uniq - .join("/") - - suggested_state_name = "> #{suggested_state_name}" - .inspect - - - triggerable_events = events - .collect { |event| readable_name_for_catch_event(event).inspect } - .uniq - .join(", ") - - - Hash[ - "state name", - suggested_state_name, - - "triggerable events", - triggerable_events - ] - end - - Hirb::Helpers::Table.render(cli_rows, fields: [ - "state name", - "triggerable events", - # *lane_ids, - ], - max_width: 186, - ) # 186 for laptop 13" - end - - def self.readable_name_for_catch_event(position, envelope_icon: false, lane_icons: {}) - envelope_icon = "(✉)➔" # TODO: implement {envelope_icon} flag. - envelope_icon = "▶" - - lane_name = position[:tuple][0] - lane_label = "#{lane_name}:" - lane_label = lane_icons[lane_name] if lane_icons.key?(lane_name) - - "#{lane_label} #{envelope_icon}#{position[:comment][1]}" - end def self.readable_name_for_resume_event(position, tuple: false, lane_icons: {}) resume_labels = position[:comment][1] @@ -226,12 +162,6 @@ def self.render_cli_event_table(discovery_state_table, render_ids: false, hide_l ) # 186 for laptop 13" end - # Find the next connected task, usually outgoing from a catch event. - def self.find_next_task_label(activity, catch_event) - task_after_catch = activity.to_h[:circuit].to_h[:map][catch_event][Trailblazer::Activity::Right] - Trailblazer::Activity::Introspect.Nodes(activity, task: task_after_catch).data[:label] || task_after_catch - end - def self.serialize_comment(event_name) ["before", event_name] end diff --git a/test/collaboration_test.rb b/test/collaboration_test.rb index c72c38a..358d5f8 100644 --- a/test/collaboration_test.rb +++ b/test/collaboration_test.rb @@ -91,46 +91,8 @@ def render_states(states, lanes:, additional_state_data:, task_map:) collaboration_state_table_interface.(schema, state_table, event: "ui_create_form", process_model_id: nil) end - it "low level {Collaboration.advance} API" do - ui_create_form = "Activity_0wc2mcq" # TODO: this is from pro-rails tests. - ui_create = "Activity_1psp91r" - ui_create_valid = "Event_0km79t5" - ui_create_invalid = "Event_0co8ygx" - ui_update_form = 'Activity_1165bw9' - ui_update = "Activity_0j78uzd" - ui_update_valid = "Event_1vf88fn" - ui_update_invalid = "Event_1nt0djb" - ui_notify_approver = "Activity_1dt5di5" - ui_accepted = "Event_1npw1tg" - ui_delete_form = "Activity_0ha7224" - ui_delete = "Activity_15nnysv" - ui_cancel = "Activity_1uhozy1" - ui_publish = "Activity_0bsjggk" - ui_archive = "Activity_0fy41qq" - ui_revise_form = "Activity_0zsock2" - ui_revise = "Activity_1wiumzv" - ui_revise_valid = "Event_1bz3ivj" - ui_revise_invalid = "Event_1wly6jj" - ui_revise_form_with_errors = "Activity_19m1lnz" - ui_create_form_with_errors = "Activity_08p0cun" - ui_update_form_with_errors = "Activity_00kfo8w" - ui_rejected = "Event_1vb197y" - - # FIXME: redundant with {lane_test}. - create_id = "Activity_0wwfenp" - update_id = "Activity_0q9p56e" - notify_id = "Activity_0wr78cv" - - - revise_id = "Activity_18qv6ob" - publish_id = "Activity_1bjelgv" - delete_id = "Activity_0cc4us9" - archive_id = "Activity_1hgscu3" - success_id = "Event_1p8873y" - - - schema, lanes, message_flow, initial_lane_positions = build_schema() - schema_hash = schema.to_h + it "{#render_cli_state_table}" do + schema, lanes, message_flow, initial_lane_positions, lanes_cfg = build_schema() lane_activity = lanes[:lifecycle] lane_activity_ui = lanes[:ui] @@ -138,52 +100,10 @@ def render_states(states, lanes:, additional_state_data:, task_map:) - - # TODO: do this in the State layer. - start_task = Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_create_form}").task # catch-before-Activity_0wc2mcq - start_position = Trailblazer::Workflow::Collaboration::Position.new(lane_activity_ui, start_task) - - - states, additional_state_data = Trailblazer::Workflow::Discovery.( - schema, - initial_lane_positions: initial_lane_positions, - start_position: start_position, - message_flow: message_flow, - - # TODO: allow translating the original "id" (?) to the stubbed. - run_multiple_times: { - # We're "clicking" the [Notify_approver] button again, this time to get rejected. - Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_notify_approver}").task => {ctx_merge: { - # decision: false, # TODO: this is how it should be. - :"approver:xxx" => Trailblazer::Activity::Left, # FIXME: {:decision} must be translated to {:"approver:xxx"} - }, config_payload: {outcome: :failure}}, - - # Click [UI Create] again, with invalid data. - Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_create}").task => {ctx_merge: { - # create: false - :"lifecycle:Create" => Trailblazer::Activity::Left, - }, config_payload: {outcome: :failure}}, # lifecycle create is supposed to fail. - - # Click [UI Update] again, with invalid data. - Trailblazer::Activity::Introspect.Nodes(lane_activity_ui, id: "catch-before-#{ui_update}").task => {ctx_merge: { - # update: false - :"lifecycle:Update" => Trailblazer::Activity::Left, - }, config_payload: {outcome: :failure}}, # lifecycle create is supposed to fail. - } - ) - - - - # render_states(states, lanes: ___lanes___ = {lane_activity => "lifecycle", lane_activity_ui => "UI", approver_activity => "approver"}, additional_state_data: additional_state_data, task_map: task_map) -# raise "figure out how to build a generated state table" - - ___lanes___ = {lane_activity => "lifecycle", lane_activity_ui => "UI", approver_activity => "approver"} - - # DISCUSS: technically, this is an event table, not a state table. - state_table = Trailblazer::Workflow::State::Discovery.generate_state_table(states, lanes: ___lanes___) + # state_table = Trailblazer::Workflow::State::Discovery.generate_state_table(states, lanes: lanes_cfg) - cli_state_table = Trailblazer::Workflow::State::Discovery.render_cli_state_table(state_table) + cli_state_table = Trailblazer::Workflow::State::Discovery.render_cli_state_table(states, lanes: lanes_cfg) puts cli_state_table assert_equal cli_state_table, %(+---------------------------------+--------------------------------------------+ diff --git a/test/discovery_test.rb b/test/discovery_test.rb index bd0f691..4e6db77 100644 --- a/test/discovery_test.rb +++ b/test/discovery_test.rb @@ -3,13 +3,14 @@ class DiscoveryTest < Minitest::Spec include BuildSchema - it "Discovery.call" do + def states ui_create_form = "Activity_0wc2mcq" # TODO: this is from pro-rails tests. ui_create = "Activity_1psp91r" ui_update = "Activity_0j78uzd" ui_notify_approver = "Activity_1dt5di5" - schema, lanes, message_flow, initial_lane_positions = build_schema() + # TODO: either {lanes} or {lanes_cfg}. + schema, lanes, message_flow, initial_lane_positions, lanes_cfg = build_schema() lane_activity = lanes[:lifecycle] lane_activity_ui = lanes[:ui] @@ -51,6 +52,12 @@ class DiscoveryTest < Minitest::Spec } ) + return states, lanes_sorted, lanes_cfg + end + + it "Discovery.call" do + states, lanes_sorted, lanes_cfg = self.states + # pp states # TODO: should we really assert the state table manually? assert_equal states.size, 15 @@ -206,9 +213,7 @@ def assert_position_before(actual_positions, expected_ids, start_id:, lanes:) def assert_positions_for(actual_lane_positions, expected_ids, lanes:) # puts actual_lane_positions.collect { |(a, t)| Trailblazer::Activity::Introspect.Nodes(a, task: t).id }.inspect - # FIXME: always use Positions -> Position actual_lane_positions.collect.with_index do |actual_position, index| - raise actual_position.inspect if actual_position.class == Array # FIXME: remove me. actual_activity, actual_task = actual_position.to_a expected_activity = lanes[index] @@ -223,4 +228,30 @@ def assert_position_after(actual_configuration, expected_ids, lanes:) assert_positions_for(actual_positions, expected_ids, lanes: lanes) end + + it "{#render_cli_state_table}" do + states, lanes_sorted, lanes_cfg = self.states() + + # DISCUSS: technically, this is an event table, not a state table. + # state_table = Trailblazer::Workflow::State::Discovery.generate_state_table(states, lanes: lanes_cfg) + + cli_state_table = Trailblazer::Workflow::Discovery::Present.render_cli_state_table(states, lanes_cfg: lanes_cfg) + puts cli_state_table + assert_equal cli_state_table, +%(+---------------------------------+--------------------------------------------+ +| state name | triggerable events | ++---------------------------------+--------------------------------------------+ +| "> Create form" | "UI: ▶Create form" | +| "> Create" | "UI: ▶Create" | +| "> Update form/Notify approver" | "UI: ▶Update form", "UI: ▶Notify approver" | +| "> Update" | "UI: ▶Update" | +| "> Delete? form/Publish" | "UI: ▶Delete? form", "UI: ▶Publish" | +| "> Revise form" | "UI: ▶Revise form" | +| "> Delete/Cancel" | "UI: ▶Delete", "UI: ▶Cancel" | +| "> Archive" | "UI: ▶Archive" | +| "> Revise" | "UI: ▶Revise" | ++---------------------------------+--------------------------------------------+ +9 rows in set) + + end end diff --git a/test/structures_test.rb b/test/structures_test.rb index 2f6d040..d65915b 100644 --- a/test/structures_test.rb +++ b/test/structures_test.rb @@ -23,7 +23,7 @@ class StructuresTest < Minitest::Spec #@ Positions#replace positions = positions.replace(activity_a, "suspend:c") # DISCUSS: check how the interal order is now different! - assert_equal positions.collect { |activity, task| [activity, task] }.inspect, %([[\"B\", \"suspend:b\"], [\"A\", \"suspend:c\"]]) + assert_equal positions.collect { |activity, task| [activity, task] }.inspect, %([[\"A\", \"suspend:c\"], [\"B\", \"suspend:b\"]]) end it "Positions#==" do diff --git a/test/test_helper.rb b/test/test_helper.rb index 1688c47..a773b1f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -101,14 +101,23 @@ def build_schema() approver_activity, extended_message_flow, extended_initial_lane_positions = build_custom_editor_lane(lanes, message_flow) + lanes_cfg = lanes_cfg.merge( + "approver" => { + label: "approver", + icon: "☑", + activity: approver_activity + } + ) + lanes = lanes.merge(approver: approver_activity) + # TODO: add {lanes_cfg}. schema = Trailblazer::Workflow::Collaboration::Schema.new( lanes: lanes, message_flow: message_flow, ) - return schema, lanes, extended_message_flow, extended_initial_lane_positions + return schema, lanes, extended_message_flow, extended_initial_lane_positions, lanes_cfg end # DISCUSS: this is mostly to play around with the "API" of building a Collaboration.