Refactor checklist [SCI-9959]

This commit is contained in:
Anton 2024-01-22 13:48:37 +01:00
parent cfb64a125c
commit 1fff6da45d
5 changed files with 66 additions and 62 deletions

View file

@ -18,7 +18,7 @@ module StepElements
checklist_item = @checklist.checklist_items.new(checklist_item_params.merge!(created_by: current_user)) checklist_item = @checklist.checklist_items.new(checklist_item_params.merge!(created_by: current_user))
new_items = [] new_items = []
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
new_items = checklist_item.save_multiline! new_items = checklist_item.save_multiline!(after_id: params[:after_id])
new_items.each do |item| new_items.each do |item|
log_activity( log_activity(
"#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added", "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added",
@ -102,9 +102,10 @@ module StepElements
end end
def reorder def reorder
checklist_item = @checklist.checklist_items.find(checklist_item_params[:id]) checklist_item = @checklist.checklist_items.find(params[:id])
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
checklist_item.insert_at(checklist_item_params[:position]) insert_at = (@checklist.checklist_items.find_by(id: params[:after_id])&.position || 0)
checklist_item.insert_at(insert_at)
end end
render json: params[:checklist_item_positions], status: :ok render json: params[:checklist_item_positions], status: :ok
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid

View file

@ -30,7 +30,7 @@
@delete="showDeleteModal" @delete="showDeleteModal"
></MenuDropdown> ></MenuDropdown>
</div> </div>
<div v-if="element.attributes.orderable.urls.create_item_url || orderedChecklistItems.length > 0" :class="{ 'pointer-events-none': locked }"> <div v-if="element.attributes.orderable.urls.create_item_url || checklistItems.length > 0" :class="{ 'pointer-events-none': locked }">
<Draggable <Draggable
v-model="checklistItems" v-model="checklistItems"
:ghostClass="'checklist-item-ghost'" :ghostClass="'checklist-item-ghost'"
@ -63,8 +63,8 @@
<div v-if="element.attributes.orderable.urls.create_item_url && !addingNewItem" <div v-if="element.attributes.orderable.urls.create_item_url && !addingNewItem"
class="flex items-center gap-1 text-sn-blue cursor-pointer mb-2 mt-1 " class="flex items-center gap-1 text-sn-blue cursor-pointer mb-2 mt-1 "
tabindex="0" tabindex="0"
@keyup.enter="addItem(orderedChecklistItems.length + 1)" @keyup.enter="addItem(checklistItems[checklistItems.length - 1]?.id)"
@click="addItem(orderedChecklistItems.length + 1)"> @click="addItem(checklistItems[checklistItems.length - 1]?.id)">
<i class="sn-icon sn-icon-new-task w-6 text-center inline-block"></i> <i class="sn-icon sn-icon-new-task w-6 text-center inline-block"></i>
{{ i18n.t('protocols.steps.insert.checklist_item') }} {{ i18n.t('protocols.steps.insert.checklist_item') }}
</div> </div>
@ -81,6 +81,9 @@
</template> </template>
<script> <script>
/* global HelperModule I18n */
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import DeleteMixin from './mixins/delete.js'; import DeleteMixin from './mixins/delete.js';
import MoveMixin from './mixins/move.js'; import MoveMixin from './mixins/move.js';
@ -90,6 +93,7 @@ import InlineEdit from '../inline_edit.vue';
import ChecklistItem from './checklistItem.vue'; import ChecklistItem from './checklistItem.vue';
import moveElementModal from './modal/move.vue'; import moveElementModal from './modal/move.vue';
import MenuDropdown from '../menu_dropdown.vue'; import MenuDropdown from '../menu_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default { export default {
name: 'Checklist', name: 'Checklist',
@ -128,7 +132,7 @@ export default {
}, },
created() { created() {
if (this.isNew) { if (this.isNew) {
this.addItem(1); this.addItem();
} else { } else {
this.loadChecklistItems(); this.loadChecklistItems();
} }
@ -139,13 +143,6 @@ export default {
} }
}, },
computed: { computed: {
orderedChecklistItems() {
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() { locked() {
return this.editingName || !this.element.attributes.orderable.urls.update_url; return this.editingName || !this.element.attributes.orderable.urls.update_url;
}, },
@ -199,20 +196,23 @@ export default {
this.update(); this.update();
}, },
postItem(item) { postItem(item) {
item.attributes.position = item.attributes.position - 1; const position = this.checklistItems.findIndex((i) => i.id === item.id);
$.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => { let afterId = null;
this.loadChecklistItems(result.data[result.data.length - 1].attributes.position); if (position > 0) {
}).fail((e) => { afterId = this.checklistItems[position - 1].id;
}
axios.post(this.element.attributes.orderable.urls.create_item_url, {
attributes: item.attributes,
after_id: afterId
}).then((result) => {
this.loadChecklistItems(result.data.data[result.data.data.length - 1].id);
}).catch(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger'); HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
}); });
// Fake element during loading
item.id = `new${Math.floor(Math.random() * 1000000000)}`;
this.checklistItems.push(item);
}, },
saveItem(item, key) { saveItem(item, key) {
if (item.id > 0) { if (item.id > 0) {
const insertAfter = key === 'Enter' ? item.attributes.position : null; const insertAfter = key === 'Enter' ? item.id : null;
$.ajax({ $.ajax({
url: item.attributes.urls.update_url, url: item.attributes.urls.update_url,
type: 'PATCH', type: 'PATCH',
@ -220,7 +220,7 @@ export default {
success: () => { success: () => {
this.loadChecklistItems(insertAfter); this.loadChecklistItems(insertAfter);
}, },
error: (xhr) => setFlashErrors(xhr.responseJSON.errors) error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors)
}); });
} else { } else {
this.postItem(item, key); this.postItem(item, key);
@ -240,20 +240,23 @@ export default {
}); });
}, },
addItem(insertAfter) { addItem(insertAfter) {
this.checklistItems.push( const afterIndex = this.checklistItems.findIndex((i) => i.id === insertAfter);
this.checklistItems.splice(
afterIndex + 1,
0,
{ {
id: `new${Math.floor(Math.random() * 1000000000)}`,
attributes: { attributes: {
text: '', text: '',
checked: false, checked: false,
position: insertAfter, isNew: true,
isNew: true with_paragraphs: false
} }
} }
); );
this.checklistItems = this.orderedChecklistItems;
}, },
removeItem(position) { removeItem(position) {
this.checklistItems = this.orderedChecklistItems.filter((item) => item.attributes.position !== position); this.checklistItems = this.checklistItems.filter((item) => item.attributes.position !== position);
}, },
startReorder() { startReorder() {
this.reordering = true; this.reordering = true;
@ -265,21 +268,26 @@ export default {
&& Number.isInteger(event.oldIndex) && Number.isInteger(event.oldIndex)
&& event.newIndex !== event.oldIndex && event.newIndex !== event.oldIndex
) { ) {
const position = this.orderedChecklistItems[event.newIndex]?.attributes.position; let afterId = null;
const id = this.checklistItems[event.oldIndex]?.id; if (event.newIndex > 0) {
this.checklistItems[event.oldIndex].attributes.position = position + (event.newIndex > event.oldIndex ? 1 : -1); if (event.newIndex > event.oldIndex) {
this.saveItemOrder(id, position); afterId = this.checklistItems[event.newIndex - 1].id;
} else {
afterId = this.checklistItems[event.newIndex + 1].id;
}
}
const id = this.checklistItems[event.newIndex]?.id;
this.saveItemOrder(id, afterId);
} }
}, },
saveItemOrder(id, position) { saveItemOrder(id, afterId) {
$.ajax({ axios.post(this.element.attributes.orderable.urls.reorder_url, {
type: 'POST', id,
url: this.element.attributes.orderable.urls.reorder_url, after_id: afterId
data: JSON.stringify({ attributes: { id, position } }), }).then(() => {
contentType: 'application/json', this.loadChecklistItems();
dataType: 'json', }).catch((e) => {
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors), this.setFlashErrors(e.response.errors);
success: () => this.loadChecklistItems()
}); });
}, },
setFlashErrors(errors) { setFlashErrors(errors) {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="content__checklist-item pl-10 ml-[-2.325rem]"> <div class="content__checklist-item pl-10 ml-[-2.325rem] group/checklist-item-header">
<div class="checklist-item-header flex rounded items-center relative w-full group/checklist-item-header" :class="{ 'locked': locked || editingText, 'editing-name': editingText }"> <div class="checklist-item-header flex rounded items-center relative w-full" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
<div v-if="reorderChecklistItemUrl" <div v-if="reorderChecklistItemUrl"
class="absolute h-6 cursor-grab justify-center left-[-2.325rem] top-0.5 px-2 tw-hidden text-sn-grey element-grip step-element-grip--draggable" class="absolute h-6 cursor-grab justify-center left-[-2.325rem] top-0.5 px-2 tw-hidden text-sn-grey element-grip step-element-grip--draggable"
:class="{ 'group-hover/checklist-item-header:flex': (!locked && !editingText && draggable) }" :class="{ 'group-hover/checklist-item-header:flex': (!locked && !editingText && draggable) }"
@ -8,7 +8,8 @@
<i class="sn-icon sn-icon-drag"></i> <i class="sn-icon sn-icon-drag"></i>
</div> </div>
<div class="flex items-start gap-2 grow" :class="{ 'done': checklistItem.attributes.checked }"> <div class="flex items-start gap-2 grow" :class="{ 'done': checklistItem.attributes.checked }">
<div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid" :class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'"> <div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid"
:class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'">
<input ref="checkbox" <input ref="checkbox"
type="checkbox" type="checkbox"
class="sci-checkbox" class="sci-checkbox"
@ -42,9 +43,10 @@
@delete="removeItem()" @delete="removeItem()"
@keypress="keyPressHandler" @keypress="keyPressHandler"
@blur="onBlurHandler" @blur="onBlurHandler"
@paste="pasteHandler"
/> />
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)" class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer" @click="showDeleteModal" tabindex="0"> <span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)"
class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer"
@click="showDeleteModal" tabindex="0">
<i class="sn-icon sn-icon-delete"></i> <i class="sn-icon sn-icon-delete"></i>
</span> </span>
</div> </div>
@ -127,8 +129,7 @@ export default {
disableTextEdit() { disableTextEdit() {
if (this.checklistItem.attributes.isNew) { if (this.checklistItem.attributes.isNew) {
if (this.deleting) return; if (this.deleting) return;
if (this.checklistItem.attributes.text.length === 0) this.removeItem();
this.removeItem();
this.$emit('editEnd'); this.$emit('editEnd');
} }
}, },
@ -162,15 +163,9 @@ export default {
this.$emit('update', this.checklistItem, withKey); this.$emit('update', this.checklistItem, withKey);
}, },
keyPressHandler(e) { keyPressHandler(e) {
if ( if ((e.shiftKey || e.metaKey) && e.key === 'Enter') {
((e.shiftKey || e.metaKey) && e.key === 'Enter')
|| ((e.ctrlKey || e.metaKey) && e.key === 'v')
) {
this.checklistItem.attributes.with_paragraphs = true; this.checklistItem.attributes.with_paragraphs = true;
} }
},
pasteHandler() {
this.checklistItem.attributes.with_paragraphs = true;
} }
} }
}; };

