From 344876c4da75efde6527b158199b07c5ac5084fe Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 15:18:00 +0100 Subject: [PATCH 01/12] track activity when the state is changed --- app/controllers/concerns/activity_tracking.rb | 4 ++++ app/controllers/qa/issues_controller.rb | 6 +++++- app/models/activity.rb | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/activity_tracking.rb b/app/controllers/concerns/activity_tracking.rb index 3029440f0..4261d882d 100644 --- a/app/controllers/concerns/activity_tracking.rb +++ b/app/controllers/concerns/activity_tracking.rb @@ -30,4 +30,8 @@ def track_recovered(trackable, user: current_user, project: current_project) def track_updated(trackable, user: current_user, project: current_project) track_activity(trackable, :update, user, project) end + + def track_updated_state(trackable, user: current_user, project: current_project) + track_activity(trackable, :update_state, user, project) + end end diff --git a/app/controllers/qa/issues_controller.rb b/app/controllers/qa/issues_controller.rb index 4eadd6f4d..acfc2c6cc 100644 --- a/app/controllers/qa/issues_controller.rb +++ b/app/controllers/qa/issues_controller.rb @@ -1,4 +1,5 @@ class QA::IssuesController < AuthenticatedController + include ActivityTracking include LiquidEnabledResource include ProjectScoped @@ -20,10 +21,13 @@ def edit end def update - @issues = current_project.issues.ready_for_review.where(id: params[:ids]) + @issues = current_project.issues.where(id: params[:ids]) respond_to do |format| if @issues.update_all(state: @state, updated_at: Time.now) + @issues.each do |issue| + track_updated_state(issue) + end format.html { redirect_to project_qa_issues_path(current_project), notice: 'State updated successfully.' } format.json { head :ok } else diff --git a/app/models/activity.rb b/app/models/activity.rb index c0965d91f..a2d689210 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -17,14 +17,14 @@ def project=(new_project); end validates_presence_of :action, :trackable_id, :trackable_type, :user - VALID_ACTIONS = %w[create update destroy recover] + VALID_ACTIONS = %w[create destroy recover update update_state] validates_inclusion_of :action, in: VALID_ACTIONS # -- Scopes --------------------------------------------------------------- scope :latest, -> do - includes(:trackable).order("activities.created_at DESC").limit(20) + includes(:trackable).order('activities.created_at DESC').limit(20) end # -- Callbacks ------------------------------------------------------------ @@ -45,7 +45,7 @@ def project=(new_project); end # FIXME - ISSUE/NOTE INHERITANCE def trackable=(new_trackable) super - self.trackable_type = "Issue" if new_trackable.is_a?(Issue) + self.trackable_type = 'Issue' if new_trackable.is_a?(Issue) new_trackable end From 10b111781718d68e093db163dc0930c2fd188b10 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 15:18:24 +0100 Subject: [PATCH 02/12] show state change activities in the activity feed --- app/presenters/activity_presenter.rb | 7 +++++-- app/views/issues/activities/_issue.html.erb | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/presenters/activity_presenter.rb b/app/presenters/activity_presenter.rb index 6c38cbd00..4cdc33a93 100644 --- a/app/presenters/activity_presenter.rb +++ b/app/presenters/activity_presenter.rb @@ -79,8 +79,11 @@ def render_title # but this may change if we add activities whose action is an irregular # verb. def verb - if activity.action == 'destroy' + case activity.action + when 'destroy' 'deleted' + when 'update_state' + 'updated' else activity.action.sub(/e?\z/, 'ed') end @@ -111,7 +114,7 @@ def partial_paths end def render_partial - locals = {activity: activity, presenter: self} + locals = { activity: activity, presenter: self } locals[trackable_name] = activity.trackable render partial_path, locals end diff --git a/app/views/issues/activities/_issue.html.erb b/app/views/issues/activities/_issue.html.erb index 00c1694e4..b09602df5 100644 --- a/app/views/issues/activities/_issue.html.erb +++ b/app/views/issues/activities/_issue.html.erb @@ -1,6 +1,6 @@ <% if issue %> <% if issue.title? %> - <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> issue. + <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> issue<%= " to #{issue.state.humanize.downcase}" if activity.action == 'update_state' %>. <% else %> <%= presenter.verb %> <%= link_to "Issue ##{issue.id}", [current_project, issue] %>. <% end %> From 7d697f8f910b3450a51682c3b8c595a829ee6857 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 15:27:41 +0100 Subject: [PATCH 03/12] update copy --- app/views/issues/activities/_issue.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/activities/_issue.html.erb b/app/views/issues/activities/_issue.html.erb index b09602df5..b3080afc0 100644 --- a/app/views/issues/activities/_issue.html.erb +++ b/app/views/issues/activities/_issue.html.erb @@ -1,6 +1,6 @@ <% if issue %> <% if issue.title? %> - <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> issue<%= " to #{issue.state.humanize.downcase}" if activity.action == 'update_state' %>. + <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> <%= activity.action == 'update_state' ? "issue's state to #{issue.state.humanize.downcase}" : 'issue' %>. <% else %> <%= presenter.verb %> <%= link_to "Issue ##{issue.id}", [current_project, issue] %>. <% end %> From f23d5ecccb642b409dc3ae165c08b0766a43268e Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 17:47:32 +0100 Subject: [PATCH 04/12] initial spec updates --- spec/support/qa_shared_examples.rb | 68 ++++++++++++++++++------------ 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index 6dd3fb397..b34bb66aa 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -1,6 +1,9 @@ shared_examples 'qa pages' do |item_type| describe 'index page', js: true do + MODEL = item_type.to_s.classify.constantize + STATES = ['Draft', 'Published'] + before do visit polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) end @@ -48,45 +51,56 @@ end it 'updates the list of records with the state' do - within '.dataTables_wrapper' do - @original_row_count = page.all('tbody tr').count - page.find('td.select-checkbox', match: :first).click - - click_button('State') - click_link('Published') + STATES.each do |state| + record = MODEL.where(state: 'ready_for_review').first + visit polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) + + within '.dataTables_wrapper' do + @original_row_count = page.all('tbody tr').count + page.find('td.select-checkbox', match: :first).click + + click_button('State') + expect { click_link state }.to have_enqueued_job(ActivityTrackingJob).with( + action: 'update_state', + project_id: current_project.id, + trackable_id: record.id, + trackable_type: record.class.to_s, + user_id: @logged_in_as.id + ) + + expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) + expect(page.all('tbody tr').count).to eq(@original_row_count - 1) + expect(page).to have_selector('.alert-success', text: 'State updated successfully.') + expect(record.reload.state).to eq state.downcase.gsub(' ', '_') + end end - - page.find('.alert') - - expect(page.all('tbody tr').count).to eq(@original_row_count - 1) - expect(page).to have_selector('.alert-success', text: 'State updated successfully.') end end end describe 'show page' do - before do - visit polymorphic_path([current_project, :qa, records.first]) - end - it 'shows the record\'s content' do + visit polymorphic_path([current_project, :qa, records.first]) expect(page).to have_content(records.first.title) end - it 'updates the state to draft' do - click_button 'Draft' + it 'updates the state' do + STATES.each do |state| + record = MODEL.where(state: 'ready_for_review').first + visit polymorphic_path([current_project, :qa, record]) - expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) - expect(page).to have_selector('.alert-success', text: 'State updated successfully.') - expect(records.first.reload.draft?).to eq true - end + expect { click_button state }.to have_enqueued_job(ActivityTrackingJob).with( + action: 'update_state', + project_id: current_project.id, + trackable_id: record.id, + trackable_type: record.class.to_s, + user_id: @logged_in_as.id + ) - it 'updates the state to published' do - click_button 'Published' - - expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) - expect(page).to have_selector('.alert-success', text: 'State updated successfully.') - expect(records.first.reload.published?).to eq true + expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) + expect(page).to have_selector('.alert-success', text: 'State updated successfully.') + expect(record.reload.state).to eq state.downcase.gsub(' ', '_') + end end end From ea3d3e07a1576d487ce0a8547a8df37b71570ab2 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 18:05:07 +0100 Subject: [PATCH 05/12] fix failing spec --- spec/support/qa_shared_examples.rb | 38 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index b34bb66aa..02feacd03 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -55,24 +55,26 @@ record = MODEL.where(state: 'ready_for_review').first visit polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) - within '.dataTables_wrapper' do - @original_row_count = page.all('tbody tr').count - page.find('td.select-checkbox', match: :first).click - - click_button('State') - expect { click_link state }.to have_enqueued_job(ActivityTrackingJob).with( - action: 'update_state', - project_id: current_project.id, - trackable_id: record.id, - trackable_type: record.class.to_s, - user_id: @logged_in_as.id - ) - - expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) - expect(page.all('tbody tr').count).to eq(@original_row_count - 1) - expect(page).to have_selector('.alert-success', text: 'State updated successfully.') - expect(record.reload.state).to eq state.downcase.gsub(' ', '_') - end + @original_row_count = page.all('tbody tr').count + page.find('td.select-checkbox', match: :first).click + + click_button('State') + expect do + click_link state + # Wait for action to complete + page.find('.alert') + end.to have_enqueued_job(ActivityTrackingJob).with( + action: 'update_state', + project_id: current_project.id, + trackable_id: record.id, + trackable_type: record.class.to_s, + user_id: @logged_in_as.id + ) + + expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) + expect(page.all('tbody tr').count).to eq(@original_row_count - 1) + expect(page).to have_selector('.alert-success', text: 'State updated successfully.') + expect(record.reload.state).to eq state.downcase.gsub(' ', '_') end end end From c5b6cc5b598e5be21a101040b204450d65ec68a2 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Fri, 24 Mar 2023 18:12:51 +0100 Subject: [PATCH 06/12] rubocop --- app/presenters/activity_presenter.rb | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/app/presenters/activity_presenter.rb b/app/presenters/activity_presenter.rb index 4cdc33a93..2e1130aae 100644 --- a/app/presenters/activity_presenter.rb +++ b/app/presenters/activity_presenter.rb @@ -41,22 +41,23 @@ def created_at_ago def icon icon_css = %w{activity-icon fa} - icon_css << case activity.trackable_type - when 'Board', 'List', 'Card' - 'fa-trello' - when 'Comment' - 'fa-comment' - when 'Evidence' - 'fa-flag' - when 'Issue' - 'fa-bug' - when 'Node' - 'fa-folder-o' - when 'Note' - 'fa-file-text-o' - else - '' - end + icon_css << + case activity.trackable_type + when 'Board', 'List', 'Card' + 'fa-trello' + when 'Comment' + 'fa-comment' + when 'Evidence' + 'fa-flag' + when 'Issue' + 'fa-bug' + when 'Node' + 'fa-folder-o' + when 'Note' + 'fa-file-text-o' + else + '' + end h.content_tag :span, nil, class: icon_css end @@ -128,9 +129,9 @@ def trackable_name def trackable_title @title ||= if activity.trackable.respond_to?(:title) && activity.trackable.title? - activity.trackable.title - elsif activity.trackable.respond_to?(:label) && activity.trackable.label? - activity.trackable.label - end + activity.trackable.title + elsif activity.trackable.respond_to?(:label) && activity.trackable.label? + activity.trackable.label + end end end From ba80ecc3a9167582c3d75004f476079135ee7332 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Tue, 28 Mar 2023 14:32:38 +0200 Subject: [PATCH 07/12] use `let` vs defining constants --- spec/support/qa_shared_examples.rb | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index 02feacd03..5a010b2f1 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -1,9 +1,8 @@ shared_examples 'qa pages' do |item_type| + let(:model) { item_type.to_s.classify.constantize } + let(:states) { ['Draft', 'Published'] } describe 'index page', js: true do - MODEL = item_type.to_s.classify.constantize - STATES = ['Draft', 'Published'] - before do visit polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) end @@ -51,8 +50,8 @@ end it 'updates the list of records with the state' do - STATES.each do |state| - record = MODEL.where(state: 'ready_for_review').first + states.each do |state| + record = model.where(state: 'ready_for_review').first visit polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) @original_row_count = page.all('tbody tr').count @@ -87,8 +86,8 @@ end it 'updates the state' do - STATES.each do |state| - record = MODEL.where(state: 'ready_for_review').first + states.each do |state| + record = model.where(state: 'ready_for_review').first visit polymorphic_path([current_project, :qa, record]) expect { click_button state }.to have_enqueued_job(ActivityTrackingJob).with( From ab8de9fe10a5eb3c5835c6a5b0076e4b3295594c Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Tue, 28 Mar 2023 14:54:30 +0200 Subject: [PATCH 08/12] make the specs a bit more DRY --- spec/support/qa_shared_examples.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index 5a010b2f1..4d763019e 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -1,4 +1,14 @@ shared_examples 'qa pages' do |item_type| + def job_params(record) + { + action: 'update_state', + project_id: current_project.id, + trackable_id: record.id, + trackable_type: record.class.to_s, + user_id: @logged_in_as.id + } + end + let(:model) { item_type.to_s.classify.constantize } let(:states) { ['Draft', 'Published'] } @@ -62,13 +72,7 @@ click_link state # Wait for action to complete page.find('.alert') - end.to have_enqueued_job(ActivityTrackingJob).with( - action: 'update_state', - project_id: current_project.id, - trackable_id: record.id, - trackable_type: record.class.to_s, - user_id: @logged_in_as.id - ) + end.to have_enqueued_job(ActivityTrackingJob).with(job_params(record)) expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) expect(page.all('tbody tr').count).to eq(@original_row_count - 1) @@ -90,13 +94,7 @@ record = model.where(state: 'ready_for_review').first visit polymorphic_path([current_project, :qa, record]) - expect { click_button state }.to have_enqueued_job(ActivityTrackingJob).with( - action: 'update_state', - project_id: current_project.id, - trackable_id: record.id, - trackable_type: record.class.to_s, - user_id: @logged_in_as.id - ) + expect { click_button state }.to have_enqueued_job(ActivityTrackingJob).with(job_params(record)) expect(current_path).to eq polymorphic_path([current_project, :qa, item_type.to_s.pluralize.to_sym]) expect(page).to have_selector('.alert-success', text: 'State updated successfully.') From 2eb43bb254d4312e485664688e66b33903810024 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Thu, 30 Mar 2023 15:48:10 +0200 Subject: [PATCH 09/12] formatting --- app/presenters/activity_presenter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/presenters/activity_presenter.rb b/app/presenters/activity_presenter.rb index 2e1130aae..28d60cdfc 100644 --- a/app/presenters/activity_presenter.rb +++ b/app/presenters/activity_presenter.rb @@ -128,7 +128,8 @@ def trackable_name end def trackable_title - @title ||= if activity.trackable.respond_to?(:title) && activity.trackable.title? + @title ||= + if activity.trackable.respond_to?(:title) && activity.trackable.title? activity.trackable.title elsif activity.trackable.respond_to?(:label) && activity.trackable.label? activity.trackable.label From a35c81004225d9be54dbd0ba8b4f39b6c1ed14d2 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Thu, 30 Mar 2023 15:49:34 +0200 Subject: [PATCH 10/12] move method to bottom of file for consistency --- spec/support/qa_shared_examples.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index 4d763019e..32cd00767 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -1,14 +1,4 @@ shared_examples 'qa pages' do |item_type| - def job_params(record) - { - action: 'update_state', - project_id: current_project.id, - trackable_id: record.id, - trackable_type: record.class.to_s, - user_id: @logged_in_as.id - } - end - let(:model) { item_type.to_s.classify.constantize } let(:states) { ['Draft', 'Published'] } @@ -129,4 +119,14 @@ def job_params(record) expect(current_path).to eq polymorphic_path([current_project, :qa, records.first]) end end + + def job_params(record) + { + action: 'update_state', + project_id: current_project.id, + trackable_id: record.id, + trackable_type: record.class.to_s, + user_id: @logged_in_as.id + } + end end From 9c64e1f3a01ab73c24883de09e45095b50a4bc4a Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Thu, 27 Apr 2023 16:43:14 +0200 Subject: [PATCH 11/12] `track_updated_state` -> `track_state_change` --- app/controllers/concerns/activity_tracking.rb | 4 ++-- app/controllers/qa/issues_controller.rb | 3 ++- app/models/activity.rb | 2 +- app/presenters/activity_presenter.rb | 2 +- app/views/issues/activities/_issue.html.erb | 2 +- spec/support/qa_shared_examples.rb | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/controllers/concerns/activity_tracking.rb b/app/controllers/concerns/activity_tracking.rb index 4261d882d..06b8dfa6c 100644 --- a/app/controllers/concerns/activity_tracking.rb +++ b/app/controllers/concerns/activity_tracking.rb @@ -31,7 +31,7 @@ def track_updated(trackable, user: current_user, project: current_project) track_activity(trackable, :update, user, project) end - def track_updated_state(trackable, user: current_user, project: current_project) - track_activity(trackable, :update_state, user, project) + def track_state_change(trackable, user: current_user, project: current_project) + track_activity(trackable, :state_change, user, project) end end diff --git a/app/controllers/qa/issues_controller.rb b/app/controllers/qa/issues_controller.rb index 8065d91cf..3f27e847f 100644 --- a/app/controllers/qa/issues_controller.rb +++ b/app/controllers/qa/issues_controller.rb @@ -22,6 +22,7 @@ def edit def update if @issue.update(state: @state, updated_at: Time.now) + track_state_change(@issue) redirect_to project_qa_issues_path(current_project), notice: 'State updated successfully.' else render :show, alert: @issue.errors.full_messages.join('; ') @@ -35,7 +36,7 @@ def multiple_update if @issues.update_all(state: @state, updated_at: Time.now) @issues.each do |issue| - track_updated_state(issue) + track_state_change(issue) end format.html { redirect_to_target_or_default project_qa_issues_path(current_project), notice: 'State updated successfully.' } diff --git a/app/models/activity.rb b/app/models/activity.rb index a2d689210..cbd1e52fa 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -17,7 +17,7 @@ def project=(new_project); end validates_presence_of :action, :trackable_id, :trackable_type, :user - VALID_ACTIONS = %w[create destroy recover update update_state] + VALID_ACTIONS = %w[create destroy recover state_change update] validates_inclusion_of :action, in: VALID_ACTIONS diff --git a/app/presenters/activity_presenter.rb b/app/presenters/activity_presenter.rb index 28d60cdfc..da5e020a2 100644 --- a/app/presenters/activity_presenter.rb +++ b/app/presenters/activity_presenter.rb @@ -83,7 +83,7 @@ def verb case activity.action when 'destroy' 'deleted' - when 'update_state' + when 'state_change' 'updated' else activity.action.sub(/e?\z/, 'ed') diff --git a/app/views/issues/activities/_issue.html.erb b/app/views/issues/activities/_issue.html.erb index b3080afc0..d42e48bb2 100644 --- a/app/views/issues/activities/_issue.html.erb +++ b/app/views/issues/activities/_issue.html.erb @@ -1,6 +1,6 @@ <% if issue %> <% if issue.title? %> - <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> <%= activity.action == 'update_state' ? "issue's state to #{issue.state.humanize.downcase}" : 'issue' %>. + <%= presenter.verb %> the <%= link_to issue.title, [current_project, issue] %> <%= activity.action == 'state_change' ? "issue's state to #{issue.state.humanize.downcase}" : 'issue' %>. <% else %> <%= presenter.verb %> <%= link_to "Issue ##{issue.id}", [current_project, issue] %>. <% end %> diff --git a/spec/support/qa_shared_examples.rb b/spec/support/qa_shared_examples.rb index 32cd00767..5eac40437 100644 --- a/spec/support/qa_shared_examples.rb +++ b/spec/support/qa_shared_examples.rb @@ -122,7 +122,7 @@ def job_params(record) { - action: 'update_state', + action: 'state_change', project_id: current_project.id, trackable_id: record.id, trackable_type: record.class.to_s, From c8282eb61810be312990417f1a4d9f7e965f5c56 Mon Sep 17 00:00:00 2001 From: Matt Budz Date: Wed, 10 May 2023 11:16:39 +0200 Subject: [PATCH 12/12] add changelog entry --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 35430edfe..8f32597d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,5 @@ [v#.#.#] ([month] [YYYY]) - - [entity]: - - [future tense verb] [feature] + - QA: Show state changes in activity feed - Upgraded gems: - [gem] - Bugs fixes: