mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-11 22:36:33 +08:00
Merge pull request #6272 from aignatov-bio/ai-sci-9340-update-checklists-interactions
Refactor checklist [SCI-9340]
This commit is contained in:
commit
a19a2d16e9
9 changed files with 166 additions and 168 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue