Add unique database index to checklist items position (#5886)

* Add unique database index to checklist items position and update errors to render flash message when a checklist item validation fails [SCI-8841]

* Improve protocol checklist item reordering [SCI-8841]

* Add act as list to gemfile and use sequential updates for reodering with act_as_list [SCI-8841]
This commit is contained in:
wandji 2023-08-02 14:37:01 +01:00 committed by GitHub
parent 382a4e57ac
commit 31f8bc19d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 51 additions and 40 deletions

View file

@ -54,6 +54,7 @@ gem 'js_cookie_rails' # Simple JS API for cookies
gem 'spinjs-rails' gem 'spinjs-rails'
gem 'activerecord-import' gem 'activerecord-import'
gem 'acts_as_list'
gem 'ajax-datatables-rails', '~> 0.3.1' gem 'ajax-datatables-rails', '~> 0.3.1'
gem 'aspector' # Aspect-oriented programming for Rails gem 'aspector' # Aspect-oriented programming for Rails
gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces AR gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces AR

View file

@ -132,6 +132,8 @@ GEM
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
acts_as_list (1.1.0)
activerecord (>= 4.2)
addressable (2.8.4) addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
@ -707,6 +709,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10.7) active_model_serializers (~> 0.10.7)
activerecord-import activerecord-import
acts_as_list
ajax-datatables-rails (~> 0.3.1) ajax-datatables-rails (~> 0.3.1)
aspector aspector
auto_strip_attributes (~> 2.1) auto_strip_attributes (~> 2.1)

View file

@ -27,15 +27,13 @@ module StepElements
render json: checklist_item, serializer: ChecklistItemSerializer, user: current_user render json: checklist_item, serializer: ChecklistItemSerializer, user: current_user
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render json: checklist_item, serializer: ChecklistItemSerializer, render json: { errors: checklist_item.errors }, status: :unprocessable_entity
user: current_user,
status: :unprocessable_entity
end end
def update def update
old_text = @checklist_item.text old_text = @checklist_item.text
@checklist_item.assign_attributes( @checklist_item.assign_attributes(
checklist_item_params.merge(last_modified_by: current_user) checklist_item_params.except(:position, :id).merge(last_modified_by: current_user)
) )
if @checklist_item.save! if @checklist_item.save!
@ -49,9 +47,7 @@ module StepElements
render json: @checklist_item, serializer: ChecklistItemSerializer, user: current_user render json: @checklist_item, serializer: ChecklistItemSerializer, user: current_user
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render json: @checklist_item, serializer: ChecklistItemSerializer, render json: { errors: @checklist_item.errors }, status: :unprocessable_entity
user: current_user,
status: :unprocessable_entity
end end
def toggle def toggle
@ -97,14 +93,14 @@ module StepElements
end end
def reorder def reorder
@checklist.with_lock do checklist_item = @checklist.checklist_items.find(checklist_item_params[:id])
params[:checklist_item_positions].each do |id, position| ActiveRecord::Base.transaction do
@checklist.checklist_items.find(id).update_column(:position, position) checklist_item.insert_at(checklist_item_params[:position])
end
@checklist.touch @checklist.touch
end end
render json: params[:checklist_item_positions], status: :ok render json: params[:checklist_item_positions], status: :ok
rescue ActiveRecord::RecordInvalid
render json: { errors: checklist_item.errors }, status: :conflict
end end
private private
@ -122,7 +118,7 @@ module StepElements
end end
def checklist_item_params def checklist_item_params
params.require(:attributes).permit(:text, :position) params.require(:attributes).permit(:text, :position, :id)
end end
def checklist_toggle_item_params def checklist_toggle_item_params

View file

