mirror of
				https://github.com/scinote-eln/scinote-web.git
				synced 2025-10-25 13:37:12 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			808 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			808 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div ref="stepContainer" class="step-container pr-8"
 | |
|        :id="`stepContainer${step.id}`"
 | |
|        @drop.prevent="dropFile"
 | |
|        @dragenter.prevent="dragEnter($event)"
 | |
|        @dragover.prevent
 | |
|        :data-id="step.id"
 | |
|        :class="{ 'draging-file': dragingFile, 'editing-name': editingName, 'locked': !urls.update_url, 'pointer-events-none': addingContent }"
 | |
|        :data-e2e="`e2e-CO-protocol-step${step.id}`"
 | |
|   >
 | |
|     <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()" :parent="step"/>
 | |
|     </div>
 | |
|     <div class="step-header">
 | |
|       <div class="step-element-header" :class="{ 'no-hover': !urls.update_url }">
 | |
|         <div class="flex items-center gap-4 py-0.5 border-0 border-y border-transparent border-solid">
 | |
|           <a ref="toggleElement" class="step-collapse-link hover:no-underline focus:no-underline"
 | |
|             :href="'#stepBody' + step.id"
 | |
|             data-toggle="collapse"
 | |
|             data-remote="true"
 | |
|             @click="toggleCollapsed"
 | |
|             :data-e2e="`e2e-BT-protocol-step${step.id}-toggleCollapsed`">
 | |
|               <span class="sn-icon sn-icon-right "></span>
 | |
|           </a>
 | |
|           <div v-if="!inRepository" class="step-complete-container" :class="{ 'step-element--locked': !urls.state_url }">
 | |
|             <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')"
 | |
|                  :data-e2e="`e2e-BT-protocol-step${step.id}-toggleCompleted`"
 | |
|             ></div>
 | |
|           </div>
 | |
|           <div class="step-position leading-5" :data-e2e="`e2e-TX-protocol-step${step.id}-position`">
 | |
|             {{ step.attributes.position + 1 }}.
 | |
|           </div>
 | |
|         </div>
 | |
|         <div class="step-name-container basis-[calc(100%_-_100px)]" :class="{'step-element--locked': !urls.update_url}">
 | |
|           <InlineEdit
 | |
|             :value="step.attributes.name"
 | |
|             :class="{ 'step-element--locked': !urls.update_url }"
 | |
|             :characterLimit="255"
 | |
|             :allowBlank="false"
 | |
|             :attributeName="`${i18n.t('Step')} ${i18n.t('name')}`"
 | |
|             :autofocus="editingName"
 | |
|             :singleLine="false"
 | |
|             :timestamp="i18n.t('protocols.steps.timestamp', { date: step.attributes.created_at, user: step.attributes.created_by })"
 | |
|             :placeholder="i18n.t('protocols.steps.placeholder')"
 | |
|             :defaultValue="i18n.t('protocols.steps.default_name')"
 | |
|             @editingEnabled="editingName = true"
 | |
|             @editingDisabled="editingName = false"
 | |
|             :editOnload="step.newStep == true"
 | |
|             :dataE2e="`protocol-step${step.id}-title`"
 | |
|             @update="updateName"
 | |
|           />
 | |
|         </div>
 | |
|       </div>
 | |
|       <div class="elements-actions-container mt-[-5px]">
 | |
|         <input type="file" class="hidden" ref="fileSelector" @change="loadFromComputer" multiple />
 | |
| 
 | |
|         <MenuDropdown
 | |
|           :listItems="this.insertMenu"
 | |
|           :btnText="i18n.t('protocols.steps.insert.button')"
 | |
|           :position="'right'"
 | |
|           :caret="true"
 | |
|           :dataE2e="`e2e-DD-protocol-step${step.id}-insertContent`"
 | |
|           @create:custom_well_plate="openCustomWellPlateModal"
 | |
|           @create:table="(...args) => this.createElement('table', ...args)"
 | |
|           @create:checklist="createElement('checklist')"
 | |
|           @create:text="createElement('text')"
 | |
|           @create:form="openFormSelectModal = true"
 | |
|           @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="step.id"
 | |
|           ref="marvinJsButton"
 | |
|           :data-marvin-url="step.attributes.marvinjs_context?.marvin_js_asset_url"
 | |
|           :data-object-type="step.attributes.type"
 | |
|           tabindex="0"
 | |
