scinote-web/app/javascript/vue/results/result.vue

558 lines
20 KiB
Vue

<template>
<div ref="resultContainer"
class="result-wrapper p-4 mb-4 rounded pr-8 relative"
@drop.prevent="dropFile"
@dragenter.prevent="dragEnter($event)"
@dragover.prevent
:data-id="result.id"
:class="{ 'bg-sn-super-light-blue': dragingFile, 'bg-white': !dragingFile, 'locked': locked, 'pointer-events-none': addingContent }"
>
<div class="text-xl items-center flex flex-col text-sn-blue h-full justify-center left-0 absolute top-0 w-full"
v-if="dragingFile"
@dragleave.prevent="dragingFile = false">
{{ i18n.t('my_modules.results.drop_message', { name: result.attributes.name }) }}
<StorageUsage v-if="showStorageUsage()" :parent="result"/>
</div>
<div :class="{ 'opacity-0 pointer-events-none': dragingFile }">
<div class="result-header flex justify-between">
<div class="result-head-left flex items-start flex-grow gap-4">
<a ref="toggleElement" class="result-collapse-link hover:no-underline focus:no-underline py-0.5 border-0 border-y border-transparent border-solid text-sn-black"
:href="'#resultBody' + result.id"
data-toggle="collapse"
data-remote="true"
@click="toggleCollapsed">
<span class="sn-icon sn-icon-right "></span>
</a>
<InlineEdit
:value="result.attributes.name"
class="flex-grow font-bold text-base"
:class="{ 'pointer-events-none': !urls.update_url }"
:singleLine="false"
:characterLimit="255"
:allowBlank="false"
:attributeName="`${i18n.t('Result')} ${i18n.t('name')}`"
:autofocus="editingName"
:placeholder="i18n.t('my_modules.results.placeholder')"
:defaultValue="i18n.t('my_modules.results.default_name')"
:timestamp="i18n.t('protocols.steps.timestamp', {date: result.attributes.created_at, user: result.attributes.created_by })"
@editingEnabled="editingName = true"
@editingDisabled="editingName = false"
:editOnload="result.newResult == true"
@update="updateName"
/>
</div>
<div class="result-head-right flex elements-actions-container">
<input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple />
<MenuDropdown
:listItems="this.insertMenu"
:btnText="i18n.t('my_modules.results.insert.button')"
:position="'right'"
:caret="true"
@create:table="(args) => args ? this.createElement('table', ...args) : this.createElement('table')"
@create:checklist="createElement('checklist')"
@create:text="createElement('text')"
@create:file="openLoadFromComputer"
@create:wopi_file="openWopiFileModal"
@create:ove_file="openOVEditor"
@create:marvinjs_file="openMarvinJsModal($refs.marvinJsButton)"
></MenuDropdown>
<span
class="new-marvinjs-upload-button hidden"
:data-object-id="result.id"
ref="marvinJsButton"
:data-marvin-url="result.attributes.marvinjs_context?.marvin_js_asset_url"
:data-object-type="result.attributes.type"
tabindex="0"
></span> <!-- Hidden element to support legacy code -->
<a href="#"
ref="comments"
class="open-comments-sidebar btn icon-btn btn-light"
data-turbolinks="false"
data-object-type="Result"
:data-object-id="result.id">
<i class="sn-icon sn-icon-comments"></i>
<span class="comments-counter" :class="{ 'hidden': !result.attributes.comments_count }"
:id="`comment-count-${result.id}`">
{{ result.attributes.comments_count }}
</span>
</a>
<MenuDropdown
v-if="!locked"
:listItems="this.actionsMenu"
:btnClasses="'btn btn-light icon-btn'"
:position="'right'"
:btnIcon="'sn-icon sn-icon-more-hori'"
@reorder="openReorderModal"
@duplicate="duplicateResult"
@archive="archiveResult"
@restore="restoreResult"
@delete="showDeleteModal"
></MenuDropdown>
</div>
</div>
<deleteResultModal v-if="confirmingDelete" @confirm="deleteResult" @cancel="closeDeleteModal"/>
<ReorderableItemsModal v-if="reordering"
:title="i18n.t('my_modules.modals.reorder_results.title')"
:items="reorderableElements"
@reorder="updateElementOrder"
@close="closeReorderModal"
/>
<div class="collapse in pl-10" :id="'resultBody' + result.id">
<div v-for="(element, index) in orderedElements" :key="element.id">
<component
:is="elements[index].attributes.orderable_type"
class="result-element"
:element.sync="elements[index]"
:inRepository="false"
:reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
:assignableMyModuleId="result.attributes.my_module_id"
:isNew="element.isNew"
@component:adding-content="($event) => addingContent = $event"
@component:delete="deleteElement"
@update="updateElement"
@reorder="openReorderModal"
@component:insert="insertElement"
@moved="moveElement"
/>
</div>
<Attachments v-if="attachments.length"
:parent="result"
:attachments="attachments"
:attachmentsReady="attachmentsReady"
@attachments:openFileModal="showFileModal = true"
@attachment:deleted="attachmentDeleted"
@attachment:update="updateAttachment"
@attachment:uploaded="loadAttachments"
@attachment:moved="moveAttachment"
@attachments:order="changeAttachmentsOrder"
@attachments:viewMode="changeAttachmentsViewMode"
@attachment:viewMode="updateAttachmentViewMode"/>
<ContentToolbar
v-if="orderedElements.length > 2 && insertMenu.length > 0"
:insertMenu="insertMenu"
@create:table="(...args) => this.createElement('table', ...args)"
@create:text="createElement('text')"
@create:file="openLoadFromComputer"
@create:wopi_file="openWopiFileModal"
@create:ove_file="openOVEditor"
@create:marvinjs_file="openMarvinJsModal($refs.marvinJsButton)"
></ContentToolbar>
</div>
</div>
</div>
</template>
<script>
import axios from '../../packs/custom_axios.js';
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue';
import ResultTable from '../shared/content/table.vue';
import ResultText from '../shared/content/text.vue';
import Attachments from '../shared/content/attachments.vue';
import InlineEdit from '../shared/inline_edit.vue';
import MenuDropdown from '../shared/menu_dropdown.vue';
import deleteResultModal from './delete_result.vue';
import ContentToolbar from '../shared/content/content_toolbar';
import AttachmentsMixin from '../shared/content/mixins/attachments.js';
import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js';
import OveMixin from '../shared/content/attachments/mixins/ove.js';
import UtilsMixin from '../mixins/utils.js';
import StorageUsage from '../shared/content/attachments/storage_usage.vue';
export default {
name: 'Results',
props: {
result: { type: Object, required: true },
resultToReload: { type: Number, required: false },
activeDragResult: {
required: false
},
userSettingsUrl: {
required: false
}
},
data() {
return {
reordering: false,
elements: [],
attachments: [],
attachmentsReady: false,
addingContent: false,
showFileModal: false,
dragingFile: false,
wellPlateOptions: [
{ text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'), emit: 'create:table', params: [[32, 48], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'), emit: 'create:table', params: [[16, 24], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'), emit: 'create:table', params: [[8, 12], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'), emit: 'create:table', params: [[6, 8], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'), emit: 'create:table', params: [[4, 6], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'), emit: 'create:table', params: [[3, 4], true] },
{ text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'), emit: 'create:table', params: [[2, 3], true] }
],
editingName: false,
confirmingDelete: false,
isCollapsed: false
};
},
mixins: [UtilsMixin, AttachmentsMixin, WopiFileModal, OveMixin],
components: {
ReorderableItemsModal,
ResultTable,
ResultText,
Attachments,
InlineEdit,
MenuDropdown,
deleteResultModal,
StorageUsage,
ContentToolbar
},
watch: {
resultToReload() {
if (Number(this.resultToReload) === Number(this.result.id)) {
this.loadElements();
this.loadAttachments();
}
},
activeDragResult() {
if (this.activeDragResult !== this.result.id && this.dragingFile) {
this.dragingFile = false;
}
},
result: {
handler(newVal) {
if (this.isCollapsed !== newVal.attributes.collapsed) {
this.toggleCollapsed();
}
},
deep: true
}
},
mounted() {
this.$nextTick(() => {
const resultId = `#resultBody${this.result.id}`;
this.isCollapsed = this.result.attributes.collapsed;
if (this.isCollapsed) {
$(resultId).collapse('hide');
} else {
$(resultId).collapse('show');
}
});
},
computed: {
reorderableElements() {
return this.orderedElements.map((e) => ({ id: e.id, attributes: e.attributes.orderable }));
},
orderedElements() {
return this.elements.sort((a, b) => a.attributes.position - b.attributes.position);
},
urls() {
return this.result.attributes.urls || {};
},
locked() {
return !(this.urls.restore_url || this.urls.archive_url || this.urls.delete_url || this.urls.update_url);
},
filesMenu() {
let menu = [];
if (this.urls.upload_attachment_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.insert.add_file'),
emit: 'create:file'
}]);
}
if (this.result.attributes.wopi_enabled) {
menu = menu.concat([{
text: this.i18n.t('assets.create_wopi_file.button_text'),
emit: 'create:wopi_file'
}]);
}
if (this.result.attributes.open_vector_editor_context.new_sequence_asset_url) {
menu = menu.concat([{
text: this.i18n.t('open_vector_editor.new_sequence_file'),
emit: 'create:ove_file'
}]);
}
if (this.result.attributes.marvinjs_enabled) {
menu = menu.concat([{
text: this.i18n.t('marvinjs.new_button'),
emit: 'create:marvinjs_file'
}]);
}
return menu;
},
insertMenu() {
let menu = [];
if (this.urls.update_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.insert.text'),
icon: 'sn-icon sn-icon-result-text',
emit: 'create:text'
}, {
text: this.i18n.t('my_modules.results.insert.attachment'),
submenu: this.filesMenu,
icon: 'sn-icon sn-icon-file',
position: 'left'
}, {
text: this.i18n.t('my_modules.results.insert.table'),
icon: 'sn-icon sn-icon-tables',
emit: 'create:table'
}, {
text: this.i18n.t('my_modules.results.insert.well_plate'),
icon: 'sn-icon sn-icon-tables',
submenu: this.wellPlateOptions,
position: 'left'
}]);
}
return menu;
},
actionsMenu() {
let menu = [];
if (this.urls.reorder_elements_url && this.elements.length > 1) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.rearrange'),
emit: 'reorder'
}]);
}
if (this.urls.duplicate_url && !this.result.attributes.archived) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.duplicate'),
emit: 'duplicate'
}]);
}
if (this.urls.archive_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.archive'),
emit: 'archive'
}]);
}
if (this.urls.restore_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.restore'),
emit: 'restore'
}]);
}
if (this.urls.delete_url) {
menu = menu.concat([{
text: this.i18n.t('my_modules.results.actions.delete'),
emit: 'delete'
}]);
}
return menu;
}
},
created() {
this.loadAttachments();
this.loadElements();
},
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
this.result.attributes.collapsed = this.isCollapsed;
const settings = {
key: 'result_states',
data: { [this.result.id]: this.isCollapsed }
};
axios.put(this.userSettingsUrl, { settings: [settings] });
},
dragEnter(e) {
if (!this.urls.upload_attachment_url) return;
// Detect if dragged element is a file
// https://stackoverflow.com/a/8494918
const dt = e.dataTransfer;
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) {
this.dragingFile = true;
this.$emit('result:drag_enter', this.result.id);
}
},
showStorageUsage() {
return (this.elements.length || this.attachments.length) && !this.isCollapsed && this.result.attributes.storage_limit;
},
openReorderModal() {
this.reordering = true;
},
closeReorderModal() {
this.reordering = false;
},
updateElementOrder(orderedElements) {
orderedElements.forEach((element, position) => {
const index = this.elements.findIndex((e) => e.id === element.id);
this.elements[index].attributes.position = position;
});
const elementPositions = {
result_orderable_element_positions: this.elements.map(
(element) => [element.id, element.attributes.position]
)
};
axios.post(this.urls.reorder_elements_url, elementPositions, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
})
.then(() => {
this.$emit('resultUpdated');
})
.catch(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
});
},
deleteElement(position) {
this.elements.splice(position, 1);
const unorderedElements = this.elements.map((e) => {
if (e.attributes.position >= position) {
e.attributes.position -= 1;
}
return e;
});
this.$emit('resultUpdated');
},
updateElement(element, skipRequest = false, callback) {
const index = this.elements.findIndex((e) => e.id === element.id);
this.elements[index].isNew = false;
if (skipRequest) {
this.elements[index].attributes.orderable = element.attributes.orderable;
this.$emit('resultUpdated');
} else {
$.ajax({
url: element.attributes.orderable.urls.update_url,
method: 'PUT',
data: element.attributes.orderable,
success: (result) => {
this.elements[index].attributes.orderable = result.data.attributes;
this.$emit('resultUpdated');
// optional callback after successful update
if (typeof callback === 'function') {
callback();
}
}
}).fail(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
});
}
},
insertElement(element) {
const { position } = element.attributes;
this.elements = this.elements.map((s) => {
if (s.attributes.position >= position) {
s.attributes.position += 1;
}
return s;
});
this.elements.push(element);
},
loadElements() {
$.get(this.urls.elements_url, (result) => {
this.elements = result.data;
this.$emit('result:elements:loaded');
});
},
loadAttachments() {
this.attachmentsReady = false;
$.get(this.urls.attachments_url, (result) => {
this.attachments = result.data;
this.$emit('result:attachments:loaded');
if (this.attachments.findIndex((e) => e.attributes.attached === false) >= 0) {
setTimeout(() => {
this.loadAttachments();
}, 10000);
} else {
this.attachmentsReady = true;
}
});
this.showFileModal = false;
},
attachmentDeleted(id) {
this.attachments = this.attachments.filter((a) => a.id !== id);
this.$emit('resultUpdated');
},
updateAttachment(attachment) {
const index = this.attachments.findIndex((a) => a.id === attachment.id);
if (index !== -1) {
this.attachments[index] = attachment;
}
},
createElement(elementType, tableDimensions = [5, 5], plateTemplate = false) {
$.post(this.urls[`create_${elementType}_url`], { tableDimensions, plateTemplate }, (result) => {
result.data.isNew = true;
this.elements.push(result.data);
if (this.isCollapsed) {
this.$refs.toggleElement.click();
}
this.$emit('resultUpdated');
}).fail(() => {
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
}).done(() => {
this.$parent.$nextTick(() => {
const children = this.$refs.resultContainer.querySelectorAll('.result-element');
const lastChild = children[children.length - 1];
lastChild.scrollIntoView(false);
window.scrollBy({
top: 200,
behavior: 'smooth'
});
});
});
},
archiveResult() {
axios.post(this.urls.archive_url).then((response) => {
this.$emit('result:archived', this.result.id);
});
},
restoreResult() {
axios.post(this.urls.restore_url).then((response) => {
this.$emit('result:restored', this.result.id);
});
},
showDeleteModal() {
this.confirmingDelete = true;
},
closeDeleteModal() {
this.confirmingDelete = false;
},
deleteResult() {
axios.delete(this.urls.delete_url).then((response) => {
this.$emit('result:deleted', this.result.id);
});
},
duplicateResult() {
axios.post(this.urls.duplicate_url).then((_) => {
this.$emit('result:duplicated');
});
},
moveElement(position, target_id) {
this.elements.splice(position, 1);
this.elements.map((e) => {
if (e.attributes.position >= position) {
e.attributes.position -= 1;
}
return e;
});
this.$emit('resultUpdated');
this.$emit('result:move_element', target_id);
},
moveAttachment(id, target_id) {
this.attachments = this.attachments.filter((a) => a.id !== id);
this.$emit('resultUpdated');
this.$emit('result:move_attachment', target_id);
},
updateName(name) {
axios.patch(this.urls.update_url, { result: { name } }).then((_) => {
this.$emit('updated');
});
}
}
};
</script>