mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-14 17:14:54 +08:00
Update inventory import front-end
This commit is contained in:
parent
4471ad40dc
commit
748292034b
11 changed files with 513 additions and 677 deletions
|
@ -1,7 +1,7 @@
|
|||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import 'vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.css';
|
||||
import ImportRepositoryModal from '../../vue/repositories/modals/import.vue';
|
||||
import ImportRepositoryModal from '../../vue/repositories/modals/import/container.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({});
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<InfoModal ref="modal"
|
||||
:startHidden="true"
|
||||
:infoParams="infoParams"
|
||||
:title="steps[activeStep].title"
|
||||
:subtitle="steps[activeStep].subtitle"
|
||||
:helpText="steps[activeStep].helpText"
|
||||
>
|
||||
<component
|
||||
:key="steps[activeStep].id"
|
||||
:is="steps[activeStep].component"
|
||||
@step:next="proceedToNextStep"
|
||||
@step:back="goBackToPrevStep"
|
||||
:stepProps="steps[activeStep].stepData"
|
||||
/>
|
||||
</InfoModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import { shallowRef } from 'vue';
|
||||
import InfoModal from '../../shared/info_modal.vue';
|
||||
import FirstStep from './import/first_step.vue';
|
||||
import SecondStep from './import/second_step.vue';
|
||||
|
||||
export default {
|
||||
name: 'ImportRepositoryModal',
|
||||
components: { InfoModal, FirstStep, SecondStep },
|
||||
props: {
|
||||
repositoryUrl: String,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeStep: 0,
|
||||
repositoryData: null,
|
||||
steps: [
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.steps.step1.id'),
|
||||
icon: this.i18n.t('repositories.import_records.steps.step1.icon'),
|
||||
label: this.i18n.t('repositories.import_records.steps.step1.label'),
|
||||
title: this.i18n.t('repositories.import_records.steps.step1.title'),
|
||||
subtitle: this.i18n.t('repositories.import_records.steps.step1.subtitle'),
|
||||
helpText: this.i18n.t('repositories.import_records.steps.step1.helpText'),
|
||||
component: shallowRef(FirstStep),
|
||||
stepData: null
|
||||
},
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.steps.step2.id'),
|
||||
icon: this.i18n.t('repositories.import_records.steps.step2.icon'),
|
||||
label: this.i18n.t('repositories.import_records.steps.step2.label'),
|
||||
title: this.i18n.t('repositories.import_records.steps.step2.title'),
|
||||
subtitle: this.i18n.t('repositories.import_records.steps.step2.subtitle'),
|
||||
component: shallowRef(SecondStep),
|
||||
stepData: null
|
||||
}
|
||||
],
|
||||
infoParams: {
|
||||
title: this.i18n.t('repositories.import_records.info_sidebar.title'),
|
||||
elements: [
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.id'),
|
||||
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.icon'),
|
||||
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.label'),
|
||||
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.subtext')
|
||||
},
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.id'),
|
||||
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.icon'),
|
||||
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.label'),
|
||||
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.subtext')
|
||||
},
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.id'),
|
||||
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.icon'),
|
||||
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.label'),
|
||||
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.subtext')
|
||||
},
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.id'),
|
||||
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.icon'),
|
||||
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.label'),
|
||||
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.subtext')
|
||||
},
|
||||
{
|
||||
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.id'),
|
||||
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.icon'),
|
||||
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.label'),
|
||||
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.subtext'),
|
||||
linkTo: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.importRepositoryModalComponent = this;
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.modal.open();
|
||||
},
|
||||
proceedToNextStep(data) {
|
||||
this.steps[this.activeStep + 1].stepData = data;
|
||||
this.activeStep += 1;
|
||||
},
|
||||
goBackToPrevStep() {
|
||||
this.activeStep -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
87
app/javascript/vue/repositories/modals/import/container.vue
Normal file
87
app/javascript/vue/repositories/modals/import/container.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<div v-if="modalOpened">
|
||||
<component
|
||||
:is="activeStep"
|
||||
:params="params"
|
||||
:uploading="uploading"
|
||||
@uploadFile="uploadFile"
|
||||
@generatePreview="generatePreview"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import InfoModal from '../../../shared/info_modal.vue';
|
||||
import UploadStep from './upload_step.vue';
|
||||
import MappingStep from './mapping_step.vue';
|
||||
|
||||
export default {
|
||||
name: 'ImportRepositoryModal',
|
||||
components: { InfoModal, UploadStep, MappingStep },
|
||||
props: {
|
||||
repositoryUrl: String,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalOpened: false,
|
||||
activeStep: 'UploadStep',
|
||||
uploading: false,
|
||||
params: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
window.importRepositoryModalComponent = this;
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.activeStep = 'UploadStep';
|
||||
this.fetchRepository();
|
||||
},
|
||||
fetchRepository() {
|
||||
axios.get(this.repositoryUrl)
|
||||
.then((response) => {
|
||||
this.params = response.data.data;
|
||||
this.modalOpened = true;
|
||||
});
|
||||
},
|
||||
uploadFile(file) {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
|
||||
// required payload
|
||||
formData.append('file', file);
|
||||
|
||||
axios.post(this.params.attributes.urls.parse_sheet, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
.then((response) => {
|
||||
this.params = { ...this.params, ...response.data, file_name: file.name };
|
||||
this.activeStep = 'MappingStep';
|
||||
this.uploading = false;
|
||||
});
|
||||
},
|
||||
|
||||
generatePreview(mappings, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.mapping = mappings;
|
||||
this.params.updateWithEmptyCells = updateWithEmptyCells;
|
||||
this.params.onlyAddNewItems = onlyAddNewItems;
|
||||
this.importRecords(true);
|
||||
},
|
||||
|
||||
importRecords(preview = false) {
|
||||
const jsonData = {
|
||||
file_id: this.params.temp_file.id,
|
||||
mappings: this.params.mapping,
|
||||
id: this.params.id,
|
||||
preview: preview,
|
||||
should_overwrite_with_empty_cells: this.params.updateWithEmptyCells,
|
||||
can_edit_existing_items: !this.params.onlyAddNewItems
|
||||
};
|
||||
axios.post(this.params.attributes.urls.import_records, jsonData);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,190 +0,0 @@
|
|||
<template>
|
||||
<div ref="firstStep" class="flex flex-col gap-6 h-full">
|
||||
|
||||
<!-- body -->
|
||||
<div class="flex flex-col gap-6 h-full w-full">
|
||||
|
||||
<!-- export -->
|
||||
<div id="export-section" class="flex flex-col gap-3">
|
||||
<h3 class="my-0 text-sn-dark-grey">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.importTitle') }}
|
||||
</h3>
|
||||
<div id="export-buttons" class="flex flex-row gap-4">
|
||||
<button class="btn btn-secondary btn-sm" @click="exportFullInventory">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportFullInvBtnText') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportEmptyInvBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- import -->
|
||||
<div id="import-section" class="flex flex-col gap-3 h-full w-full">
|
||||
<h3 class="my-0 text-sn-dark-grey">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.importBtnText') }}
|
||||
</h3>
|
||||
<DragAndDropUpload
|
||||
@file:dropped="uploadFile"
|
||||
@file:error="handleError"
|
||||
@file:error:clear="this.error = null"
|
||||
:supportingText="`${i18n.t('repositories.import_records.steps.step1.dragAndDropSupportingText')}`"
|
||||
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- divider -->
|
||||
<div class="sci-divider"></div>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="flex justify-end">
|
||||
<div v-if="error" class="flex flex-row gap-2 my-auto mr-auto text-sn-delete-red">
|
||||
<i class="sn-icon sn-icon-alert-warning"></i>
|
||||
<div class="my-auto">{{ error }}</div>
|
||||
</div>
|
||||
<div v-if="exportInventoryMessage" class="flex flex-row gap-2 my-auto mr-auto text-sn-alert-green">
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" data-dismiss="modal" aria-label="Close">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.cancelBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
|
||||
|
||||
export default {
|
||||
name: 'FirstStep',
|
||||
emits: ['step:next', 'info:hide'],
|
||||
components: {
|
||||
DragAndDropUpload
|
||||
},
|
||||
props: {
|
||||
stepProps: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showingInfo: false,
|
||||
error: null,
|
||||
teamId: null,
|
||||
parseSheetUrl: null,
|
||||
exportInventoryMessage: null
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
// Fetch repository data and set it to state
|
||||
const repositoryData = await this.fetchSerializedRepositoryData();
|
||||
this.teamId = String(repositoryData.data.attributes.team_id);
|
||||
this.parseSheetUrl = repositoryData.data.attributes.urls.parse_sheet;
|
||||
},
|
||||
watch: {
|
||||
// clearing export message
|
||||
exportInventoryMessage(newVal, oldVal) {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
setTimeout(() => {
|
||||
this.exportInventoryMessage = null;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchSerializedRepositoryData() {
|
||||
const url = window.location.pathname;
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
async exportFullInventory() {
|
||||
const exportFullInventoryUrl = `/teams/${this.teamId}/export_repositories`;
|
||||
const formData = new FormData();
|
||||
const repositoryIds = [this.teamId];
|
||||
const fileType = 'csv';
|
||||
|
||||
// required payload
|
||||
formData.append('repository_ids', repositoryIds);
|
||||
formData.append('file_type', fileType);
|
||||
|
||||
try {
|
||||
const response = await axios.post(exportFullInventoryUrl, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
|
||||
if (!response.status === 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
this.exportInventoryMessage = response.data.message;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
async uploadFile(file) {
|
||||
this.uploading = true;
|
||||
|
||||
// First, parse the sheet
|
||||
const parsedSheetResponse = await this.parseSheet(file);
|
||||
|
||||
// If parsed successfully, go to next step and pass through the necessary data
|
||||
if (parsedSheetResponse) {
|
||||
const {
|
||||
header: columnNames,
|
||||
available_fields: availableFields,
|
||||
columns: exampleData
|
||||
} = parsedSheetResponse.data.import_data;
|
||||
const fileName = file.name;
|
||||
const tempFile = parsedSheetResponse.data.temp_file;
|
||||
|
||||
this.$emit('step:next', {
|
||||
columnNames,
|
||||
availableFields,
|
||||
exampleData,
|
||||
fileName,
|
||||
tempFile
|
||||
});
|
||||
}
|
||||
},
|
||||
async parseSheet(file) {
|
||||
const formData = new FormData();
|
||||
|
||||
// required payload
|
||||
formData.append('file', file);
|
||||
formData.append('team_id', this.teamId);
|
||||
|
||||
try {
|
||||
const response = await axios.post(this.parseSheetUrl, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
|
||||
if (!response.status === 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
handleError(error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
261
app/javascript/vue/repositories/modals/import/mapping_step.vue
Normal file
261
app/javascript/vue/repositories/modals/import/mapping_step.vue
Normal file
|
@ -0,0 +1,261 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-sn-dark-grey">
|
||||
{{ this.i18n.t('repositories.import_records.steps.step2.subtitle') }}
|
||||
</p>
|
||||
<div class="flex gap-6 items-center my-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="autoMapping" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input type="checkbox" class="sci-checkbox" :checked="updateWithEmptyCells" @change="toggleUpdateWithEmptyCells"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.updateEmptyCellsText') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input type="checkbox" class="sci-checkbox" :checked="onlyAddNewItems" @change="toggleOnlyAddNewItems" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.onlyAddNewItemsText') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
|
||||
<hr class="m-0 mt-6">
|
||||
<div class="grid grid-cols-[3rem_auto_1.5rem_auto_5rem_auto] px-2">
|
||||
|
||||
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2">{{ column }}</div>
|
||||
|
||||
<template v-for="(item, index) in params.import_data.header" :key="item">
|
||||
<MappingStepTableRow
|
||||
:index="index"
|
||||
:item="item"
|
||||
:dropdownOptions="computedDropdownOptions"
|
||||
:params="params"
|
||||
:selected="this.selectedItemsIndexes.includes(index)"
|
||||
@selection:changed="handleChange"
|
||||
:availableFields="this.availableFields"
|
||||
:autoMapping="this.autoMapping"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- imported/ignored section -->
|
||||
<div class="flex gap-1 mt-6"
|
||||
v-html="i18n.t('repositories.import_records.steps.step2.importedIgnoredSection', {
|
||||
imported: computedImportedIgnoredInfo.importedSum,
|
||||
ignored: computedImportedIgnoredInfo.ignoredSum
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="modal-footer">
|
||||
<div id="error" class="flex flex-row gap-3 text-sn-delete-red">
|
||||
<i v-if="error" class="sn-icon sn-icon-alert-warning my-auto"></i>
|
||||
<div class="my-auto">{{ error ? error : '' }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary ml-auto" @click="close" aria-label="Close">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.cancelBtnText') }}
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="!rowsIsValid" @click="importRecords">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.confirmBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
import MappingStepTableRow from './mapping_step_table_row.vue';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'MappingStep',
|
||||
emits: ['step:next'],
|
||||
mixins: [modalMixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
MappingStepTableRow
|
||||
},
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
autoMapping: true,
|
||||
updateWithEmptyCells: false,
|
||||
onlyAddNewItems: false,
|
||||
columnLabels: {
|
||||
0: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.number'),
|
||||
1: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.importedColumns'),
|
||||
2: '',
|
||||
3: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.scinoteColumns'),
|
||||
4: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.status'),
|
||||
5: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.exampleData')
|
||||
},
|
||||
selectedItems: [],
|
||||
selectedItemsIndexes: [],
|
||||
importRecordsUrl: null,
|
||||
teamId: null,
|
||||
repositoryId: null,
|
||||
availableFields: [],
|
||||
alwaysAvailableFields: [],
|
||||
repositoryColumns: null,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleUpdateWithEmptyCells() {
|
||||
this.updateWithEmptyCells = !this.updateWithEmptyCells;
|
||||
},
|
||||
toggleOnlyAddNewItems() {
|
||||
this.onlyAddNewItems = !this.onlyAddNewItems;
|
||||
},
|
||||
handleChange(payload) {
|
||||
this.error = null;
|
||||
const { index, key, value } = payload;
|
||||
|
||||
// checking if the mapping is already selected
|
||||
const foundItem = this.selectedItems.find((item) => item.index === index);
|
||||
|
||||
// if it's not, add it
|
||||
if (!foundItem && key) {
|
||||
this.selectedItems = [...this.selectedItems, { index, key, value }];
|
||||
this.selectedItemsIndexes.push(index);
|
||||
}
|
||||
// if it is but the key is null then clear it
|
||||
if (foundItem && !key) {
|
||||
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
|
||||
const indexToRemoveStr = this.selectedItemsIndexes.indexOf(index);
|
||||
if ((indexToRemoveObj !== -1) && (indexToRemoveStr !== -1)) {
|
||||
this.selectedItems.splice(indexToRemoveObj, 1);
|
||||
this.selectedItemsIndexes.splice(indexToRemoveStr, 1);
|
||||
}
|
||||
}
|
||||
// if it is and the key is not null then update it
|
||||
if (foundItem && key) {
|
||||
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
|
||||
this.selectedItems.splice(indexToRemoveObj, 1);
|
||||
this.selectedItems = [...this.selectedItems, { index, key, value }];
|
||||
}
|
||||
|
||||
this.updateAvailableItemsStatus();
|
||||
},
|
||||
// necessary for tracking which options are already selected
|
||||
updateAvailableItemsStatus() {
|
||||
let updatedAvailableFields = [];
|
||||
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
|
||||
|
||||
this.alwaysAvailableFields.forEach((field) => {
|
||||
if (selectedItemsKeys.has(field.key)) {
|
||||
const tempObj = { key: field.key, value: field.value, alreadySelected: true };
|
||||
updatedAvailableFields.push(tempObj);
|
||||
} else {
|
||||
updatedAvailableFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
this.availableFields = updatedAvailableFields;
|
||||
updatedAvailableFields = [];
|
||||
},
|
||||
generateMapping() {
|
||||
const mapping = {};
|
||||
for (let i = 0; i < this.params.import_data.header.length; i++) {
|
||||
const foundItem = this.selectedItems.find((item) => item.index === i);
|
||||
if (foundItem) {
|
||||
mapping[foundItem.index] = (foundItem.key === 'new' ? foundItem.value : foundItem.key);
|
||||
} else {
|
||||
mapping[i] = '';
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
},
|
||||
importRecords() {
|
||||
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
|
||||
if (!selectedItemsKeys.has('-1')) {
|
||||
this.error = this.i18n.t('repositories.import_records.steps.step2.selectNamePropertyError');
|
||||
return '';
|
||||
}
|
||||
|
||||
this.$emit(
|
||||
'generatePreview',
|
||||
this.generateMapping(),
|
||||
this.updateWithEmptyCells,
|
||||
this.onlyAddNewItems
|
||||
);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedDropdownOptions() {
|
||||
const columnKeyToLabelMapping = {};
|
||||
columnKeyToLabelMapping[-1] = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
|
||||
|
||||
if (this.repositoryColumns) {
|
||||
this.repositoryColumns.forEach((el) => {
|
||||
const [key, colName, colType] = el;
|
||||
columnKeyToLabelMapping[key] = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${colType}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.availableFields) {
|
||||
let options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${columnKeyToLabelMapping[el.key]})`]);
|
||||
options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
|
||||
return options;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
computedImportedIgnoredInfo() {
|
||||
const importedSum = this.selectedItems.length;
|
||||
const ignoredSum = this.params.import_data.header.length - importedSum;
|
||||
return { importedSum, ignoredSum };
|
||||
},
|
||||
rowsIsValid() {
|
||||
let valid = true;
|
||||
this.selectedItems.forEach((v) => {
|
||||
if (v.key === 'new' && (!v.value.type || v.value.name.length < 2)) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.repositoryColumns = this.params.attributes.repository_columns;
|
||||
|
||||
// Adding alreadySelected attribute for tracking
|
||||
const tempAvailableFields = [];
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
const field = { key, value, alreadySelected: false };
|
||||
tempAvailableFields.push(field);
|
||||
});
|
||||
this.availableFields = tempAvailableFields;
|
||||
this.alwaysAvailableFields = tempAvailableFields;
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,21 +1,28 @@
|
|||
<template>
|
||||
<!-- columns -->
|
||||
<div class="flex flex-row justify-between gap-6">
|
||||
<!-- number col -->
|
||||
<div class="w-6 h-10 flex items-center">{{ index + 1 }}</div>
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ index + 1 }}</div>
|
||||
|
||||
<div class="w-40 h-10 flex items-center truncate" :title="item">{{ item }}</div>
|
||||
<div class="py-1 truncate min-h-12 px-2 flex items-center" :title="item" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ item }}</div>
|
||||
|
||||
<i class="sn-icon sn-icon-arrow-right w-6 h-10 flex items-center relative left-5"></i>
|
||||
<div class="py-1 min-h-12 flex items-center justify-center text-sn-grey" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
<i class="sn-icon sn-icon-arrow-right text-sn-gray"></i>
|
||||
</div>
|
||||
|
||||
<div class="w-60 min-h-10 flex items-center flex-col gap-2">
|
||||
<div class="py-1 min-h-12 flex items-center flex-col gap-2 px-2" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
|
||||
<!-- system generated data -->
|
||||
<SelectDropdown v-if="systemGeneratedData.includes(item)"
|
||||
:disabled="true"
|
||||
:placeholder="String(item)"
|
||||
></SelectDropdown>
|
||||
|
||||
<SelectDropdown
|
||||
v-else
|
||||
:options="dropdownOptions"
|
||||
|
@ -43,7 +50,9 @@
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<div class="w-14 h-10 flex items-center flex justify-center">
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">
|
||||
<!-- import -->
|
||||
<i v-if="this.selectedColumnType?.key && this.selectedColumnType?.value === item && !systemGeneratedData.includes(item)"
|
||||
class="sn-icon sn-icon-check" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.importedColumnTitle')">
|
||||
|
@ -71,8 +80,9 @@
|
|||
<i v-else class="sn-icon sn-icon-close text-sn-sleepy-grey" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.doNotImportColumnTitle')"></i>
|
||||
</div>
|
||||
|
||||
<div class="w-56 truncate h-10 flex items-center" :title="stepProps.exampleData[index]">{{ stepProps.exampleData[index] }}</div>
|
||||
</div>
|
||||
<div class="py-1 min-h-12 px-2 flex items-center" :title="params.import_data.columns[index]" :class="{
|
||||
'bg-sn-super-light-blue': selected
|
||||
}">{{ params.import_data.columns[index] }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -97,19 +107,17 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
stepProps: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
autoMapping: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
newColumn() {
|
||||
this.selectedColumnType.value = this.newColumn;
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -135,6 +143,10 @@ export default {
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
newColumn() {
|
||||
this.selectedColumnType.value = this.newColumn;
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
},
|
||||
autoMapping(newVal, oldVal) {
|
||||
if (newVal === true) {
|
||||
this.autoMap();
|
||||
|
@ -150,7 +162,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
autoMap() {
|
||||
Object.entries(this.stepProps.availableFields).forEach(([key, value]) => {
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
if (this.item === value) {
|
||||
this.changeSelected(key);
|
||||
}
|
||||
|
@ -164,17 +176,11 @@ export default {
|
|||
if (e === 'new') {
|
||||
value = this.newColumn;
|
||||
} else {
|
||||
value = this.stepProps.availableFields[e];
|
||||
value = this.params.import_data.available_fields[e];
|
||||
}
|
||||
const selectedColumnType = { index: this.index, key: e, value };
|
||||
this.selectedColumnType = selectedColumnType;
|
||||
this.$emit('selection:changed', selectedColumnType);
|
||||
},
|
||||
handleIsOpen(isOpen) {
|
||||
const tableRows = this.$parent.$refs.tableRowsRef;
|
||||
if (isOpen) {
|
||||
tableRows.style.overflow = 'hidden';
|
||||
} else tableRows.style.overflow = 'auto';
|
||||
}
|
||||
},
|
||||
mounted() {
|
|
@ -1,340 +0,0 @@
|
|||
<template>
|
||||
<div ref="secondStep" class="flex flex-col gap-6 h-full">
|
||||
|
||||
<!-- body -->
|
||||
<div class="flex flex-col gap-6 h-fit w-full">
|
||||
|
||||
<!-- toggle section -->
|
||||
<div id="toggle-section" class="flex flex-row gap-6">
|
||||
<!-- auto-mapping -->
|
||||
<div id="auto-mapping-toggle" class="flex flex-row gap-1">
|
||||
<span class="sci-toggle-checkbox-container">
|
||||
<input type="checkbox"
|
||||
class="sci-toggle-checkbox"
|
||||
v-model="autoMapping"
|
||||
/>
|
||||
<span class="sci-toggle-checkbox-label"></span>
|
||||
</span>
|
||||
<div class="flex my-auto w-32">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }} {{ autoMapping ? 'ON' : 'OFF' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- update empty cells -->
|
||||
<div id="update-empty-cells" class="flex flex-row gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sci-checkbox"
|
||||
:checked="updateWithEmptyCells"
|
||||
@change="toggleUpdateWithEmptyCells"
|
||||
/>
|
||||
<label class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<div class="flex my-auto">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.updateEmptyCellsText') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- only add new items -->
|
||||
<div id="only-add-new-items" class="flex flex-row gap-1">
|
||||
<div class="sci-checkbox-container my-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sci-checkbox"
|
||||
:checked="onlyAddNewItems"
|
||||
@change="toggleOnlyAddNewItems"
|
||||
/>
|
||||
<label class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<div class="flex my-auto">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.onlyAddNewItemsText') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- imported file section -->
|
||||
<div class="flex flex-row text-sn-black">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ stepProps.fileName }}
|
||||
</div>
|
||||
|
||||
<div id="table-section" class="flex flex-col w-full h-full gap-1">
|
||||
<!-- divider -->
|
||||
<div class="sci-divider"></div>
|
||||
|
||||
<!-- table -->
|
||||
<div id="table" class="flex flex-col h-[28rem] w-full">
|
||||
<!-- labels -->
|
||||
<div id="column-labels" class="flex flex-row justify-between font-bold p-3">
|
||||
<div class="w-6">{{ i18n.t('repositories.import_records.steps.step2.table.columnLabels.number') }}</div>
|
||||
<div class="w-40">{{ i18n.t('repositories.import_records.steps.step2.table.columnLabels.importedColumns') }}</div>
|
||||
<div class="w-6"></div>
|
||||
<div class="w-60">{{ i18n.t('repositories.import_records.steps.step2.table.columnLabels.scinoteColumns') }}</div>
|
||||
<div class="w-14">{{ i18n.t('repositories.import_records.steps.step2.table.columnLabels.status') }}</div>
|
||||
<div class="w-56">{{ i18n.t('repositories.import_records.steps.step2.table.columnLabels.exampleData') }}</div>
|
||||
</div>
|
||||
|
||||
<div id="table-rows" ref="tableRowsRef" class="w-full h-[28rem] flex flex-col py-4 overflow-auto gap-1">
|
||||
<!-- rows -->
|
||||
<div v-for="(item, index) in stepProps.columnNames" :key="item"
|
||||
class="flex flex-col gap-4 min-h-[56px] justify-center px-4 rounded shrink-0"
|
||||
:class="{'bg-sn-super-light-blue': this.selectedItemsIndexes.includes(index)}"
|
||||
>
|
||||
<SecondStepTableRow
|
||||
:key="item"
|
||||
:index="index"
|
||||
:item="item"
|
||||
:dropdownOptions="computedDropdownOptions"
|
||||
:stepProps="stepProps"
|
||||
@selection:changed="handleChange"
|
||||
:availableFields="this.availableFields"
|
||||
:autoMapping="this.autoMapping"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- divider -->
|
||||
<div class="sci-divider"></div>
|
||||
|
||||
<!-- imported/ignored section -->
|
||||
<div class="flex flex-row">
|
||||
<b class="pr-1">{{ computedImportedIgnoredInfo.importedSum }}</b>
|
||||
<div class="pr-1">{{ i18n.t('repositories.import_records.steps.step2.importedIgnoredSection.columnsTo') }}</div>
|
||||
<b class="pr-1">{{ i18n.t('repositories.import_records.steps.step2.importedIgnoredSection.import') }}</b>
|
||||
<b class="pr-1">{{ computedImportedIgnoredInfo.ignoredSum }}</b>
|
||||
<div class="pr-1">{{ i18n.t('repositories.import_records.steps.step2.importedIgnoredSection.columns') }}</div>
|
||||
<b>{{ i18n.t('repositories.import_records.steps.step2.importedIgnoredSection.ignored') }}</b>
|
||||
</div>
|
||||
|
||||
<!-- divider -->
|
||||
<div class="sci-divider"></div>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="flex justify-between">
|
||||
<div id="error" class="flex flex-row gap-3 text-sn-delete-red">
|
||||
<i v-if="error" class="sn-icon sn-icon-alert-warning my-auto"></i>
|
||||
<div class="my-auto">{{ error ? error : '' }}</div>
|
||||
</div>
|
||||
|
||||
<div id="buttons" class="flex gap-4">
|
||||
<button class="btn btn-secondary" data-dismiss="modal" aria-label="Close">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.cancelBtnText') }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" :disabled="!rowsIsValid" @click="importRecords">
|
||||
{{ i18n.t('repositories.import_records.steps.step2.confirmBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
import SecondStepTableRow from './second_step_table_row.vue';
|
||||
|
||||
export default {
|
||||
name: 'SecondStep',
|
||||
emits: ['step:next'],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
SecondStepTableRow
|
||||
},
|
||||
props: {
|
||||
stepProps: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
file: {
|
||||
type: File,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
autoMapping: true,
|
||||
updateWithEmptyCells: false,
|
||||
onlyAddNewItems: false,
|
||||
columnLabels: {
|
||||
0: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.number'),
|
||||
1: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.importedColumns'),
|
||||
2: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.scinoteColumns'),
|
||||
3: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.status'),
|
||||
4: this.i18n.t('repositories.import_records.steps.step2.table.columnLabels.exampleData')
|
||||
},
|
||||
selectedItems: [],
|
||||
selectedItemsIndexes: [],
|
||||
importRecordsUrl: null,
|
||||
teamId: null,
|
||||
repositoryId: null,
|
||||
availableFields: [],
|
||||
alwaysAvailableFields: [],
|
||||
repositoryColumns: null,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleUpdateWithEmptyCells() {
|
||||
this.updateWithEmptyCells = !this.updateWithEmptyCells;
|
||||
},
|
||||
toggleOnlyAddNewItems() {
|
||||
this.onlyAddNewItems = !this.onlyAddNewItems;
|
||||
},
|
||||
handleChange(payload) {
|
||||
this.error = null;
|
||||
const { index, key, value } = payload;
|
||||
|
||||
// checking if the mapping is already selected
|
||||
const foundItem = this.selectedItems.find((item) => item.index === index);
|
||||
|
||||
// if it's not, add it
|
||||
if (!foundItem && key) {
|
||||
this.selectedItems = [...this.selectedItems, { index, key, value }];
|
||||
this.selectedItemsIndexes.push(index);
|
||||
}
|
||||
// if it is but the key is null then clear it
|
||||
if (foundItem && !key) {
|
||||
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
|
||||
const indexToRemoveStr = this.selectedItemsIndexes.indexOf(index);
|
||||
if ((indexToRemoveObj !== -1) && (indexToRemoveStr !== -1)) {
|
||||
this.selectedItems.splice(indexToRemoveObj, 1);
|
||||
this.selectedItemsIndexes.splice(indexToRemoveStr, 1);
|
||||
}
|
||||
}
|
||||
// if it is and the key is not null then update it
|
||||
if (foundItem && key) {
|
||||
const indexToRemoveObj = this.selectedItems.findIndex((item) => item.index === index);
|
||||
this.selectedItems.splice(indexToRemoveObj, 1);
|
||||
this.selectedItems = [...this.selectedItems, { index, key, value }];
|
||||
}
|
||||
|
||||
this.updateAvailableItemsStatus();
|
||||
},
|
||||
// necessary for tracking which options are already selected
|
||||
updateAvailableItemsStatus() {
|
||||
let updatedAvailableFields = [];
|
||||
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
|
||||
|
||||
this.alwaysAvailableFields.forEach((field) => {
|
||||
if (selectedItemsKeys.has(field.key)) {
|
||||
const tempObj = { key: field.key, value: field.value, alreadySelected: true };
|
||||
updatedAvailableFields.push(tempObj);
|
||||
} else {
|
||||
updatedAvailableFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
this.availableFields = updatedAvailableFields;
|
||||
updatedAvailableFields = [];
|
||||
},
|
||||
generateMapping() {
|
||||
const mapping = {};
|
||||
for (let i = 0; i < this.stepProps.columnNames.length; i++) {
|
||||
const foundItem = this.selectedItems.find((item) => item.index === i);
|
||||
if (foundItem) {
|
||||
mapping[foundItem.index] = (foundItem.key === 'new' ? foundItem.value : foundItem.key);
|
||||
} else {
|
||||
mapping[i] = '';
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
},
|
||||
async importRecords() {
|
||||
const selectedItemsKeys = new Set(this.selectedItems.map((item) => item.key));
|
||||
if (!selectedItemsKeys.has('-1')) {
|
||||
this.error = this.i18n.t('repositories.import_records.steps.step2.selectNamePropertyError');
|
||||
return '';
|
||||
}
|
||||
|
||||
const mapping = this.generateMapping();
|
||||
|
||||
const jsonData = {
|
||||
file_id: this.stepProps.tempFile.id,
|
||||
mappings: mapping,
|
||||
id: this.teamId,
|
||||
preview: true,
|
||||
should_overwrite_with_empty_cells: this.updateWithEmptyCells,
|
||||
can_edit_existing_items: !this.onlyAddNewItems
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(this.importRecordsUrl, jsonData);
|
||||
if (!response.status === 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
async fetchSerializedRepositoryData() {
|
||||
const url = window.location.pathname;
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedDropdownOptions() {
|
||||
const columnKeyToLabelMapping = {};
|
||||
columnKeyToLabelMapping[-1] = this.i18n.t('repositories.import_records.steps.step2.computedDropdownOptions.name');
|
||||
|
||||
if (this.repositoryColumns) {
|
||||
this.repositoryColumns.forEach((el) => {
|
||||
const [key, colName, colType] = el;
|
||||
columnKeyToLabelMapping[key] = this.i18n.t(`repositories.import_records.steps.step2.computedDropdownOptions.${colType}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.availableFields) {
|
||||
let options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${columnKeyToLabelMapping[el.key]})`]);
|
||||
options = [['new', this.i18n.t('repositories.import_records.steps.step2.table.tableRow.importAsNewColumn')]].concat(options);
|
||||
return options;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
computedImportedIgnoredInfo() {
|
||||
const importedSum = this.selectedItems.length;
|
||||
const ignoredSum = this.stepProps.columnNames.length - importedSum;
|
||||
return { importedSum, ignoredSum };
|
||||
},
|
||||
rowsIsValid() {
|
||||
let valid = true;
|
||||
this.selectedItems.forEach((v) => {
|
||||
if (v.key === 'new' && (!v.value.type || v.value.name.length < 2)) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// Fetch repository data and set it to state
|
||||
const repositoryData = await this.fetchSerializedRepositoryData();
|
||||
this.teamId = String(repositoryData.data.attributes.team_id);
|
||||
this.repositoryId = String(repositoryData.data.id);
|
||||
this.importRecordsUrl = repositoryData.data.attributes.urls.import_records;
|
||||
this.repositoryColumns = repositoryData.data.attributes.repository_columns;
|
||||
|
||||
// Adding alreadySelected attribute for tracking
|
||||
const tempAvailableFields = [];
|
||||
Object.entries(this.stepProps.availableFields).forEach(([key, value]) => {
|
||||
const field = { key, value, alreadySelected: false };
|
||||
tempAvailableFields.push(field);
|
||||
});
|
||||
this.availableFields = tempAvailableFields;
|
||||
this.alwaysAvailableFields = tempAvailableFields;
|
||||
|
||||
// Remove infoComponent if it's still present
|
||||
const infoComponent = this.$parent.$refs.infoPartRef;
|
||||
infoComponent.remove();
|
||||
}
|
||||
};
|
||||
</script>
|
129
app/javascript/vue/repositories/modals/import/upload_step.vue
Normal file
129
app/javascript/vue/repositories/modals/import/upload_step.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog flex" role="document" :class="{'!w-[900px]': showingInfo}">
|
||||
<div v-if="showingInfo" class="w-[300px] h-full bg-sn-super-light-grey p-6 rounded-s text-sn-dark-grey">
|
||||
<h3 class="my-0 mb-4">{{ this.i18n.t('repositories.import_records.info_sidebar.title') }}</h3>
|
||||
<div v-for="i in 4" :key="i" class="flex gap-3 mb-4">
|
||||
<span class="btn btn-secondary icon-btn !text-sn-black">
|
||||
<i class="sn-icon"
|
||||
:class="i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.icon`)"
|
||||
></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-bold mb-2">{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.label`) }}</div>
|
||||
<div>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.subtext`) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mb-4 items-center">
|
||||
<span class="btn btn-secondary icon-btn !text-sn-black">
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</span>
|
||||
<a :href="i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')" class="font-bold">
|
||||
{{ i18n.t('repositories.import_records.info_sidebar.elements.element4.label') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content grow" :class="{'!rounded-s-none': showingInfo}">
|
||||
<div class="modal-header gap-4">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<button class="btn btn-light btn-sm mr-auto" @click="showingInfo = !showingInfo">
|
||||
<i class="sn-icon sn-icon-help-s"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.helpText') }}
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block !mr-0" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-sn-dark-grey">
|
||||
{{ this.i18n.t('repositories.import_records.steps.step1.subtitle') }}
|
||||
</p>
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportTitle') }}
|
||||
</h3>
|
||||
<div class="flex gap-4 mb-6">
|
||||
<button class="btn btn-secondary btn-sm" @click="exportFullInventory">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportFullInvBtnText') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportEmptyInvBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.importTitle') }}
|
||||
</h3>
|
||||
<DragAndDropUpload
|
||||
class="h-60"
|
||||
@file:dropped="uploadFile"
|
||||
@file:error="handleError"
|
||||
@file:error:clear="this.error = null"
|
||||
:supportingText="`${i18n.t('repositories.import_records.steps.step1.dragAndDropSupportingText')}`"
|
||||
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div v-if="error" class="flex flex-row gap-2 my-auto mr-auto text-sn-delete-red">
|
||||
<i class="sn-icon sn-icon-alert-warning"></i>
|
||||
<div class="my-auto">{{ error }}</div>
|
||||
</div>
|
||||
<div v-if="exportInventoryMessage" class="flex flex-row gap-2 my-auto mr-auto text-sn-alert-green">
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" @click="close" aria-label="Close">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.cancelBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../../packs/custom_axios';
|
||||
import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
|
||||
import modalMixin from '../../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'UploadStep',
|
||||
emits: ['step:next', 'info:hide'],
|
||||
components: {
|
||||
DragAndDropUpload
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showingInfo: false,
|
||||
error: null,
|
||||
parseSheetUrl: null,
|
||||
exportInventoryMessage: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
exportFullInventory() {
|
||||
axios.post(this.params.attributes.urls.export_repository)
|
||||
.then((response) => {
|
||||
this.exportInventoryMessage = response.data.message;
|
||||
setTimeout(() => { this.exportInventoryMessage = null; }, 5000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = error;
|
||||
});
|
||||
},
|
||||
uploadFile(file) {
|
||||
this.$emit('uploadFile', file);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -12,7 +12,8 @@ class RepositorySerializer < ActiveModel::Serializer
|
|||
def urls
|
||||
{
|
||||
parse_sheet: parse_sheet_repository_path(object),
|
||||
import_records: import_records_repository_path(object)
|
||||
import_records: import_records_repository_path(object),
|
||||
export_repository: export_repositories_team_path(object.team, file_type: :csv, repository_ids: object.id),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
<%= render partial: 'save_repository_filter_modal' %>
|
||||
|
||||
<div id="importRepositoryModal" data-behaviour="vue">
|
||||
<import-repository-modal repositoryUrl="<%= repository_path(@repository) %>" />
|
||||
<import-repository-modal repository-url="<%= repository_path(@repository) %>" />
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag 'vue_components_action_toolbar' %>
|
||||
|
|
|
@ -2219,12 +2219,7 @@ en:
|
|||
importedFileText: 'Imported file:'
|
||||
cancelBtnText: 'Cancel'
|
||||
confirmBtnText: 'Confirm'
|
||||
importedIgnoredSection:
|
||||
columnsTo: 'columns to'
|
||||
import: 'import.'
|
||||
columns: 'columns'
|
||||
ignored: 'ignored.'
|
||||
|
||||
importedIgnoredSection: '<b>%{imported}</b> columns to <b>import.</b> <b>%{ignored}</b> columns <b>ignored</b>.'
|
||||
computedDropdownOptions:
|
||||
name: 'Name'
|
||||
RepositoryTextValue: 'Text'
|
||||
|
|
Loading…
Add table
Reference in a new issue