Merge pull request #6272 from aignatov-bio/ai-sci-9340-update-checklists-interactions

Refactor checklist [SCI-9340]
This commit is contained in:
aignatov-bio 2023-09-22 12:09:41 +02:00 committed by GitHub
commit a19a2d16e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 168 deletions

View file

@ -10,22 +10,28 @@ module StepElements
before_action :check_toggle_permissions, only: %i(toggle) before_action :check_toggle_permissions, only: %i(toggle)
before_action :check_manage_permissions, only: %i(create update destroy) before_action :check_manage_permissions, only: %i(create update destroy)
def create def index
checklist_item = @checklist.checklist_items.build(checklist_item_params.merge!(created_by: current_user)) 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 ActiveRecord::Base.transaction do
checklist_item.save! new_items = checklist_item.save_multiline!
log_activity( new_items.each do |item|
"#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added", log_activity(
{ "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added",
checklist_item: checklist_item.text, {
checklist_name: @checklist.name checklist_item: item.text,
} checklist_name: @checklist.name
) }
checklist_item_annotation(@step, checklist_item) )
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 rescue ActiveRecord::RecordInvalid
render json: { errors: checklist_item.errors }, status: :unprocessable_entity render json: { errors: checklist_item.errors }, status: :unprocessable_entity
end end
@ -35,17 +41,20 @@ module StepElements
@checklist_item.assign_attributes( @checklist_item.assign_attributes(
checklist_item_params.except(:position, :id).merge(last_modified_by: current_user) checklist_item_params.except(:position, :id).merge(last_modified_by: current_user)
) )
new_items = []
if @checklist_item.save! ActiveRecord::Base.transaction do
log_activity( new_items = @checklist_item.save_multiline!
"#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_edited", new_items.each_with_index do |item, i|
checklist_item: @checklist_item.text, log_activity(
checklist_name: @checklist.name "#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_#{i.zero? ? 'edited' : 'added'}",
) checklist_item: item.text,
checklist_item_annotation(@step, @checklist_item, old_text) checklist_name: @checklist.name
)
checklist_item_annotation(@step, item, old_text)
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 rescue ActiveRecord::RecordInvalid
render json: { errors: @checklist_item.errors }, status: :unprocessable_entity render json: { errors: @checklist_item.errors }, status: :unprocessable_entity
end end
@ -96,7 +105,6 @@ module StepElements
checklist_item = @checklist.checklist_items.find(checklist_item_params[:id]) checklist_item = @checklist.checklist_items.find(checklist_item_params[:id])
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
checklist_item.insert_at(checklist_item_params[:position]) checklist_item.insert_at(checklist_item_params[:position])
@checklist.touch
end end
render json: params[:checklist_item_positions], status: :ok render json: params[:checklist_item_positions], status: :ok
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
@ -118,7 +126,7 @@ module StepElements
end end
def checklist_item_params def checklist_item_params
params.require(:attributes).permit(:text, :position, :id) params.require(:attributes).permit(:text, :position, :id, :with_paragraphs)
end end
def checklist_toggle_item_params def checklist_toggle_item_params

View file

