<template> <div class="task-protocol"> <div class="task-section-header" v-if="!inRepository"> <div class="portocol-header-left-part"> <a class="task-section-caret" tabindex="0" role="button" data-toggle="collapse" href="#protocol-content" aria-expanded="true" aria-controls="protocol-content"> <i class="fas fa-caret-right"></i> <div class="task-section-title"> <h2>{{ i18n.t('Protocol') }}</h2> </div> </a> <div class="my-module-protocol-status"> <!-- protocol status dropdown gets mounted here --> </div> </div> <div class="actions-block"> <div class="protocol-buttons-group"> <a v-if="urls.add_step_url" class="btn btn-primary" @keyup.enter="addStep(steps.length)" @click="addStep(steps.length)" tabindex="0"> <span class="fas fa-plus" aria-hidden="true"></span> <span>{{ i18n.t("protocols.steps.new_step") }}</span> </a> <button class="btn btn-secondary" data-toggle="modal" data-target="#print-protocol-modal" tabindex="0"> <span class="fas fa-print" aria-hidden="true"></span> <span>{{ i18n.t("protocols.print.button") }}</span> </button> <ProtocolOptions v-if="protocol.attributes && protocol.attributes.urls" :protocol="protocol" @protocol:delete_steps="deleteSteps" :canDeleteSteps="steps.length > 0 && urls.delete_steps_url !== null" /> </div> </div> </div> <div v-if="protocol.id" id="protocol-content" class="protocol-content collapse in" aria-expanded="true"> <div class="protocol-description"> <div class="protocol-name" v-if="!inRepository"> <InlineEdit v-if="urls.update_protocol_name_url" :value="protocol.attributes.name" :characterLimit="255" :placeholder="i18n.t('my_modules.protocols.protocol_status_bar.enter_name')" :allowBlank="!inRepository" :attributeName="`${i18n.t('Protocol')} ${i18n.t('name')}`" @update="updateName" /> <span v-else> {{ protocol.attributes.name }} </span> </div> <ProtocolMetadata v-if="protocol.attributes && protocol.attributes.in_repository" :protocol="protocol" @update="updateProtocol" @publish="startPublish"/> <div :class="inRepository ? 'protocol-section protocol-information' : ''"> <div v-if="inRepository" id="protocol-description" class="protocol-section-header"> <div class="protocol-description-container"> <a class="protocol-section-caret" role="button" data-toggle="collapse" href="#protocol-description-container" aria-expanded="false" aria-controls="protocol-description-container"> <i class="fas fa-caret-right"></i> <span id="protocolDescriptionLabel" class="protocol-section-title"> <h2> {{ i18n.t("protocols.header.protocol_description") }} </h2> </span> </a> </div> </div> <div id="protocol-description-container" :class=" inRepository ? 'protocol-description collapse in' : ''" > <div v-if="urls.update_protocol_description_url"> <Tinymce :value="protocol.attributes.description" :value_html="protocol.attributes.description_view" :placeholder="i18n.t('my_modules.protocols.protocol_status_bar.empty_description_edit_label')" :updateUrl="urls.update_protocol_description_url" :objectType="'Protocol'" :objectId="parseInt(protocol.id)" :fieldName="'protocol[description]'" :lastUpdated="protocol.attributes.updated_at" :characterLimit="100000" @update="updateDescription" /> </div> <div v-else-if="protocol.attributes.description_view" v-html="protocol.attributes.description_view"></div> <div v-else class="empty-protocol-description"> {{ i18n.t("protocols.no_text_placeholder") }} </div> </div> </div> </div> <div :class="inRepository ? 'protocol-section protocol-steps-section protocol-information' : ''"> <div v-if="inRepository" id="protocol-steps" class="protocol-section-header"> <div class="protocol-steps-container"> <a class="protocol-section-caret" role="button" data-toggle="collapse" href="#protocol-steps-container" aria-expanded="false" aria-controls="protocol-steps-container"> <i class="fas fa-caret-right"></i> <span id="protocolStepsLabel" class="protocol-section-title"> <h2> {{ i18n.t("protocols.header.protocol_steps") }} </h2> </span> </a> </div> </div> <div id="protocol-steps-container" :class=" inRepository ? 'protocol-steps collapse in' : ''"> <div v-if="steps.length > 0" class="protocol-step-actions"> <button class="btn btn-light" @click="collapseSteps" tabindex="0"> <span class="fas fa-caret-up"></span> {{ i18n.t("protocols.steps.collapse_label") }} </button> <button class="btn btn-light" @click="expandSteps" tabindex="0"> <span class="fas fa-caret-down"></span> {{ i18n.t("protocols.steps.expand_label") }} </button> <a v-if="urls.reorder_steps_url" class="btn btn-light" data-toggle="modal" @click="startStepReorder" @keyup.enter="startStepReorder" :class="{'disabled': steps.length == 1}" tabindex="0" > <i class="fas fas-rotated-90 fa-exchange-alt" aria-hidden="true"></i> <span>{{ i18n.t("protocols.reorder_steps.button") }}</span> </a> </div> <div class="protocol-steps"> <template v-for="(step, index) in steps"> <div class="step-block" :key="step.id"> <div v-if="index > 0 && urls.add_step_url" class="insert-step" @click="addStep(index)"> <i class="fas fa-plus"></i> </div> <Step :step.sync="steps[index]" @reorder="startStepReorder" :inRepository="inRepository" @step:delete="updateStepsPosition" @step:update="updateStep" @stepUpdated="refreshProtocolStatus" @step:insert="updateStepsPosition" :reorderStepUrl="steps.length > 1 ? urls.reorder_steps_url : null" /> </div> </template> </div> <button v-if="(steps.length > 0 || inRepository) && urls.add_step_url" :class="!inRepository ? 'btn btn-primary' : 'btn btn-secondary'" @click="addStep(steps.length)"> <i class="fas fa-plus"></i> {{ i18n.t("protocols.steps.new_step") }} </button> </div> </div> </div> <ProtocolModals/> <ReorderableItemsModal v-if="reordering" :title="i18n.t('protocols.reorder_steps.modal.title')" :items="steps" :includeNumbers="true" @reorder="updateStepOrder" @close="closeStepReorderModal" /> <PublishProtocol v-if="publishing" :protocol="protocol" @publish="publishProtocol" @close="closePublishModal" /> </div> </template> <script> import InlineEdit from 'vue/shared/inline_edit.vue' import Step from 'vue/protocol/step' import ProtocolMetadata from 'vue/protocol/protocolMetadata' import ProtocolOptions from 'vue/protocol/protocolOptions' import ProtocolModals from 'vue/protocol/modals' import Tinymce from 'vue/shared/tinymce.vue' import ReorderableItemsModal from 'vue/protocol/modals/reorderable_items_modal.vue' import PublishProtocol from 'vue/protocol/modals/publish_protocol.vue' import UtilsMixin from 'vue/mixins/utils.js' export default { name: 'ProtocolContainer', props: { protocolUrl: { type: String, required: true } }, components: { Step, InlineEdit, ProtocolModals, ProtocolOptions, Tinymce, ReorderableItemsModal, ProtocolMetadata, PublishProtocol }, mixins: [UtilsMixin], computed: { inRepository() { return this.protocol.attributes.in_repository }, urls() { return this.protocol.attributes.urls || {} } }, data() { return { protocol: { attributes: {} }, steps: [], reordering: false, publishing: false } }, created() { $.get(this.protocolUrl, (result) => { this.protocol = result.data; $.get(this.urls.steps_url, (result) => { this.steps = result.data }) }); }, methods: { collapseSteps() { $('.step-container .collapse').collapse('hide') }, expandSteps() { $('.step-container .collapse').collapse('show') }, deleteSteps() { $.post(this.urls.delete_steps_url, () => { this.steps = [] }).error(() => { HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger') }) }, refreshProtocolStatus() { if (this.inRepository) return // legacy method from app/assets/javascripts/my_modules/protocols.js refreshProtocolStatusBar(); }, updateProtocol(attributes) { this.protocol.attributes = attributes }, updateName(newName) { this.protocol.attributes.name = newName; this.refreshProtocolStatus(); $.ajax({ type: 'PATCH', url: this.urls.update_protocol_name_url, data: { protocol: { name: newName } } }); }, updateDescription(protocol) { this.protocol.attributes = protocol.attributes }, addStep(position) { $.post(this.urls.add_step_url, {position: position}, (result) => { result.data.newStep = true this.updateStepsPosition(result.data); // scroll to bottom if step was appended at the end if(position === this.steps.length - 1) { this.$nextTick(() => this.scrollToBottom()); } }) this.refreshProtocolStatus(); }, updateStepsPosition(step, action = 'add') { let position = step.attributes.position; if (action === 'delete') { this.steps.splice(position, 1) } let unordered_steps = this.steps.map( s => { if (s.attributes.position >= position) { if (action === 'add') { s.attributes.position += 1; } else { s.attributes.position -= 1; } } return s; }) if (action === 'add') { unordered_steps.push(step); } this.reorderSteps(unordered_steps) }, updateStep(attributes) { this.steps[attributes.position].attributes = { ...this.steps[attributes.position].attributes, ...attributes }; this.refreshProtocolStatus(); }, reorderSteps(steps) { this.steps = steps.sort((a, b) => a.attributes.position - b.attributes.position); this.refreshProtocolStatus(); }, updateStepOrder(orderedSteps) { orderedSteps.forEach((step, position) => { let index = this.steps.findIndex((e) => e.id === step.id); this.steps[index].attributes.position = position; }); let stepPositions = { step_positions: this.steps.map( (step) => [step.id, step.attributes.position] ) }; $.ajax({ type: "POST", url: this.protocol.attributes.urls.reorder_steps_url, data: JSON.stringify(stepPositions), contentType: "application/json", dataType: "json", error: (() => HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger')), success: (() => this.reorderSteps(this.steps)) }); }, startStepReorder() { this.reordering = true; }, closeStepReorderModal() { this.reordering = false; }, startPublish() { this.publishing = true; }, closePublishModal() { this.publishing = false; }, scrollToBottom() { window.scrollTo(0, document.body.scrollHeight); }, publishProtocol(comment) { this.protocol.attributes.version_comment = comment; $.post(this.urls.publish_url, {version_comment: comment, view: 'show'}) } } } </script>