@ -172,8 +172,8 @@
); );
if(callback) callback(); if(callback) callback();
}).error(() => { }).error((xhr) => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger'); this.setFlashErrors(xhr.responseJSON.errors)
}); });
this.update(); this.update();
@ -190,7 +190,7 @@
updatedItem.attributes.id = item.attributes.id updatedItem.attributes.id = item.attributes.id
this.$set(this.checklistItems, item.attributes.position, updatedItem) this.$set(this.checklistItems, item.attributes.position, updatedItem)
}, },
error: () => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger') error: (xhr) => setFlashErrors(xhr.responseJSON.errors)
}); });
} else { } else {
// create item, then append next one // create item, then append next one
@ -231,26 +231,26 @@
startReorder() { startReorder() {
this.reordering = true; this.reordering = true;
}, },
endReorder() { endReorder(event) {
this.reordering = false; this.reordering = false;
this.saveItemOrder(); if(
Number.isInteger(event.newIndex)
&& Number.isInteger(event.newIndex)
&& event.newIndex !== event.oldIndex
){
const { id, position } = this.orderedChecklistItems[event.newIndex]?.attributes
this.saveItemOrder(id, position);
}
}, },
saveItemOrder() { saveItemOrder(id, position) {
let checklistItemPositions =
{
checklist_item_positions: this.orderedChecklistItems.map(
(i) => [i.attributes.id, i.attributes.position]
)
};
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: this.element.attributes.orderable.urls.reorder_url, url: this.element.attributes.orderable.urls.reorder_url,
data: JSON.stringify(checklistItemPositions), data: JSON.stringify({ attributes: { id, position } }),
contentType: "application/json", contentType: "application/json",
dataType: "json", dataType: "json",
error: (() => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger')), error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors),
success: (() => this.update()) success: () => this.update()
}); });
}, },
handleMultilinePaste(data) { handleMultilinePaste(data) {
@ -274,6 +274,14 @@
}; };
synchronousPost(0); synchronousPost(0);
},
setFlashErrors(errors) {
for(const key in errors){
HelperModule.flashAlertMsg(
this.i18n.t(`activerecord.errors.models.checklist_item.attributes.${key}`),
'danger'
)
}
} }
} }
} }

View file

@ -5,10 +5,11 @@ class ChecklistItem < ApplicationRecord
length: { maximum: Constants::TEXT_MAX_LENGTH } length: { maximum: Constants::TEXT_MAX_LENGTH }
validates :checklist, presence: true validates :checklist, presence: true
validates :checked, inclusion: { in: [true, false] } validates :checked, inclusion: { in: [true, false] }
validates :position, uniqueness: { scope: :checklist }, unless: -> { position.nil? } validates :position, uniqueness: { scope: :checklist }
belongs_to :checklist, belongs_to :checklist,
inverse_of: :checklist_items inverse_of: :checklist_items
acts_as_list scope: :checklist, top_of_list: 0, sequential_updates: true
belongs_to :created_by, belongs_to :created_by,
foreign_key: 'created_by_id', foreign_key: 'created_by_id',
class_name: 'User', class_name: 'User',
@ -18,8 +19,6 @@ class ChecklistItem < ApplicationRecord
class_name: 'User', class_name: 'User',
optional: true optional: true
after_destroy :update_positions
# conditional touch excluding checked updates # conditional touch excluding checked updates
after_destroy :touch_checklist after_destroy :touch_checklist
after_save :touch_checklist after_save :touch_checklist
@ -35,12 +34,4 @@ class ChecklistItem < ApplicationRecord
checklist.touch checklist.touch
# rubocop:enable Rails/SkipsModelValidations # rubocop:enable Rails/SkipsModelValidations
end end
def update_positions
transaction do
checklist.checklist_items.order(position: :asc).each_with_index do |checklist_item, i|
checklist_item.update!(position: i)
end
end
end
end end

View file

@ -232,6 +232,10 @@ en:
attributes: attributes:
output_id: output_id:
creates_cycle: "mustn't create cycle" creates_cycle: "mustn't create cycle"
checklist_item:
attributes:
text: Text is too long
position: "Position has already been taken by another item in the checklist"
errors: errors:
general: "Something went wrong." general: "Something went wrong."
general_text_too_long: 'Text is too long' general_text_too_long: 'Text is too long'

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class
AddIndexToChecklistItems < ActiveRecord::Migration[7.0]
def change
add_index :checklist_items, %i(checklist_id position), unique: true
end
end