|         ></span> <!-- Hidden element to support legacy code -->
 | |
|         <button v-if="step.attributes.results.length == 0" class="btn btn-light icon-btn" @click="this.openLinkResultsModal = true">
 | |
|           <i class="sn-icon sn-icon-results"></i>
 | |
|         </button>
 | |
|         <GeneralDropdown v-else ref="linkedResultsDropdown"  position="right">
 | |
|           <template v-slot:field>
 | |
|             <button class="btn btn-light icon-btn">
 | |
|               <i class="sn-icon sn-icon-results"></i>
 | |
|               <span class="absolute top-1 right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
 | |
|                 {{ step.attributes.results.length }}
 | |
|               </span>
 | |
|             </button>
 | |
|           </template>
 | |
|           <template v-slot:flyout>
 | |
|             <a v-for="result in step.attributes.results"
 | |
|                :key="result.id"
 | |
|                :title="result.name"
 | |
|                :href="resultUrl(result.id)"
 | |
|                class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer block hover:no-underline text-sn-blue truncate"
 | |
|             >
 | |
|               {{ result.name }}
 | |
|             </a>
 | |
|             <hr class="my-0">
 | |
|             <div class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer text-sn-blue"
 | |
|                  @click="this.openLinkResultsModal = true; $refs.linkedResultsDropdown.closeMenu()">
 | |
|               {{ i18n.t('protocols.steps.manage_links') }}
 | |
|             </div>
 | |
|           </template>
 | |
|         </GeneralDropdown>
 | |
|         <a href=" #"
 | |
|            v-if="!inRepository"
 | |
|            ref="comments"
 | |
|            class="open-comments-sidebar btn icon-btn btn-light"
 | |
|            data-turbolinks="false"
 | |
|            data-object-type="Step"
 | |
|            @click="openCommentsSidebar"
 | |
|            :data-object-id="step.id">
 | |
|           <i class="sn-icon sn-icon-comments"></i>
 | |
|           <span class="comments-counter"
 | |
|                 :id="`comment-count-${step.id}`"
 | |
|                 :class="{'unseen': step.attributes.unseen_comments, 'hidden': !step.attributes.comments_count}"
 | |
|           >
 | |
|             {{ step.attributes.comments_count }}
 | |
|           </span>
 | |
|         </a>
 | |
|         <MenuDropdown
 | |
|           :listItems="this.actionsMenu"
 | |
|           :btnClasses="'btn btn-light icon-btn'"
 | |
|           :position="'right'"
 | |
|           :btnIcon="'sn-icon sn-icon-more-hori'"
 | |
|           :dataE2e="`e2e-DD-protocol-step${step.id}-options`"
 | |
|           @reorder="openReorderModal"
 | |
|           @duplicate="duplicateStep"
 | |
|           @delete="showDeleteModal"
 | |
|         ></MenuDropdown>
 | |
|       </div>
 | |
|     </div>
 | |
|     <div class="collapse in" :id="'stepBody' + step.id">
 | |
|       <div class="step-elements">
 | |
|         <component
 | |
|           v-for="(element, index) in orderedElements"
 | |
|           :is="elements[index].attributes.orderable_type"
 | |
|           :key="element.id"
 | |
|           class="step-element"
 | |
|           :element.sync="elements[index]"
 | |
|           :inRepository="inRepository"
 | |
|           :reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
 | |
|           :assignableMyModuleId="assignableMyModuleId"
 | |
|           :isNew="element.isNew"
 | |
|           :dataE2e="`protocol-step${step.id}`"
 | |
|           @component:adding-content="($event) => addingContent = $event"
 | |
|           @component:delete="deleteElement"
 | |
|           @update="updateElement"
 | |
|           @reorder="openReorderModal"
 | |
|           @component:insert="insertElement"
 | |
|           @moved="moveElement"
 | |
|         />
 | |
|         <Attachments v-if="attachments.length"
 | |
|                     :parent="step"
 | |
|                     :attachments="attachments"
 | |
|                     :attachmentsReady="attachmentsReady"
 | |
|                     :dataE2e="`protocol-step${step.id}`"
 | |
|                     @attachments:openFileModal="showFileModal = true"
 | |
|                     @attachment:deleted="attachmentDeleted"
 | |
|                     @attachment:update="updateAttachment"
 | |
|                     @attachment:uploaded="loadAttachments"
 | |
