scinote-web/app/javascript/vue/protocol/step.vue

479 lines
18 KiB
Vue
Raw Normal View History

<template>
<div ref="stepContainer" class="step-container"
2022-05-25 21:04:14 +08:00
:id="`stepContainer${step.id}`"
2022-05-24 21:54:22 +08:00
@drop.prevent="dropFile"
@dragenter.prevent="dragEnter($event)"
2022-05-24 21:54:22 +08:00
@dragover.prevent
2022-07-11 16:45:25 +08:00
:class="{ 'draging-file': dragingFile, 'showing-comments': showCommentsSidebar, 'editing-name': editingName }"
2022-05-25 21:04:14 +08:00
>
<div class="drop-message" @dragleave.prevent="!showFileModal ? dragingFile = false : null">
{{ i18n.t('protocols.steps.drop_message', { position: step.attributes.position + 1 }) }}
<StorageUsage v-if="showStorageUsage()" :step="step"/>
2022-05-24 21:54:22 +08:00
</div>
2022-07-11 16:45:25 +08:00
<div class="step-header">
2022-08-02 17:00:13 +08:00
<div class="step-element-header" :class="{ 'no-hover': !urls.update_url }">
2022-07-11 16:45:25 +08:00
<div class="step-controls">
<div v-if="reorderStepUrl" class="step-element-grip" @click="$emit('reorder')" :class="{ 'step-element--locked': !urls.update_url }">
<i class="fas fas-rotated-90 fa-exchange-alt"></i>
2022-07-11 16:45:25 +08:00
</div>
2022-07-13 21:15:01 +08:00
<div v-else class="step-element-grip-placeholder"></div>
2022-07-11 16:45:25 +08:00
<a class="step-collapse-link"
:href="'#stepBody' + step.id"
data-toggle="collapse"
data-remote="true"
@click="toggleCollapsed">
2022-07-11 16:45:25 +08:00
<span class="fas fa-caret-right"></span>
</a>
<div v-if="!inRepository" class="step-complete-container" :class="{ 'step-element--locked': !urls.state_url }">
2022-07-14 18:50:06 +08:00
<div :class="`step-state ${step.attributes.completed ? 'completed' : ''}`"
@click="changeState"
@keyup.enter="changeState"
tabindex="0"
:title="step.attributes.completed ? i18n.t('protocols.steps.status.uncomplete') : i18n.t('protocols.steps.status.complete')"
></div>
2022-07-11 16:45:25 +08:00
</div>
<div class="step-position">
{{ step.attributes.position + 1 }}.
</div>
</div>
<div class="step-name-container" :class="{'step-element--locked': !urls.update_url}">
2022-07-11 16:45:25 +08:00
<InlineEdit
:value="step.attributes.name"
:class="{ 'step-element--locked': !urls.update_url }"
2022-07-11 16:45:25 +08:00
:characterLimit="255"
:allowBlank="false"
:attributeName="`${i18n.t('Step')} ${i18n.t('name')}`"
:autofocus="editingName"
:placeholder="i18n.t('protocols.steps.placeholder')"
:defaultValue="i18n.t('protocols.steps.default_name')"
2022-07-11 16:45:25 +08:00
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
2022-07-14 18:50:06 +08:00
:editOnload="step.newStep == true"
2022-07-11 16:45:25 +08:00
@update="updateName"
/>
</div>
<button v-if="urls.update_url && !editingName" class="step-name-edit-icon btn icon-btn btn-light " @click="editingName = true">
<i class="fas fa-pen"></i>
</button>
2022-04-28 17:13:38 +08:00
</div>
<div class="step-actions-container">
<div ref="actionsDropdownButton" v-if="urls.update_url" class="dropdown">
<button class="btn btn-light dropdown-toggle insert-button" type="button" :id="'stepInserMenu_' + step.id" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{ i18n.t('protocols.steps.insert.button') }}
<span class="caret"></span>
</button>
<ul ref="actionsDropdown" class="dropdown-menu insert-element-dropdown dropdown-menu-right" :aria-labelledby="'stepInserMenu_' + step.id">
<li class="title">
{{ i18n.t('protocols.steps.insert.title') }}
</li>
<li class="action" @click="createElement('table')">
<i class="fas fa-table"></i>
{{ i18n.t('protocols.steps.insert.table') }}
</li>
<li class="action" @click="createElement('checklist')">
<i class="fas fa-list"></i>
{{ i18n.t('protocols.steps.insert.checklist') }}
</li>
<li class="action" @click="createElement('text')">
<i class="fas fa-font"></i>
{{ i18n.t('protocols.steps.insert.text') }}
</li>
<li v-if="attachmentsReady" class="action" @click="showFileModal = true">
2022-05-18 17:12:53 +08:00
<i class="fas fa-paperclip"></i>
{{ i18n.t('protocols.steps.insert.attachment') }}
</li>
</ul>
</div>
2022-05-25 21:04:14 +08:00
<a href="#"
v-if="!inRepository"
2022-05-25 21:04:14 +08:00
ref="comments"
class="open-comments-sidebar btn icon-btn btn-light"
data-turbolinks="false"
data-object-type="Step"
2022-07-12 19:54:10 +08:00
@click="openCommentsSidebar"
2022-05-25 21:04:14 +08:00
:data-object-id="step.id">
<i class="fas fa-comment"></i>
<span class="comments-counter"
:id="`comment-count-${step.id}`"
:class="{'unseen': step.attributes.unseen_comments}"
>
{{ step.attributes.comments_count }}
</span>
</a>
<div v-if="urls.update_url" class="step-actions-container">
<div class="dropdown">
<button class="btn btn-light dropdown-toggle insert-button" type="button" :id="'stepOptionsMenu_' + step.id" data-toggle="dropdown" data-display="static" aria-haspopup="true" aria-expanded="true">
<i class="fas fa-ellipsis-h"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right insert-element-dropdown" :aria-labelledby="'stepOptionsMenu_' + step.id">
<li class="title">
{{ i18n.t('protocols.steps.options_dropdown.title') }}
</li>
2022-07-05 18:49:12 +08:00
<li v-if="urls.reorder_elements_url" class="action" @click="openReorderModal" :class="{ 'disabled': elements.length < 2 }">
<i class="fas fas-rotated-90 fa-exchange-alt"></i>
2022-07-05 18:49:12 +08:00
{{ i18n.t('protocols.steps.options_dropdown.rearrange') }}
</li>
2022-08-23 23:54:33 +08:00
<li v-if="urls.duplicate_step_url" class="action" @click="duplicateStep">
<i class="fas fa-clone"></i>
{{ i18n.t('protocols.steps.options_dropdown.duplicate') }}
</li>
2022-07-05 18:49:12 +08:00
<li v-if="urls.delete_url" class="action" @click="showDeleteModal">
<i class="fas fa-trash"></i>
{{ i18n.t('protocols.steps.options_dropdown.delete') }}
</li>
</ul>
</div>
</div>
2022-04-28 17:13:38 +08:00
</div>
</div>
<div class="collapse in" :id="'stepBody' + step.id">
2022-06-21 17:32:27 +08:00
<div class="step-elements">
<template v-for="(element, index) in orderedElements">
2022-06-21 17:32:27 +08:00
<component
:is="elements[index].attributes.orderable_type"
:key="index"
:element.sync="elements[index]"
:inRepository="inRepository"
:reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
:isNew="element.isNew"
2022-06-21 17:32:27 +08:00
@component:delete="deleteElement"
@update="updateElement"
@reorder="openReorderModal"
@component:insert="insertElement"
2022-06-21 17:32:27 +08:00
/>
</template>
<Attachments v-if="attachments.length"
:step="step"
2022-06-21 17:32:27 +08:00
:attachments="attachments"
:attachmentsReady="attachmentsReady"
2022-06-21 17:32:27 +08:00
@attachments:openFileModal="showFileModal = true"
@attachment:deleted="attachmentDeleted"
@attachment:uploaded="loadAttachments"
2022-06-21 17:32:27 +08:00
@attachments:order="changeAttachmentsOrder"
@attachments:viewMode="changeAttachmentsViewMode"
@attachment:viewMode="updateAttachmentViewMode"/>
</div>
</div>
2022-05-04 21:57:13 +08:00
<deleteStepModal v-if="confirmingDelete" @confirm="deleteStep" @cancel="closeDeleteModal"/>
<fileModal v-if="showFileModal"
:step="step"
@cancel="showFileModal = false"
@files="uploadFiles"
@attachmentUploaded="addAttachment"
@attachmentsChanged="loadAttachments"
@copyPasteImageModal="copyPasteImageModal"
/>
<clipboardPasteModal v-if="showClipboardPasteModal"
:step="step"
:image="pasteImages"
@files="uploadFiles"
@cancel="showClipboardPasteModal = false"
/>
<ReorderableItemsModal v-if="reordering"
2022-07-13 21:15:01 +08:00
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
:items="reorderableElements"
@reorder="updateElementOrder"
@close="closeReorderModal"
/>
</div>
</template>
<script>
const ICON_MAP = {
'Checklist': 'fa-list-ul',
'StepText': 'fa-font',
'StepTable': 'fa-table'
}
2022-04-28 17:13:38 +08:00
import InlineEdit from 'vue/shared/inline_edit.vue'
import StepTable from 'vue/protocol/step_elements/table.vue'
import StepText from 'vue/protocol/step_elements/text.vue'
import Checklist from 'vue/protocol/step_elements/checklist.vue'
2022-05-04 21:57:13 +08:00
import deleteStepModal from 'vue/protocol/modals/delete_step.vue'
2022-05-19 17:13:34 +08:00
import Attachments from 'vue/protocol/attachments.vue'
import fileModal from 'vue/protocol/step_attachments/file_modal.vue'
import clipboardPasteModal from 'vue/protocol/step_attachments/clipboard_paste_modal.vue'
import ReorderableItemsModal from 'vue/protocol/modals/reorderable_items_modal.vue'
2022-08-04 17:06:25 +08:00
import UtilsMixin from 'vue/mixins/utils.js'
2022-05-19 17:13:34 +08:00
import AttachmentsMixin from 'vue/protocol/mixins/attachments.js'
import StorageUsage from 'vue/protocol/storage_usage.vue'
2022-04-28 17:13:38 +08:00
export default {
name: 'StepContainer',
props: {
step: {
type: Object,
required: true
},
inRepository: {
type: Boolean,
required: true
},
reorderStepUrl: {
required: false
}
},
data() {
return {
2022-05-04 21:57:13 +08:00
elements: [],
attachments: [],
attachmentsReady: false,
2022-05-18 17:12:53 +08:00
confirmingDelete: false,
2022-05-25 21:04:14 +08:00
showFileModal: false,
showClipboardPasteModal: false,
2022-05-24 21:54:22 +08:00
showCommentsSidebar: false,
dragingFile: false,
2022-07-11 16:45:25 +08:00
reordering: false,
isCollapsed: false,
2022-07-11 16:45:25 +08:00
editingName: false
}
},
mixins: [UtilsMixin, AttachmentsMixin],
components: {
InlineEdit,
StepTable,
StepText,
2022-05-04 21:57:13 +08:00
Checklist,
2022-05-12 23:05:07 +08:00
deleteStepModal,
2022-05-18 17:12:53 +08:00
fileModal,
clipboardPasteModal,
Attachments,
StorageUsage,
ReorderableItemsModal
},
created() {
this.loadAttachments();
this.loadElements();
},
2022-05-25 21:04:14 +08:00
mounted() {
2022-07-11 16:45:25 +08:00
$(this.$refs.comments).data('closeCallback', this.closeCommentsSidebar);
2022-07-12 19:54:10 +08:00
$(this.$refs.comments).data('openCallback', this.closeCommentsSidebar);
$(this.$refs.actionsDropdownButton).on('shown.bs.dropdown hidden.bs.dropdown', this.handleDropdownPosition);
2022-05-25 21:04:14 +08:00
},
computed: {
reorderableElements() {
return this.orderedElements.map((e) => { return { id: e.id, attributes: e.attributes.orderable } })
},
orderedElements() {
return this.elements.sort((a, b) => a.attributes.position - b.attributes.position);
},
urls() {
return this.step.attributes.urls || {}
}
},
methods: {
dragEnter(e) {
2022-08-23 23:59:28 +08:00
if (this.showFileModal || !this.urls.upload_attachment_url) return;
// Detect if dragged element is a file
// https://stackoverflow.com/a/8494918
let dt = e.dataTransfer;
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('Files'))) {
this.dragingFile = true;
}
},
loadAttachments() {
this.attachmentsReady = false
$.get(this.urls.attachments_url, (result) => {
this.attachments = result.data
if (this.attachments.findIndex((e) => e.attributes.attached === false) >= 0) {
setTimeout(() => {
this.loadAttachments()
}, 10000)
} else {
this.attachmentsReady = true
}
});
this.showFileModal = false;
},
loadElements() {
$.get(this.urls.elements_url, (result) => {
this.elements = result.data
});
},
showStorageUsage() {
return (this.elements.length || this.attachments.length) && !this.isCollapsed && this.step.attributes.storage_limit;
},
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
},
2022-05-04 21:57:13 +08:00
showDeleteModal() {
this.confirmingDelete = true;
},
closeDeleteModal() {
this.confirmingDelete = false;
},
deleteStep() {
$.ajax({
url: this.urls.delete_url,
type: 'DELETE',
success: (result) => {
this.$emit(
'step:delete',
result.data,
'delete'
);
}
});
2022-04-28 17:13:38 +08:00
},
changeState() {
if (!this.urls.state_url) return;
this.step.attributes.completed = !this.step.attributes.completed;
this.$emit('step:update', {
completed: this.step.attributes.completed,
position: this.step.attributes.position
});
$.post(this.urls.state_url, {completed: this.step.attributes.completed}).error(() => {
this.step.attributes.completed = !this.step.attributes.completed;
this.$emit('step:update', {
completed: this.step.attributes.completed,
position: this.step.attributes.position
});
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
2022-04-28 17:13:38 +08:00
})
},
deleteElement(position) {
this.elements.splice(position, 1)
let unorderedElements = this.elements.map( e => {
if (e.attributes.position >= position) {
e.attributes.position -= 1;
}
return e;
})
this.$emit('stepUpdated')
},
updateElement(element, skipRequest=false, callback) {
let index = this.elements.findIndex((e) => e.id === element.id);
this.elements[index].isNew = false;
2022-05-10 19:28:09 +08:00
if (skipRequest) {
2022-07-08 19:49:22 +08:00
this.elements[index].attributes.orderable = element.attributes.orderable;
this.$emit('stepUpdated');
2022-05-10 19:28:09 +08:00
} else {
$.ajax({
url: element.attributes.orderable.urls.update_url,
method: 'PUT',
data: element.attributes.orderable,
success: (result) => {
2022-07-08 19:49:22 +08:00
this.elements[index].attributes.orderable = result.data.attributes;
this.$emit('stepUpdated');
// optional callback after successful update
if(typeof callback === 'function') {
callback();
}
2022-05-10 19:28:09 +08:00
}
}).error(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
})
}
},
updateElementOrder(orderedElements) {
orderedElements.forEach((element, position) => {
let index = this.elements.findIndex((e) => e.id === element.id);
this.elements[index].attributes.position = position;
});
let elementPositions =
{
step_orderable_element_positions: this.elements.map(
(element) => [element.id, element.attributes.position]
)
};
$.ajax({
type: "POST",
url: this.urls.reorder_elements_url,
data: JSON.stringify(elementPositions),
contentType: "application/json",
dataType: "json",
error: (() => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger')),
success: (() => this.$emit('stepUpdated'))
});
},
2022-04-28 17:13:38 +08:00
updateName(newName) {
$.ajax({
url: this.urls.update_url,
2022-04-28 17:13:38 +08:00
type: 'PATCH',
data: {step: {name: newName}},
success: (result) => {
this.$emit('step:update', {
name: result.data.attributes.name,
position: this.step.attributes.position
})
2022-04-28 17:13:38 +08:00
}
});
},
createElement(elementType) {
$.post(this.urls[`create_${elementType}_url`], (result) => {
result.data.isNew = true;
this.elements.push(result.data)
}).error(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
})
},
addAttachment(attachment) {
this.attachments.push(attachment);
this.showFileModal = false;
2022-05-25 21:04:14 +08:00
},
2022-07-12 19:54:10 +08:00
openCommentsSidebar() {
$('.comments-sidebar .close-btn').click();
this.showCommentsSidebar = true
},
attachmentDeleted(id) {
this.attachments = this.attachments.filter((a) => a.id !== id );
this.$emit('stepUpdated');
},
2022-05-25 21:04:14 +08:00
closeCommentsSidebar() {
this.showCommentsSidebar = false
},
openReorderModal() {
this.reordering = true;
},
closeReorderModal() {
this.reordering = false;
},
handleDropdownPosition() {
this.$refs.actionsDropdownButton.classList.toggle("dropup", !this.isInViewport(this.$refs.actionsDropdown));
},
isInViewport(el) {
let rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || $(window).height()) &&
rect.right <= (window.innerWidth || $(window).width())
);
},
copyPasteImageModal(pasteImages) {
this.pasteImages = pasteImages;
this.showClipboardPasteModal = true;
},
insertElement(element) {
let position = element.attributes.position;
this.elements = this.elements.map( s => {
if (s.attributes.position >= position) {
s.attributes.position += 1;
}
return s;
})
this.elements.push(element);
2022-08-23 23:54:33 +08:00
},
duplicateStep() {
$.post(this.urls.duplicate_step_url, (result) => {
this.$emit('step:insert', result.data);
HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.step_duplicated'), 'success');
}).error(() => {
HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.step_duplication_failed'), 'danger');
2022-08-23 23:54:33 +08:00
});
}
}
}
</script>