View file

@ -270,8 +270,8 @@ export default {
this.newValue = this.$refs.input.value.trim(); // Fix for smart annotation this.newValue = this.$refs.input.value.trim(); // Fix for smart annotation
this.editing = false; this.editing = false;
this.$emit('editingDisabled');
this.$emit('update', this.newValue, withKey); this.$emit('update', this.newValue, withKey);
this.$emit('editingDisabled');
}, },
refreshTexareaHeight() { refreshTexareaHeight() {
if (this.editing && !this.singleLine) { if (this.editing && !this.singleLine) {

View file

@ -27,13 +27,13 @@ class ChecklistItem < ApplicationRecord
after_save :touch_checklist after_save :touch_checklist
after_touch :touch_checklist after_touch :touch_checklist
def save_multiline! def save_multiline!(after_id: nil)
at_position = checklist.checklist_items.find_by(id: after_id).position if after_id
if with_paragraphs if with_paragraphs
if new_record? if new_record?
original_position = position
self.position = nil
save! save!
insert_at(original_position + 1) insert_at(at_position + 1) || 0
else else
save! save!
end end
@ -42,7 +42,7 @@ class ChecklistItem < ApplicationRecord
items = [] items = []
if new_record? if new_record?
start_position = position start_position = at_position || 0
text.split("\n").compact.each do |line| text.split("\n").compact.each do |line|
new_item = checklist.checklist_items.create!(text: line) new_item = checklist.checklist_items.create!(text: line)
new_item.insert_at(start_position + 1) new_item.insert_at(start_position + 1)