|                     @attachment:changed="reloadAttachment"
 | |
|                     @attachments:order="changeAttachmentsOrder"
 | |
|                     @attachment:moved="moveAttachment"
 | |
|                     @attachments:viewMode="changeAttachmentsViewMode"
 | |
|                     @attachment:viewMode="updateAttachmentViewMode"/>
 | |
|       </div>
 | |
|       <ContentToolbar
 | |
|         v-if="orderedElements.length > 2 && insertMenu.length > 0"
 | |
|         :insertMenu="insertMenu"
 | |
|         @create:custom_well_plate="openCustomWellPlateModal"
 | |
|         @create:table="(...args) => this.createElement('table', ...args)"
 | |
|         @create:checklist="createElement('checklist')"
 | |
|         @create:text="createElement('text')"
 | |
|         @create:form="openFormSelectModal = true"
 | |
|         @create:file="openLoadFromComputer"
 | |
|         @create:wopi_file="openWopiFileModal"
 | |
|         @create:ove_file="openOVEditor"
 | |
|         @create:marvinjs_file="openMarvinJsModal($refs.marvinJsButton)"
 | |
|       ></ContentToolbar>
 | |
|     </div>
 | |
| 
 | |
|     <deleteStepModal v-if="confirmingDelete" @confirm="deleteStep" @cancel="closeDeleteModal"/>
 | |
|     <ReorderableItemsModal v-if="reordering"
 | |
|       :title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
 | |
|       :items="reorderableElements"
 | |
|       :dataE2e="`protocol-step${step.id}-reorder`"
 | |
|       @reorder="updateElementOrder"
 | |
|       @close="closeReorderModal"
 | |
|     />
 | |
| 
 | |
|     <CustomWellPlateModal v-if="customWellPlate"
 | |
|       @cancel="closeCustomWellPlateModal"
 | |
|       @create:table="(...args) => this.createElement('table', ...args)"
 | |
|     />
 | |
| 
 | |
|     <SelectFormModal
 | |
|       v-if="openFormSelectModal"
 | |
|       @close="openFormSelectModal = false"
 | |
|       @submit="createElement('form_response', null, null, $event); openFormSelectModal = false"
 | |
|     />
 | |
|     <LinkResultsModal
 | |
|       v-if="openLinkResultsModal"
 | |
|       :step="step"
 | |
|       @updateStep="updateLinkedResults"
 | |
|       @close="openLinkResultsModal = false"
 | |
|     />
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
|  <script>
 | |
|   const ICON_MAP = {
 | |
|     'Checklist': 'fa-list-ul',
 | |
|     'StepText': 'fa-font',
 | |
|     'StepTable': 'fa-table'
 | |
|   }
 | |
| 
 | |
|   import InlineEdit from '../shared/inline_edit.vue'
 | |
|   import StepTable from '../shared/content/table.vue'
 | |
|   import StepText from '../shared/content/text.vue'
 | |
|   import Checklist from '../shared/content/checklist.vue'
 | |
|   import FormResponse from '../shared/content/form_response.vue'
 | |
|   import deleteStepModal from './modals/delete_step.vue'
 | |
|   import LinkResultsModal from './modals/link_results.vue'
 | |
|   import Attachments from '../shared/content/attachments.vue'
 | |
|   import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
 | |
|   import MenuDropdown from '../shared/menu_dropdown.vue'
 | |
|   import GeneralDropdown from '../shared/general_dropdown.vue'
 | |
|   import ContentToolbar from '../shared/content/content_toolbar.vue'
 | |
|   import CustomWellPlateModal from '../shared/content/modal/custom_well_plate_modal.vue'
 | |
| 
 | |
|   import UtilsMixin from '../mixins/utils.js'
 | |
|   import AttachmentsMixin from '../shared/content/mixins/attachments.js'
 | |
|   import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js'
 | |
|   import SelectFormModal from '../shared/content/modal/form_select.vue'
 | |
|   import OveMixin from '../shared/content/attachments/mixins/ove.js'
 | |
|   import StorageUsage from '../shared/content/attachments/storage_usage.vue'
 | |
|   import axios from '../../packs/custom_axios';
 | |
| 
 | |
|   import {
 | |
|     my_module_results_path,
 | |
|   } from '../../routes.js';
 | |
| 
 | |
