From 77948572f1b912d6c239f0a64e97e79b33129dc0 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 21 Sep 2023 19:24:58 +0200 Subject: [PATCH 1/3] Refactor checklist [SCI-9340] --- .../checklist_items_controller.rb | 65 +++++---- app/javascript/vue/protocol/container.vue | 4 +- .../vue/shared/content/checklist.vue | 128 +++++++----------- .../vue/shared/content/checklistItem.vue | 42 +++--- app/javascript/vue/shared/inline_edit.vue | 49 ++----- app/models/checklist_item.rb | 43 ++++++ app/serializers/checklist_item_serializer.rb | 2 +- app/serializers/checklist_serializer.rb | 7 +- config/routes.rb | 2 +- ...0_add_with_paragraphs_to_checklist_item.rb | 9 ++ db/schema.rb | 3 +- 11 files changed, 185 insertions(+), 169 deletions(-) create mode 100644 db/migrate/20230921142200_add_with_paragraphs_to_checklist_item.rb diff --git a/app/controllers/step_elements/checklist_items_controller.rb b/app/controllers/step_elements/checklist_items_controller.rb index 1320efbbd..e9872e1dd 100644 --- a/app/controllers/step_elements/checklist_items_controller.rb +++ b/app/controllers/step_elements/checklist_items_controller.rb @@ -10,22 +10,28 @@ module StepElements before_action :check_toggle_permissions, only: %i(toggle) before_action :check_manage_permissions, only: %i(create update destroy) - def create - checklist_item = @checklist.checklist_items.build(checklist_item_params.merge!(created_by: current_user)) + def index + render json: @checklist.checklist_items, each_serializer: ChecklistItemSerializer, user: current_user + end + def create + checklist_item = @checklist.checklist_items.new(checklist_item_params.merge!(created_by: current_user)) + new_items = [] ActiveRecord::Base.transaction do - checklist_item.save! - log_activity( - "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added", - { - checklist_item: checklist_item.text, - checklist_name: @checklist.name - } - ) - checklist_item_annotation(@step, checklist_item) + new_items = checklist_item.save_multiline! + new_items.each do |item| + log_activity( + "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added", + { + checklist_item: item.text, + checklist_name: @checklist.name + } + ) + checklist_item_annotation(@step, item) + end end - render json: checklist_item, serializer: ChecklistItemSerializer, user: current_user + render json: new_items, each_serializer: ChecklistItemSerializer, user: current_user rescue ActiveRecord::RecordInvalid render json: { errors: checklist_item.errors }, status: :unprocessable_entity end @@ -35,17 +41,31 @@ module StepElements @checklist_item.assign_attributes( checklist_item_params.except(:position, :id).merge(last_modified_by: current_user) ) - - if @checklist_item.save! - log_activity( - "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_edited", - checklist_item: @checklist_item.text, - checklist_name: @checklist.name - ) - checklist_item_annotation(@step, @checklist_item, old_text) + new_items = [] + ActiveRecord::Base.transaction do + new_items = @checklist_item.save_multiline! + new_items.each_with_index do |item, i| + if i.zero? + log_activity( + "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_edited", + checklist_item: item.text, + checklist_name: @checklist.name + ) + checklist_item_annotation(@step, item, old_text) + else + log_activity( + "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added", + { + checklist_item: item.text, + checklist_name: @checklist.name + } + ) + checklist_item_annotation(@step, item) + end + end end - render json: @checklist_item, serializer: ChecklistItemSerializer, user: current_user + render json: new_items, each_serializer: ChecklistItemSerializer, user: current_user rescue ActiveRecord::RecordInvalid render json: { errors: @checklist_item.errors }, status: :unprocessable_entity end @@ -96,7 +116,6 @@ module StepElements checklist_item = @checklist.checklist_items.find(checklist_item_params[:id]) ActiveRecord::Base.transaction do checklist_item.insert_at(checklist_item_params[:position]) - @checklist.touch end render json: params[:checklist_item_positions], status: :ok rescue ActiveRecord::RecordInvalid @@ -118,7 +137,7 @@ module StepElements end def checklist_item_params - params.require(:attributes).permit(:text, :position, :id) + params.require(:attributes).permit(:text, :position, :id, :with_paragraphs) end def checklist_toggle_item_params diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index cceb86569..0fa83b9ee 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -431,7 +431,7 @@ secondaryNavigation.style.zIndex= 251; } else { secondaryNavigation.style.boxShadow = 'none'; - secondaryNavigation.style.zIndex= 0; + if (secondaryNavigationTop > 10) secondaryNavigation.style.zIndex= 0; } if (protocolHeaderTop - 5 < protocolHeaderHeight) { // When secondary navigation touch protocol header @@ -457,7 +457,7 @@ secondaryNavigation.style.top = newSecondaryTop + 'px'; // Secondary navigation starts slowly disappear protocolHeader.style.top = newSecondaryTop + secondaryNavigationHeight - 1 + 'px'; // Protocol header starts getting offset to compensate secondary navigation position // -1 to compensate small gap between protocol header and secondary navigation - if (newSecondaryTop <= 1) secondaryNavigation.style.zIndex= 0; + if (newSecondaryTop * -1 >= secondaryNavigationHeight) secondaryNavigation.style.zIndex= 0; } } else { // Just reset secondary navigation and protocol header styles to initial state diff --git a/app/javascript/vue/shared/content/checklist.vue b/app/javascript/vue/shared/content/checklist.vue index 76d47c2f4..f82561faf 100644 --- a/app/javascript/vue/shared/content/checklist.vue +++ b/app/javascript/vue/shared/content/checklist.vue @@ -43,7 +43,7 @@ > -
+ @keyup.enter="addItem(orderedChecklistItems.length + 1)" + @click="addItem(orderedChecklistItems.length + 1)"> {{ i18n.t('protocols.steps.insert.checklist_item') }}
@@ -117,36 +116,37 @@ data() { return { checklistItems: [], - linesToPaste: 0, editingName: false, reordering: false, - editingItem: false + editingItem: false, } }, created() { - this.initChecklistItems(); + this.loadChecklistItems(); if (this.isNew) { - this.addItem(); + this.addItem(orderedChecklistItems.length + 1); } }, watch: { element() { - this.initChecklistItems(); + this.loadChecklistItems(); } }, computed: { orderedChecklistItems() { - return this.checklistItems.map((item, index) => { - return { attributes: {...item.attributes, position: index } } - }); - }, - pastingMultiline() { - return this.linesToPaste > 0; + return this.checklistItems.sort((a, b) => a.attributes.position - b.attributes.position || b.id - a.id) + .map((item, index) => { + item.attributes.position = index + 1; + return item; + }); }, locked() { return this.reordering || this.editingName || !this.element.attributes.orderable.urls.update_url }, + addingNewItem() { + return this.checklistItems.find((item) => item.attributes.isNew); + }, actionMenu() { let menu = []; if (this.element.attributes.orderable.urls.update_url) { @@ -177,57 +177,46 @@ } }, methods: { - initChecklistItems() { - this.checklistItems = this.element.attributes.orderable.checklist_items.map((item, index) => { - return { attributes: {...item, position: index } } + loadChecklistItems(insertAfter) { + $.get(this.element.attributes.orderable.urls.checklist_items_url, (result) => { + this.checklistItems = result.data; + if (insertAfter != null) { + this.addItem(insertAfter); + } }); }, updateName(name) { this.element.attributes.orderable.name = name; this.editingName = false; - this.update(false); }, - update(skipRequest = true) { - this.element.attributes.orderable.checklist_items = - this.checklistItems.map((i) => i.attributes); - - this.$emit('update', this.element, skipRequest); - }, - postItem(item, callback) { - console.log(this.element.attributes.orderable.urls.create_item_url) + postItem(item) { + item.attributes.position = item.attributes.position - 1; $.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => { - this.checklistItems.splice( - result.data.attributes.position, - 1, - { attributes: { ...result.data.attributes, id: result.data.id } } - ); - - if(callback) callback(); + this.loadChecklistItems(result.data[result.data.length - 1].attributes.position) }).fail((e) => { HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger'); }); - this.update(); + // Fake element during loading + item.id = 'new' + Math.floor(Math.random() * 1000000000); + this.checklistItems.push(item); + }, - saveItem(item) { - if (item.attributes.id) { + saveItem(item, key) { + if (item.id > 0) { + let insertAfter = key === 'Enter' ? item.attributes.position : null; $.ajax({ url: item.attributes.urls.update_url, type: 'PATCH', data: item, - success: (result) => { - let updatedItem = this.checklistItems[item.attributes.position] - updatedItem.attributes = result.data.attributes - updatedItem.attributes.id = item.attributes.id - this.$set(this.checklistItems, item.attributes.position, updatedItem) + success: () => { + this.loadChecklistItems(insertAfter) }, error: (xhr) => setFlashErrors(xhr.responseJSON.errors) }); } else { - // create item, then append next one - this.postItem(item, this.addItem); + this.postItem(item, key); } - this.update(true); }, saveItemChecked(item) { $.ajax({ @@ -235,29 +224,28 @@ type: 'PATCH', data: { attributes: { checked: item.attributes.checked } }, success: (result) => { - let updatedItem = this.checklistItems[item.attributes.position] - updatedItem.attributes = result.data.attributes - updatedItem.attributes.id = item.attributes.id - this.$set(this.checklistItems, item.attributes.position, updatedItem) + this.checklistItems.find( + (i) => i.id === item.id + ).attributes.checked = result.data.attributes.checked; }, error: () => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger') }); }, - addItem() { + addItem(insertAfter) { this.checklistItems.push( { attributes: { text: '', checked: false, - position: this.checklistItems.length, + position: insertAfter, isNew: true } } ); + this.checklistItems = this.orderedChecklistItems; }, removeItem(position) { - this.checklistItems.splice(position, 1); - this.update(); + this.checklistItems = this.orderedChecklistItems.filter((item) => item.attributes.position !== position); }, startReorder() { this.reordering = true; @@ -266,10 +254,12 @@ this.reordering = false; if( Number.isInteger(event.newIndex) - && Number.isInteger(event.newIndex) + && Number.isInteger(event.oldIndex) && event.newIndex !== event.oldIndex ){ - const { id, position } = this.orderedChecklistItems[event.newIndex]?.attributes + let position = this.orderedChecklistItems[event.newIndex]?.attributes.position; + let id = this.checklistItems[event.oldIndex]?.id; + this.checklistItems[event.oldIndex].attributes.position = position + (event.newIndex > event.oldIndex ? 1 : -1); this.saveItemOrder(id, position); } }, @@ -281,31 +271,9 @@ contentType: "application/json", dataType: "json", error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors), - success: () => this.update() + success: () => this.loadChecklistItems() }); }, - handleMultilinePaste(data) { - this.linesToPaste = data.length; - let nextPosition = this.checklistItems.length - 1; - - // we need to post items to API in the right order, to avoid positions breaking - let synchronousPost = (index) => { - if(index === data.length) return; - - let item = { - attributes: { - text: data[index], - checked: false, - position: nextPosition + index - } - }; - - this.linesToPaste -= 1; - this.postItem(item, () => synchronousPost(index + 1)); - }; - - synchronousPost(0); - }, setFlashErrors(errors) { for(const key in errors){ HelperModule.flashAlertMsg( diff --git a/app/javascript/vue/shared/content/checklistItem.vue b/app/javascript/vue/shared/content/checklistItem.vue index e17951255..a6da777f4 100644 --- a/app/javascript/vue/shared/content/checklistItem.vue +++ b/app/javascript/vue/shared/content/checklistItem.vue @@ -2,13 +2,13 @@
-
-
+
+
-
+
- +
@@ -81,7 +83,8 @@ }, data() { return { - editingText: false + editingText: false, + deleting: false } }, computed: { @@ -116,6 +119,8 @@ }, disableTextEdit() { if (this.checklistItem.attributes.isNew) { + if (this.deleting) return + this.removeItem(); this.$emit('editEnd'); this.editingText = false; @@ -129,25 +134,30 @@ this.checklistItem.attributes.checked = this.$refs.checkbox.checked; this.$emit('toggle', this.checklistItem); }, - updateText(text) { + updateText(text, withKey) { if (text.length === 0) { this.disableTextEdit(); - this.removeItem(); } else { this.checklistItem.attributes.text = text; - this.update(); + this.update(withKey); } }, removeItem() { + this.deleting = true; if (this.deleteUrl) { this.deleteElement(); } else { this.$emit('removeItem', this.checklistItem.attributes.position); } }, - update() { - this.$emit('update', this.checklistItem); - } + update(withKey) { + this.$emit('update', this.checklistItem, withKey); + }, + keyPressHandler(e) { + if (e.key === 'Enter' && e.shiftKey) { + this.checklistItem.attributes.with_paragraphs = true; + } + }, } } diff --git a/app/javascript/vue/shared/inline_edit.vue b/app/javascript/vue/shared/inline_edit.vue index 358b62882..df998ee8d 100644 --- a/app/javascript/vue/shared/inline_edit.vue +++ b/app/javascript/vue/shared/inline_edit.vue @@ -12,13 +12,12 @@ }" v-model="newValue" @keydown="handleKeypress" - @paste="handlePaste" @blur="handleBlur" @keyup.escape="cancelEdit" @focus="setCaretAtEnd"/>