@ -431,7 +431,7 @@
secondaryNavigation.style.zIndex= 251; secondaryNavigation.style.zIndex= 251;
} else { } else {
secondaryNavigation.style.boxShadow = 'none'; 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 if (protocolHeaderTop - 5 < protocolHeaderHeight) { // When secondary navigation touch protocol header
@ -457,7 +457,7 @@
secondaryNavigation.style.top = newSecondaryTop + 'px'; // Secondary navigation starts slowly disappear 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 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 // -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 { } else {
// Just reset secondary navigation and protocol header styles to initial state // Just reset secondary navigation and protocol header styles to initial state

View file

@ -43,7 +43,7 @@
> >
<ChecklistItem <ChecklistItem
v-for="checklistItem in orderedChecklistItems" v-for="checklistItem in orderedChecklistItems"
:key="checklistItem.attributes.id" :key="checklistItem.id"
:checklistItem="checklistItem" :checklistItem="checklistItem"
:locked="locked" :locked="locked"
:reorderChecklistItemUrl="element.attributes.orderable.urls.reorder_url" :reorderChecklistItemUrl="element.attributes.orderable.urls.reorder_url"
@ -55,14 +55,13 @@
@toggle="saveItemChecked" @toggle="saveItemChecked"
@removeItem="removeItem" @removeItem="removeItem"
@component:delete="removeItem" @component:delete="removeItem"
@multilinePaste="handleMultilinePaste"
/> />
</Draggable> </Draggable>
<div v-if="element.attributes.orderable.urls.create_item_url" <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" @keyup.enter="addItem(orderedChecklistItems.length + 1)"
@click="addItem"> @click="addItem(orderedChecklistItems.length + 1)">
<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>
@ -117,36 +116,37 @@
data() { data() {
return { return {
checklistItems: [], checklistItems: [],
linesToPaste: 0,
editingName: false, editingName: false,
reordering: false, reordering: false,
editingItem: false editingItem: false,
} }
}, },
created() { created() {
this.initChecklistItems(); this.loadChecklistItems();
if (this.isNew) { if (this.isNew) {
this.addItem(); this.addItem(orderedChecklistItems.length + 1);
} }
}, },
watch: { watch: {
element() { element() {
this.initChecklistItems(); this.loadChecklistItems();
} }
}, },
computed: { computed: {
orderedChecklistItems() { orderedChecklistItems() {
return this.checklistItems.map((item, index) => { return this.checklistItems.sort((a, b) => a.attributes.position - b.attributes.position || b.id - a.id)
return { attributes: {...item.attributes, position: index } } .map((item, index) => {
}); item.attributes.position = index + 1;
}, return item;
pastingMultiline() { });
return this.linesToPaste > 0;
}, },
locked() { locked() {
return this.reordering || this.editingName || !this.element.attributes.orderable.urls.update_url return this.reordering || this.editingName || !this.element.attributes.orderable.urls.update_url
}, },
addingNewItem() {
return this.checklistItems.find((item) => item.attributes.isNew);
},
actionMenu() { actionMenu() {
let menu = []; let menu = [];
if (this.element.attributes.orderable.urls.update_url) { if (this.element.attributes.orderable.urls.update_url) {
@ -177,57 +177,46 @@
} }
}, },
methods: { methods: {
initChecklistItems() { loadChecklistItems(insertAfter) {
this.checklistItems = this.element.attributes.orderable.checklist_items.map((item, index) => { $.get(this.element.attributes.orderable.urls.checklist_items_url, (result) => {
return { attributes: {...item, position: index } } this.checklistItems = result.data;
if (insertAfter != null) {
this.addItem(insertAfter);
}
}); });
}, },
updateName(name) { updateName(name) {
this.element.attributes.orderable.name = name; this.element.attributes.orderable.name = name;
this.editingName = false; this.editingName = false;
this.update(false);
}, },
update(skipRequest = true) { postItem(item) {
this.element.attributes.orderable.checklist_items = item.attributes.position = item.attributes.position - 1;
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)
$.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => { $.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => {
this.checklistItems.splice( this.loadChecklistItems(result.data[result.data.length - 1].attributes.position)
result.data.attributes.position,
1,
{ attributes: { ...result.data.attributes, id: result.data.id } }
);
if(callback) callback();
}).fail((e) => { }).fail((e) => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger'); 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) { saveItem(item, key) {
if (item.attributes.id) { if (item.id > 0) {
let insertAfter = key === 'Enter' ? item.attributes.position : null;
$.ajax({ $.ajax({
url: item.attributes.urls.update_url, url: item.attributes.urls.update_url,
type: 'PATCH', type: 'PATCH',
data: item, data: item,
success: (result) => { success: () => {
let updatedItem = this.checklistItems[item.attributes.position] this.loadChecklistItems(insertAfter)
updatedItem.attributes = result.data.attributes
updatedItem.attributes.id = item.attributes.id
this.$set(this.checklistItems, item.attributes.position, updatedItem)
}, },
error: (xhr) => setFlashErrors(xhr.responseJSON.errors) error: (xhr) => setFlashErrors(xhr.responseJSON.errors)
}); });
} else { } else {
// create item, then append next one this.postItem(item, key);
this.postItem(item, this.addItem);
} }
this.update(true);
}, },
saveItemChecked(item) { saveItemChecked(item) {
$.ajax({ $.ajax({
@ -235,29 +224,28 @@
type: 'PATCH', type: 'PATCH',
data: { attributes: { checked: item.attributes.checked } }, data: { attributes: { checked: item.attributes.checked } },
success: (result) => { success: (result) => {
let updatedItem = this.checklistItems[item.attributes.position] this.checklistItems.find(
updatedItem.attributes = result.data.attributes (i) => i.id === item.id
updatedItem.attributes.id = item.attributes.id ).attributes.checked = result.data.attributes.checked;
this.$set(this.checklistItems, item.attributes.position, updatedItem)
}, },
error: () => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger') error: () => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger')
}); });
}, },
addItem() { addItem(insertAfter) {
this.checklistItems.push( this.checklistItems.push(
{ {
attributes: { attributes: {
text: '', text: '',
checked: false, checked: false,
position: this.checklistItems.length, position: insertAfter,
isNew: true isNew: true
} }
} }
); );
this.checklistItems = this.orderedChecklistItems;
}, },
removeItem(position) { removeItem(position) {
this.checklistItems.splice(position, 1); this.checklistItems = this.orderedChecklistItems.filter((item) => item.attributes.position !== position);
this.update();
}, },
startReorder() { startReorder() {
this.reordering = true; this.reordering = true;
@ -266,10 +254,12 @@
this.reordering = false; this.reordering = false;
if( if(
Number.isInteger(event.newIndex) Number.isInteger(event.newIndex)
&& Number.isInteger(event.newIndex) && Number.isInteger(event.oldIndex)
&& event.newIndex !== 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); this.saveItemOrder(id, position);
} }
}, },
@ -281,31 +271,9 @@
contentType: "application/json", contentType: "application/json",
dataType: "json", dataType: "json",
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors), 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) { setFlashErrors(errors) {
for(const key in errors){ for(const key in errors){
HelperModule.flashAlertMsg( HelperModule.flashAlertMsg(

View file

@ -2,13 +2,13 @@
<div class="content__checklist-item"> <div class="content__checklist-item">
<div class="checklist-item-header flex rounded pl-10 ml-[-2.325rem] items-center relative w-full group/checklist-item-header" :class="{ 'locked': locked || editingText, 'editing-name': editingText }"> <div class="checklist-item-header flex rounded pl-10 ml-[-2.325rem] items-center relative w-full group/checklist-item-header" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
<div v-if="reorderChecklistItemUrl" <div v-if="reorderChecklistItemUrl"
class="absolute h-6 cursor-grab justify-center left-0 top-1 px-2 tw-hidden text-sn-grey element-grip step-element-grip--draggable" class="absolute h-6 cursor-grab justify-center left-0 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) }"
> >
<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 pt-[1px]" :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-y border-transparent border-solid w-6" :class="{ 'disabled': !toggleUrl }"> <div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid" :class="{ 'disabled': !toggleUrl }">
<input ref="checkbox" <input ref="checkbox"
type="checkbox" type="checkbox"
class="sci-checkbox" class="sci-checkbox"
@ -18,7 +18,11 @@
</span> </span>
</div> </div>
<div v-else class="w-6"></div> <div v-else class="w-6"></div>
<div class="pr-10 grow relative flex items-start max-w-[90ch]" :class="{ 'pointer-events-none': !checklistItem.attributes.isNew && !updateUrl }"> <div class="pr-24 relative flex items-start max-w-[90ch]"
:class="{
'pointer-events-none': !checklistItem.attributes.isNew && !updateUrl,
'flex-grow': editingText,
}">
<InlineEdit <InlineEdit
:value="checklistItem.attributes.text" :value="checklistItem.attributes.text"
:sa_value="checklistItem.attributes.sa_text" :sa_value="checklistItem.attributes.sa_text"
@ -28,18 +32,16 @@
:singleLine="false" :singleLine="false"
:autofocus="editingText" :autofocus="editingText"
:attributeName="`${i18n.t('ChecklistItem')} ${i18n.t('name')}`" :attributeName="`${i18n.t('ChecklistItem')} ${i18n.t('name')}`"
:multilinePaste="true"
:editOnload="checklistItem.attributes.isNew" :editOnload="checklistItem.attributes.isNew"
:smartAnnotation="true" :smartAnnotation="true"
:saveOnEnter="false"
:allowNewLine="true"
@editingEnabled="enableTextEdit" @editingEnabled="enableTextEdit"
@editingDisabled="disableTextEdit" @editingDisabled="disableTextEdit"
@update="updateText" @update="updateText"
@delete="removeItem()" @delete="removeItem()"
@multilinePaste="(data) => { $emit('multilinePaste', data) && removeItem() }" @keypress="keyPressHandler"
@blur="editingText = false"
/> />
<span v-if="!checklistItem.attributes.urls || deleteUrl" class="absolute right-0 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>
@ -81,7 +83,8 @@
}, },
data() { data() {
return { return {
editingText: false editingText: false,
deleting: false
} }
}, },
computed: { computed: {
@ -116,6 +119,8 @@
}, },
disableTextEdit() { disableTextEdit() {
if (this.checklistItem.attributes.isNew) { if (this.checklistItem.attributes.isNew) {
if (this.deleting) return
this.removeItem(); this.removeItem();
this.$emit('editEnd'); this.$emit('editEnd');
this.editingText = false; this.editingText = false;
@ -129,25 +134,30 @@
this.checklistItem.attributes.checked = this.$refs.checkbox.checked; this.checklistItem.attributes.checked = this.$refs.checkbox.checked;
this.$emit('toggle', this.checklistItem); this.$emit('toggle', this.checklistItem);
}, },
updateText(text) { updateText(text, withKey) {
if (text.length === 0) { if (text.length === 0) {
this.disableTextEdit(); this.disableTextEdit();
this.removeItem();
} else { } else {
this.checklistItem.attributes.text = text; this.checklistItem.attributes.text = text;
this.update(); this.update(withKey);
} }
}, },
removeItem() { removeItem() {
this.deleting = true;
if (this.deleteUrl) { if (this.deleteUrl) {
this.deleteElement(); this.deleteElement();
} else { } else {
this.$emit('removeItem', this.checklistItem.attributes.position); this.$emit('removeItem', this.checklistItem.attributes.position);
} }
}, },
update() { update(withKey) {
this.$emit('update', this.checklistItem); this.$emit('update', this.checklistItem, withKey);
} },
keyPressHandler(e) {
if (e.key === 'Enter' && e.shiftKey) {
this.checklistItem.attributes.with_paragraphs = true;
}
},
} }
} }
</script> </script>

View file

@ -12,13 +12,12 @@
}" }"
v-model="newValue" v-model="newValue"
@keydown="handleKeypress" @keydown="handleKeypress"
@paste="handlePaste"
@blur="handleBlur" @blur="handleBlur"
@keyup.escape="cancelEdit" @keyup.escape="cancelEdit"
@focus="setCaretAtEnd"/> @focus="setCaretAtEnd"/>
<textarea v-else <textarea v-else
ref="input" ref="input"
class="overflow-hidden leading-5 inline-block outline-none px-0 py-1 border-0 border-solid border-y w-full border-t-transparent mb-[1px]" class="overflow-hidden leading-5 inline-block outline-none px-0 py-1 border-0 border-solid border-y w-full border-t-transparent mb-0.5"
:class="{ :class="{
'inline-edit-placeholder text-sn-grey caret-black': isBlank, 'inline-edit-placeholder text-sn-grey caret-black': isBlank,
'border-sn-delete-red': error, 'border-sn-delete-red': error,
@ -27,7 +26,6 @@
:placeholder="placeholder" :placeholder="placeholder"
v-model="newValue" v-model="newValue"
@keydown="handleKeypress" @keydown="handleKeypress"
@paste="handlePaste"
@blur="handleBlur" @blur="handleBlur"
@keyup.escape="cancelEdit" @keyup.escape="cancelEdit"
@focus="setCaretAtEnd"/> @focus="setCaretAtEnd"/>
@ -36,7 +34,7 @@
v-else v-else
ref="view" ref="view"
class="grid sci-cursor-edit leading-5 border-0 py-1 outline-none inline-block border-solid border-y border-transparent" class="grid sci-cursor-edit leading-5 border-0 py-1 outline-none inline-block border-solid border-y border-transparent"
:class="{ 'text-sn-grey font-normal': isBlank }" :class="{ 'text-sn-grey font-normal': isBlank, 'whitespace-pre': !singleLine }"
@click="enableEdit($event)" @click="enableEdit($event)"
> >
<span :class="{'truncate': singleLine}" v-if="smartAnnotation" v-html="sa_value || placeholder" ></span> <span :class="{'truncate': singleLine}" v-if="smartAnnotation" v-html="sa_value || placeholder" ></span>
@ -151,6 +149,7 @@
}, },
handleBlur() { handleBlur() {
if ($('.atwho-view:visible').length) return; if ($('.atwho-view:visible').length) return;
this.$emit('blur');
if (this.allowBlank || !this.isBlank) { if (this.allowBlank || !this.isBlank) {
this.$nextTick(this.update); this.$nextTick(this.update);
} else { } else {
@ -198,37 +197,6 @@
this.newValue = this.value || ''; this.newValue = this.value || '';
this.$emit('editingDisabled'); this.$emit('editingDisabled');
}, },
handlePaste(e) {
e.preventDefault();
this.dirty = true;
const clipboardData = (e.clipboardData || window.clipboardData).getData("text");
let lines = clipboardData.split(/[\n\r]/).filter((l) => l).map((l) => l.trim());
const selection = window.getSelection();
if (!selection.rangeCount) return;
selection.deleteFromDocument();
let textNode = document.createTextNode(lines[0]);
selection.getRangeAt(0).insertNode(textNode);
let range = document.createRange();
range.setStart(textNode, textNode.length);
range.setEnd(textNode, textNode.length);
this.$nextTick(() => {
this.newValue = e.target.textContent;
selection.removeAllRanges();
selection.addRange(range);
// Handle multi-line paste
if (this.multilinePaste && lines.length > 1) {
this.$emit('multilinePaste', lines);
this.update();
}
});
},
handleInput(e) { handleInput(e) {
this.dirty = true; this.dirty = true;
@ -242,15 +210,16 @@
handleKeypress(e) { handleKeypress(e) {
if (e.key == 'Escape') { if (e.key == 'Escape') {
this.cancelEdit(); this.cancelEdit();
} else if (e.key == 'Enter' && this.saveOnEnter) { } else if (e.key == 'Enter' && this.saveOnEnter && e.shiftKey == false) {
e.preventDefault() e.preventDefault()
this.update(); this.update(e.key);
} else { } else {
if (!this.error) this.$emit('error-cleared'); if (!this.error) this.$emit('error-cleared');
this.dirty = true; this.dirty = true;
} }
this.$emit('keypress', e)
}, },
update() { update(withKey = null) {
this.refreshTexareaHeight(); this.refreshTexareaHeight();
if (!this.dirty && !this.isBlank) { if (!this.dirty && !this.isBlank) {
@ -264,11 +233,13 @@
this.editing = false; this.editing = false;
this.$emit('editingDisabled'); this.$emit('editingDisabled');
this.$emit('update', this.newValue); this.$emit('update', this.newValue, withKey);
}, },
refreshTexareaHeight() { refreshTexareaHeight() {
if (this.editing && !this.singleLine) { if (this.editing && !this.singleLine) {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.input) return;
this.$refs.input.style.height = '0px'; this.$refs.input.style.height = '0px';
this.$refs.input.style.height = this.$refs.input.scrollHeight + 'px'; this.$refs.input.style.height = this.$refs.input.scrollHeight + 'px';
}); });

View file

@ -1,4 +1,7 @@
class ChecklistItem < ApplicationRecord class ChecklistItem < ApplicationRecord
attr_accessor :with_paragraphs
auto_strip_attributes :text, nullify: false auto_strip_attributes :text, nullify: false
validates :text, validates :text,
presence: true, presence: true,
@ -24,6 +27,45 @@ class ChecklistItem < ApplicationRecord
after_save :touch_checklist after_save :touch_checklist
after_touch :touch_checklist after_touch :touch_checklist
def save_multiline!
if with_paragraphs
if new_record?
original_position = position
self.position = nil
save!
insert_at(original_position + 1)
else
save!
end
return [self]
end
items = []
if new_record?
start_position = position
text.split("\n").compact.each do |line|
new_item = checklist.checklist_items.create!(text: line)
new_item.insert_at(start_position + 1)
start_position = new_item.position
items.push(new_item)
end
else
item = self
text.split("\n").compact.each_with_index do |line, index|
if index.zero?
update!(text: line)
items.push(self)
else
new_item = checklist.checklist_items.create!(text: line)
new_item.insert_at(item.position + 1)
item = new_item
items.push(new_item)
end
end
end
items
end
private private
def touch_checklist def touch_checklist

View file

@ -6,7 +6,11 @@ class ChecklistItemSerializer < ActiveModel::Serializer
include ApplicationHelper include ApplicationHelper
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
attributes :id, :text, :checked, :position, :urls, :sa_text attributes :id, :text, :checked, :position, :urls, :sa_text, :with_paragraphs
def with_paragraphs
object.text.include?("\n")
end
def sa_text def sa_text
@user = scope[:user] || @instance_options[:user] @user = scope[:user] || @instance_options[:user]

View file

@ -16,12 +16,6 @@ class ChecklistSerializer < ActiveModel::Serializer
:step :step
end end
def checklist_items
object.checklist_items.map do |item|
ChecklistItemSerializer.new(item, scope: { user: scope[:user] || @instance_options[:user] }).as_json
end
end
def sa_name def sa_name
@user = scope[:user] || @instance_options[:user] @user = scope[:user] || @instance_options[:user]
custom_auto_link(object.name, custom_auto_link(object.name,
@ -34,6 +28,7 @@ class ChecklistSerializer < ActiveModel::Serializer
return {} if object.destroyed? || !can_manage_step?(scope[:user] || @instance_options[:user], object.step) return {} if object.destroyed? || !can_manage_step?(scope[:user] || @instance_options[:user], object.step)
{ {
checklist_items_url: step_checklist_checklist_items_path(object.step, object),
duplicate_url: duplicate_step_checklist_path(object.step, object), duplicate_url: duplicate_step_checklist_path(object.step, object),
delete_url: step_checklist_path(object.step, object), delete_url: step_checklist_path(object.step, object),
update_url: step_checklist_path(object.step, object), update_url: step_checklist_path(object.step, object),

View file

@ -582,7 +582,7 @@ Rails.application.routes.draw do
post :move post :move
post :duplicate post :duplicate
end end
resources :checklist_items, controller: 'step_elements/checklist_items', only: %i(create update destroy) do resources :checklist_items, controller: 'step_elements/checklist_items', only: %i(index create update destroy) do
patch :toggle, on: :member patch :toggle, on: :member
post :reorder, on: :collection post :reorder, on: :collection
end end