|   export default {
 | |
|     name: 'StepContainer',
 | |
|     props: {
 | |
|       step: {
 | |
|         type: Object,
 | |
|         required: true
 | |
|       },
 | |
|       inRepository: {
 | |
|         type: Boolean,
 | |
|         required: true
 | |
|       },
 | |
|       reorderStepUrl: {
 | |
|         required: false
 | |
|       },
 | |
|       userSettingsUrl: {
 | |
|         required: false
 | |
|       },
 | |
|       assignableMyModuleId: {
 | |
|         type: Number,
 | |
|         required: false
 | |
|       },
 | |
|       stepToReload: {
 | |
|         type: Number,
 | |
|         required: false
 | |
|       },
 | |
|       activeDragStep: {
 | |
|         required: false
 | |
|       }
 | |
|     },
 | |
|     data() {
 | |
|       return {
 | |
|         elements: [],
 | |
|         attachments: [],
 | |
|         attachmentsReady: false,
 | |
|         confirmingDelete: false,
 | |
|         addingContent: false,
 | |
|         showFileModal: false,
 | |
|         showCommentsSidebar: false,
 | |
|         dragingFile: false,
 | |
|         reordering: false,
 | |
|         isCollapsed: false,
 | |
|         editingName: false,
 | |
|         inlineEditError: null,
 | |
|         customWellPlate: false,
 | |
|         openFormSelectModal: false,
 | |
|         openLinkResultsModal: false,
 | |
|         wellPlateOptions: [
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.custom'),
 | |
|             emit: 'create:custom_well_plate',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-customWellPlate` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.32_x_48'),
 | |
|             emit: 'create:table',
 | |
|             params: [32, 48],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-32` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'),
 | |
|             emit: 'create:table',
 | |
|             params: [16, 24],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-16` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'),
 | |
|             emit: 'create:table',
 | |
|             params: [8, 12],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-8` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'),
 | |
|             emit: 'create:table',
 | |
|             params: [6, 8],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-6` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'),
 | |
|             emit: 'create:table',
 | |
|             params: [4, 6],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-4` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'),
 | |
|             emit: 'create:table',
 | |
|             params: [3, 4],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-3` },
 | |
|           { text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'),
 | |
|             emit: 'create:table',
 | |
|             params: [2, 3],
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellPlate-2` }
 | |
|         ]
 | |
|       }
 | |
|     },
 | |
|     mixins: [UtilsMixin, AttachmentsMixin, WopiFileModal, OveMixin],
 | |
|     components: {
 | |
|       InlineEdit,
 | |
|       StepTable,
 | |
|       StepText,
 | |
|       Checklist,
 | |
|       deleteStepModal,
 | |
|       Attachments,
 | |
|       StorageUsage,
 | |
|       ReorderableItemsModal,
 | |
|       MenuDropdown,
 | |
|       ContentToolbar,
 | |
|       CustomWellPlateModal,
 | |
|       SelectFormModal,
 | |
|       FormResponse,
 | |
|       LinkResultsModal,
 | |
|       GeneralDropdown
 | |
|     },
 | |
|     created() {
 | |
|       this.loadAttachments();
 | |
|       this.loadElements();
 | |
|     },
 | |
|     watch: {
 | |
|       stepToReload() {
 | |
|         if (this.stepToReload == this.step.id) {
 | |
|           this.loadElements();
 | |
|           this.loadAttachments();
 | |
|         }
 | |
|       },
 | |
|       activeDragStep() {
 | |
|         if (this.activeDragStep != this.step.id && this.dragingFile) {
 | |
|           this.dragingFile = false;
 | |
|         }
 | |
|       },
 | |
|       step: {
 | |
|         handler(newVal) {
 | |
|           if (this.isCollapsed !== newVal.attributes.collapsed) {
 | |
|             this.toggleCollapsed();
 | |
|           }
 | |
|         },
 | |
|         deep: true
 | |
|       }
 | |
|     },
 | |
|     mounted() {
 | |
|       this.$nextTick(() => {
 | |
|         const stepId = `#stepBody${this.step.id}`;
 | |
|         this.isCollapsed = this.step.attributes.collapsed;
 | |
|         if (this.isCollapsed) {
 | |
|           $(stepId).collapse('hide');
 | |
|         } else {
 | |
|           $(stepId).collapse('show');
 | |
|         }
 | |
|         this.$emit('step:collapsed');
 | |
|       });
 | |
|       $(this.$refs.comments).data('closeCallback', this.closeCommentsSidebar);
 | |
|       $(this.$refs.comments).data('openCallback', this.closeCommentsSidebar);
 | |
|       $(this.$refs.actionsDropdownButton).on('shown.bs.dropdown hidden.bs.dropdown', () => {
 | |
|         this.handleDropdownPosition(this.$refs.actionsDropdownButton, this.$refs.actionsDropdown)
 | |
|       });
 | |
|       $(this.$refs.elementsDropdownButton).on('shown.bs.dropdown hidden.bs.dropdown', () => {
 | |
|         this.handleDropdownPosition(this.$refs.elementsDropdownButton, this.$refs.elementsDropdown)
 | |
|       });
 | |
|     },
 | |
|     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 || {}
 | |
|       },
 | |
|       filesMenu() {
 | |
|         let menu = [];
 | |
|         if (this.urls.upload_attachment_url) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('protocols.steps.insert.add_file'),
 | |
|             emit: 'create:file',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-file`
 | |
|           }]);
 | |
|         }
 | |
|         if (this.step.attributes.wopi_enabled) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('assets.create_wopi_file.button_text'),
 | |
|             emit: 'create:wopi_file',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-wopi`
 | |
|           }]);
 | |
|         }
 | |
|         if (this.step.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',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-sequence`
 | |
|           }]);
 | |
|         }
 | |
|         if (this.step.attributes.marvinjs_enabled) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('marvinjs.new_button'),
 | |
|             emit: 'create:marvinjs_file',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment-chemicalDrawing`
 | |
|           }]);
 | |
|         }
 | |
|         return menu;
 | |
|       },
 | |
|       insertMenu() {
 | |
|         let menu = [];
 | |
|         if (this.urls.update_url) {
 | |
|           menu = menu.concat([{
 | |
|                     text: this.i18n.t('protocols.steps.insert.text'),
 | |
|                     icon: 'sn-icon sn-icon-result-text',
 | |
|                     emit: 'create:text',
 | |
|                     data_e2e: `e2e-BT-protocol-step${this.step.id}-insertText`
 | |
|                   },{
 | |
|                     text: this.i18n.t('protocols.steps.insert.attachment'),
 | |
|                     submenu: this.filesMenu,
 | |
|                     position: 'left',
 | |
|                     icon: 'sn-icon sn-icon-file',
 | |
|                     data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment`
 | |
|                   },{
 | |
|                     text: this.i18n.t('protocols.steps.insert.table'),
 | |
|                     icon: 'sn-icon sn-icon-tables',
 | |
|                     emit: 'create:table',
 | |
|                     data_e2e: `e2e-BT-protocol-step${this.step.id}-insertTable`
 | |
|                   },{
 | |
|                     text: this.i18n.t('protocols.steps.insert.well_plate'),
 | |
|                     submenu: this.wellPlateOptions,
 | |
|                     icon: 'sn-icon sn-icon-tables',
 | |
|                     position: 'left',
 | |
|                     data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellplate`
 | |
|                   },{
 | |
|                     text: this.i18n.t('protocols.steps.insert.checklist'),
 | |
|                     emit: 'create:checklist',
 | |
|                     icon: 'sn-icon sn-icon-checkllist',
 | |
|                     data_e2e: `e2e-BT-protocol-step${this.step.id}-insertChecklist`
 | |
|                   }]);
 | |
|         }
 | |
| 
 | |
|         if (this.urls.create_form_response_url) {
 | |
|           menu = menu.concat([{
 | |
|                    text: this.i18n.t('protocols.steps.insert.form'),
 | |
|                    emit: 'create:form',
 | |
|                    icon: 'sn-icon sn-icon-forms',
 | |
|                    data_e2e: `e2e-BT-protocol-step${this.step.id}-insertForm`
 | |
|                  }]);
 | |
|         }
 | |
| 
 | |
|         return menu;
 | |
|       },
 | |
|       actionsMenu() {
 | |
|         let menu = [];
 | |
|         if (this.urls.reorder_elements_url && this.elements.length > 1) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('protocols.steps.options_dropdown.rearrange'),
 | |
|             emit: 'reorder',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-rearrange`
 | |
|           }]);
 | |
|         }
 | |
|         if (this.urls.duplicate_step_url) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('protocols.steps.options_dropdown.duplicate'),
 | |
|             emit: 'duplicate',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-duplicate`
 | |
|           }]);
 | |
|         }
 | |
|         if (this.urls.delete_url) {
 | |
|           menu = menu.concat([{
 | |
|             text: this.i18n.t('protocols.steps.options_dropdown.delete'),
 | |
|             emit: 'delete',
 | |
|             data_e2e: `e2e-BT-protocol-step${this.step.id}-stepOptions-delete`
 | |
|           }]);
 | |
|         }
 | |
|         return menu;
 | |
|       }
 | |
|     },
 | |
|     methods: {
 | |
|       dragEnter(e) {
 | |
|         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;
 | |
|           this.$emit('step:drag_enter', this.step.id);
 | |
|         }
 | |
|       },
 | |
|       loadAttachments() {
 | |
|         this.attachmentsReady = false
 | |
| 
 | |
|         $.get(this.urls.attachments_url, (result) => {
 | |
|           this.attachments = result.data
 | |
|           this.$emit('step:attachments:loaded');
 | |
|           if (this.attachments.findIndex((e) => e.attributes.attached === false) >= 0) {
 | |
|             setTimeout(() => {
 | |
|               this.loadAttachments()
 | |
|             }, 10000)
 | |
|           } else {
 | |
|             this.attachmentsReady = true
 | |
|           }
 | |
|         });
 | |
|         this.showFileModal = false;
 | |
|       },
 | |
|       reloadAttachment(attachmentId) {
 | |
|         const index = this.attachments.findIndex(attachment => attachment.id === attachmentId);
 | |
|         const attachmentUrl = this.attachments[index].attributes.urls.asset_show
 | |
| 
 | |
|         axios.get(attachmentUrl)
 | |
|           .then((response) => {
 | |
|             const updatedAttachment = response.data.data;
 | |
|             const index = this.attachments.findIndex(attachment => attachment.id === attachmentId);
 | |
| 
 | |
|             if (index !== -1) {
 | |
|               this.attachments[index] = updatedAttachment;
 | |
|             }
 | |
|           })
 | |
|           .catch((error) => {
 | |
|             console.error("Failed to reload attachment:", error);
 | |
|           });
 | |
| 
 | |
|         this.showFileModal = false;
 | |
|       },
 | |
|       loadElements() {
 | |
|         $.get(this.urls.elements_url, (result) => {
 | |
|           this.elements = result.data
 | |
|           this.$emit('step:elements:loaded');
 | |
|         });
 | |
|       },
 | |
|       showStorageUsage() {
 | |
|         return (this.elements.length || this.attachments.length) && !this.isCollapsed && this.step.attributes.storage_limit;
 | |
|       },
 | |
|       toggleCollapsed() {
 | |
|         this.isCollapsed = !this.isCollapsed;
 | |
| 
 | |
|         this.step.attributes.collapsed = this.isCollapsed;
 | |
| 
 | |
|         const settings = {
 | |
|           key: 'task_step_states',
 | |
|           data: { [this.step.id]: this.isCollapsed }
 | |
|         };
 | |
| 
 | |
|         this.$emit('step:collapsed');
 | |
|         axios.put(this.userSettingsUrl, { settings: [settings] });
 | |
|       },
 | |
|       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'
 | |
|             );
 | |
|           }
 | |
|         });
 | |
|       },
 | |
|       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}).fail(() => {
 | |
|           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');
 | |
|         })
 | |
|       },
 | |
|       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);
 | |
| 
 | |
|         if (!this.elements[index]) return;
 | |
| 
 | |
|         this.elements[index].isNew = false;
 | |
| 
 | |
|         if (skipRequest || !element.attributes.orderable?.urls?.update_url) {
 | |
|           this.elements[index].attributes.orderable = element.attributes.orderable;
 | |
|           this.$emit('stepUpdated');
 | |
|         } 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('stepUpdated');
 | |
| 
 | |
|               // optional callback after successful update
 | |
|               if(typeof callback === 'function') {
 | |
|                 callback();
 | |
|               }
 | |
|             }
 | |
|           }).fail(() => {
 | |
|             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'))
 | |
|         });
 | |
|       },
 | |
|       updateName(newName) {
 | |
|         $.ajax({
 | |
|           url: this.urls.update_url,
 | |
|           type: 'PATCH',
 | |
|           data: {step: {name: newName}},
 | |
|           success: (result) => {
 | |
|             this.$emit('step:update', {
 | |
|               name: result.data.attributes.name,
 | |
|               position: this.step.attributes.position
 | |
|             })
 | |
|           },
 | |
|           error: (data) => {
 | |
|             HelperModule.flashAlertMsg(data.responseJSON.errors ? Object.values(data.responseJSON.errors).join(', ') : I18n.t('errors.general'), 'danger');
 | |
|           }
 | |
|         });
 | |
|       },
 | |
|       createElement(elementType, tableDimensions = null, name = '', formId = null) {
 | |
|         let plateTemplate = tableDimensions != null;
 | |
|         tableDimensions ||= [5, 5];
 | |
|         $.post(this.urls[`create_${elementType}_url`], { tableDimensions: tableDimensions, plateTemplate: plateTemplate, name: name, form_id: formId }, (result) => {
 | |
|           result.data.isNew = true;
 | |
|           this.elements.push(result.data)
 | |
| 
 | |
|           if (this.isCollapsed) {
 | |
|             this.$refs.toggleElement.click();
 | |
|           }
 | |
|           this.$emit('stepUpdated')
 | |
|         }).fail(() => {
 | |
|           HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
 | |
|         }).done(() => {
 | |
|           this.$parent.$nextTick(() => {
 | |
|             const children = this.$refs.stepContainer.querySelectorAll(".step-element");
 | |
|             const lastChild = children[children.length - 1];
 | |
| 
 | |
|             lastChild.scrollIntoView(false)
 | |
|             window.scrollBy({
 | |
|               top: 200,
 | |
|               behavior: 'smooth'
 | |
|             });
 | |
|           })
 | |
|         });
 | |
|       },
 | |
|       openCustomWellPlateModal() {
 | |
|         this.customWellPlate = true;
 | |
|       },
 | |
|       closeCustomWellPlateModal() {
 | |
|         this.customWellPlate = false;
 | |
|       },
 | |
|       openCommentsSidebar() {
 | |
|         $('.comments-sidebar .close-btn').click();
 | |
|         this.showCommentsSidebar = true
 | |
|       },
 | |
|       attachmentDeleted(id) {
 | |
|         this.attachments = this.attachments.filter((a) => a.id !== id );
 | |
|         this.$emit('stepUpdated');
 | |
|       },
 | |
|       updateAttachment(attachment) {
 | |
|         const index = this.attachments.findIndex(a => a.id === attachment.id);
 | |
|         if (index !== -1) {
 | |
|           this.attachments[index] = attachment;
 | |
|           this.$emit('stepUpdated');
 | |
|         }
 | |
|       },
 | |
|       closeCommentsSidebar() {
 | |
|         this.showCommentsSidebar = false
 | |
|       },
 | |
|       openReorderModal() {
 | |
|         this.reordering = true;
 | |
|       },
 | |
|       closeReorderModal() {
 | |
|         this.reordering = false;
 | |
|       },
 | |
|       handleDropdownPosition(refButton, refDropdown) {
 | |
|         refButton.classList.toggle("dropup", !this.isInViewport(refDropdown));
 | |
|       },
 | |
|       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())
 | |
|           );
 | |
|       },
 | |
|       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);
 | |
|         this.$emit('stepUpdated');
 | |
|       },
 | |
|       moveElement(position, target_id) {
 | |
|         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')
 | |
|         this.$emit('step:move_element', target_id)
 | |
|       },
 | |
|       moveAttachment(id, target_id) {
 | |
|         this.attachments = this.attachments.filter((a) => a.id !== id );
 | |
|         this.$emit('stepUpdated')
 | |
|         this.$emit('step:move_attachment', target_id)
 | |
|       },
 | |
|       duplicateStep() {
 | |
|         $.post(this.urls.duplicate_step_url, (result) => {
 | |
|           this.$emit('step:insert', result.data);
 | |
|           HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.step_duplicated'), 'success');
 | |
|         }).fail(() => {
 | |
|           HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.step_duplication_failed'), 'danger');
 | |
|         });
 | |
|       },
 | |
|       updateLinkedResults(results) {
 | |
|         this.$emit('step:update', {
 | |
|           results: results,
 | |
|           position: this.step.attributes.position
 | |
|         })
 | |
|       },
 | |
|       resultUrl(result_id) {
 | |
|         return my_module_results_path({my_module_id: this.step.attributes.my_module_id, result_id: result_id})
 | |
|       },
 | |
|     }
 | |
|   }
 | |
| </script>
 |