mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-12 23:08:30 +08:00
Create 'import mapping' step [SCI-10579]
This commit is contained in:
parent
bc6e43052f
commit
db3ba2aadb
16 changed files with 683 additions and 122 deletions
|
@ -327,13 +327,14 @@ class RepositoriesController < ApplicationController
|
||||||
.find_by_id(import_params[:id]))
|
.find_by_id(import_params[:id]))
|
||||||
|
|
||||||
# Access the checkbox values from params
|
# Access the checkbox values from params
|
||||||
can_edit_existing_items = params[:edit_existing_items_checkbox]
|
can_edit_existing_items = params[:can_edit_existing_items]
|
||||||
should_overwrite_with_empty_cells = params[:overwrite_with_empty_cells]
|
should_overwrite_with_empty_cells = params[:should_overwrite_with_empty_cells]
|
||||||
|
preview = params[:preview]
|
||||||
|
|
||||||
# Check if there exist mapping for repository record (it's mandatory)
|
# Check if there exist mapping for repository record (it's mandatory)
|
||||||
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
|
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
|
||||||
import_records = repostiory_import_actions
|
import_records = repostiory_import_actions
|
||||||
status = import_records.import!(can_edit_existing_items, should_overwrite_with_empty_cells)
|
status = import_records.import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
|
|
||||||
if status[:status] == :ok
|
if status[:status] == :ok
|
||||||
log_activity(:import_inventory_items,
|
log_activity(:import_inventory_items,
|
||||||
|
@ -343,10 +344,12 @@ class RepositoriesController < ApplicationController
|
||||||
number_of_rows: status[:nr_of_added],
|
number_of_rows: status[:nr_of_added],
|
||||||
total_nr: status[:total_nr])
|
total_nr: status[:total_nr])
|
||||||
|
|
||||||
log_activity(:item_added_with_import,
|
if preview
|
||||||
num_of_items: status[:nr_of_added])
|
render json: status, status: :ok
|
||||||
|
else
|
||||||
render json: {}, status: :ok
|
render json: {}, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
flash[:alert] =
|
flash[:alert] =
|
||||||
t('repositories.import_records.partial_success_flash',
|
t('repositories.import_records.partial_success_flash',
|
||||||
|
@ -558,7 +561,7 @@ class RepositoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_params
|
def import_params
|
||||||
params.permit(:id, :file, :file_id, mappings: {}).to_h
|
params.permit(:id, :file, :file_id, :preview, mappings: {}).to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
def repository_response(message)
|
def repository_response(message)
|
||||||
|
|
|
@ -3,12 +3,15 @@
|
||||||
:startHidden="true"
|
:startHidden="true"
|
||||||
:infoParams="infoParams"
|
:infoParams="infoParams"
|
||||||
:title="steps[activeStep].title"
|
:title="steps[activeStep].title"
|
||||||
:helpText="steps[activeStep].helpText">
|
:subtitle="steps[activeStep].subtitle"
|
||||||
|
:helpText="steps[activeStep].helpText"
|
||||||
|
>
|
||||||
<component
|
<component
|
||||||
|
:key="steps[activeStep].id"
|
||||||
:is="steps[activeStep].component"
|
:is="steps[activeStep].component"
|
||||||
@step:next="proceedToNext"
|
@step:next="proceedToNextStep"
|
||||||
@step:back="activeStep -= 1"
|
@step:back="goBackToPrevStep"
|
||||||
:stepData="stepData"
|
:stepProps="steps[activeStep].stepData"
|
||||||
/>
|
/>
|
||||||
</InfoModal>
|
</InfoModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,86 +22,91 @@
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import InfoModal from '../../shared/info_modal.vue';
|
import InfoModal from '../../shared/info_modal.vue';
|
||||||
import FirstStep from './import/first_step.vue';
|
import FirstStep from './import/first_step.vue';
|
||||||
|
import SecondStep from './import/second_step.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImportRepositoryModal',
|
name: 'ImportRepositoryModal',
|
||||||
components: { InfoModal, FirstStep },
|
components: { InfoModal, FirstStep, SecondStep },
|
||||||
props: {
|
props: {
|
||||||
repositoryUrl: String, required: true
|
repositoryUrl: String,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeStep: 0,
|
activeStep: 0,
|
||||||
|
repositoryData: null,
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.steps.step0.id'),
|
id: this.i18n.t('repositories.import_records.steps.step1.id'),
|
||||||
icon: I18n.t('repositories.import_records.steps.step0.icon'),
|
icon: this.i18n.t('repositories.import_records.steps.step1.icon'),
|
||||||
label: I18n.t('repositories.import_records.steps.step0.label'),
|
label: this.i18n.t('repositories.import_records.steps.step1.label'),
|
||||||
title: I18n.t('repositories.import_records.steps.step0.title'),
|
title: this.i18n.t('repositories.import_records.steps.step1.title'),
|
||||||
helpText: I18n.t('repositories.import_records.steps.step0.helpText'),
|
subtitle: this.i18n.t('repositories.import_records.steps.step1.subtitle'),
|
||||||
component: shallowRef(FirstStep)
|
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: {
|
infoParams: {
|
||||||
title: I18n.t('repositories.import_records.info_sidebar.title'),
|
title: this.i18n.t('repositories.import_records.info_sidebar.title'),
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element0.id'),
|
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.id'),
|
||||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element0.icon'),
|
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.icon'),
|
||||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element0.label'),
|
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.label'),
|
||||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element0.subtext')
|
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element0.subtext')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element1.id'),
|
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.id'),
|
||||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element1.icon'),
|
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.icon'),
|
||||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element1.label'),
|
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.label'),
|
||||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element1.subtext')
|
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element1.subtext')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element2.id'),
|
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.id'),
|
||||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element2.icon'),
|
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.icon'),
|
||||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element2.label'),
|
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.label'),
|
||||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element2.subtext')
|
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element2.subtext')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element3.id'),
|
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.id'),
|
||||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element3.icon'),
|
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.icon'),
|
||||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element3.label'),
|
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.label'),
|
||||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element3.subtext')
|
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element3.subtext')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element4.id'),
|
id: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.id'),
|
||||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element4.icon'),
|
icon: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.icon'),
|
||||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element4.label'),
|
label: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.label'),
|
||||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element4.subtext'),
|
subtext: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.subtext'),
|
||||||
linkTo: I18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')
|
linkTo: this.i18n.t('repositories.import_records.info_sidebar.elements.element4.linkTo')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
stepData: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
activeStep(newVal, oldVal) {
|
|
||||||
console.log(`${oldVal} -> ${newVal}`);
|
|
||||||
},
|
|
||||||
stepData(newVal, oldVal) {
|
|
||||||
console.log(`${oldVal} -> ${newVal}`);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
window.importRepositoryModalComponent = this;
|
window.importRepositoryModalComponent = this;
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
open() {
|
open() {
|
||||||
this.$refs.modal.open();
|
this.$refs.modal.open();
|
||||||
},
|
},
|
||||||
proceedToNext(data) {
|
proceedToNextStep(data) {
|
||||||
console.log('incoming data', data);
|
this.steps[this.activeStep + 1].stepData = data;
|
||||||
this.stepData = data;
|
|
||||||
this.activeStep += 1;
|
this.activeStep += 1;
|
||||||
|
},
|
||||||
|
goBackToPrevStep() {
|
||||||
|
this.activeStep -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,16 +7,16 @@
|
||||||
<!-- export -->
|
<!-- export -->
|
||||||
<div id="export-section" class="flex flex-col gap-3">
|
<div id="export-section" class="flex flex-col gap-3">
|
||||||
<h3 class="my-0 text-sn-dark-grey">
|
<h3 class="my-0 text-sn-dark-grey">
|
||||||
{{ i18n.t('repositories.import_records.steps.step0.importTitle') }}
|
{{ i18n.t('repositories.import_records.steps.step1.importTitle') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div id="export-buttons" class="flex flex-row gap-4">
|
<div id="export-buttons" class="flex flex-row gap-4">
|
||||||
<button class="btn btn-secondary btn-sm" @click="exportFullInventory">
|
<button class="btn btn-secondary btn-sm" @click="exportFullInventory">
|
||||||
<i class="sn-icon sn-icon-export"></i>
|
<i class="sn-icon sn-icon-export"></i>
|
||||||
{{ i18n.t('repositories.import_records.steps.step0.exportFullInvBtnText') }}
|
{{ i18n.t('repositories.import_records.steps.step1.exportFullInvBtnText') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm">
|
<button class="btn btn-secondary btn-sm">
|
||||||
<i class="sn-icon sn-icon-export"></i>
|
<i class="sn-icon sn-icon-export"></i>
|
||||||
{{ i18n.t('repositories.import_records.steps.step0.exportEmptyInvBtnText') }}
|
{{ i18n.t('repositories.import_records.steps.step1.exportEmptyInvBtnText') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,13 +24,13 @@
|
||||||
<!-- import -->
|
<!-- import -->
|
||||||
<div id="import-section" class="flex flex-col gap-3 h-full w-full">
|
<div id="import-section" class="flex flex-col gap-3 h-full w-full">
|
||||||
<h3 class="my-0 text-sn-dark-grey">
|
<h3 class="my-0 text-sn-dark-grey">
|
||||||
{{ i18n.t('repositories.import_records.steps.step0.importBtnText') }}
|
{{ i18n.t('repositories.import_records.steps.step1.importBtnText') }}
|
||||||
</h3>
|
</h3>
|
||||||
<DragAndDropUpload
|
<DragAndDropUpload
|
||||||
@file:dropped="uploadFile"
|
@file:dropped="uploadFile"
|
||||||
@file:error="handleError"
|
@file:error="handleError"
|
||||||
@file:error:clear="this.error = null"
|
@file:error:clear="this.error = null"
|
||||||
:supportingText="`${i18n.t('repositories.import_records.steps.step0.dragAndDropSupportingText')}`"
|
:supportingText="`${i18n.t('repositories.import_records.steps.step1.dragAndDropSupportingText')}`"
|
||||||
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" data-dismiss="modal" aria-label="Close">
|
<button class="btn btn-secondary" data-dismiss="modal" aria-label="Close">
|
||||||
{{ i18n.t('repositories.import_records.steps.step0.cancelBtnText') }}
|
{{ i18n.t('repositories.import_records.steps.step1.cancelBtnText') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,10 +62,16 @@ import DragAndDropUpload from '../../../shared/drag_and_drop_upload.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FirstStep',
|
name: 'FirstStep',
|
||||||
emits: ['step:next'],
|
emits: ['step:next', 'info:hide'],
|
||||||
components: {
|
components: {
|
||||||
DragAndDropUpload
|
DragAndDropUpload
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
stepProps: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showingInfo: false,
|
showingInfo: false,
|
||||||
|
@ -136,14 +142,23 @@ export default {
|
||||||
// First, parse the sheet
|
// First, parse the sheet
|
||||||
const parsedSheetResponse = await this.parseSheet(file);
|
const parsedSheetResponse = await this.parseSheet(file);
|
||||||
|
|
||||||
// If parsed successfully, go to next step and pass the necessary data
|
// If parsed successfully, go to next step and pass through the necessary data
|
||||||
if (parsedSheetResponse) {
|
if (parsedSheetResponse) {
|
||||||
const {
|
const {
|
||||||
header: columnNames,
|
header: columnNames,
|
||||||
available_fields: availableFields,
|
available_fields: availableFields,
|
||||||
columns: exampleData
|
columns: exampleData
|
||||||
} = parsedSheetResponse.data.import_data;
|
} = parsedSheetResponse.data.import_data;
|
||||||
this.$emit('step:next', { columnNames, availableFields, exampleData });
|
const fileName = file.name;
|
||||||
|
const tempFile = parsedSheetResponse.data.temp_file;
|
||||||
|
|
||||||
|
this.$emit('step:next', {
|
||||||
|
columnNames,
|
||||||
|
availableFields,
|
||||||
|
exampleData,
|
||||||
|
fileName,
|
||||||
|
tempFile
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async parseSheet(file) {
|
async parseSheet(file) {
|
||||||
|
|
329
app/javascript/vue/repositories/modals/import/second_step.vue
Normal file
329
app/javascript/vue/repositories/modals/import/second_step.vue
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
<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"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
</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" @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: false,
|
||||||
|
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;
|
||||||
|
} 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) {
|
||||||
|
const options = this.availableFields.map((el) => [String(el.key), `${String(el.value)} (${columnKeyToLabelMapping[el.key]})`]);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
computedImportedIgnoredInfo() {
|
||||||
|
const importedSum = this.selectedItems.length;
|
||||||
|
const ignoredSum = this.stepProps.columnNames.length - importedSum;
|
||||||
|
return { importedSum, ignoredSum };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<!-- columns -->
|
||||||
|
<div class="flex flex-row justify-between gap-6">
|
||||||
|
<!-- number col -->
|
||||||
|
<div class="w-6 my-auto">{{ index + 1 }}</div>
|
||||||
|
|
||||||
|
<div class="w-40 my-auto truncate" :title="item">{{ item }}</div>
|
||||||
|
|
||||||
|
<i class="sn-icon sn-icon-arrow-right w-6 my-auto relative left-5"></i>
|
||||||
|
|
||||||
|
<div class="w-60 my-auto">
|
||||||
|
|
||||||
|
<!-- system generated data -->
|
||||||
|
<SelectDropdown v-if="systemGeneratedData.includes(item)"
|
||||||
|
:disabled="true"
|
||||||
|
:placeholder="String(item)"
|
||||||
|
></SelectDropdown>
|
||||||
|
|
||||||
|
<SelectDropdown
|
||||||
|
v-else
|
||||||
|
:options="dropdownOptions"
|
||||||
|
@change="changeSelected"
|
||||||
|
@isOpen="handleIsOpen"
|
||||||
|
:clearable="true"
|
||||||
|
:size="'sm'"
|
||||||
|
placeholder="Do not import"
|
||||||
|
:title="this.selectedColumnType?.value"
|
||||||
|
></SelectDropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-14 my-auto flex justify-center">
|
||||||
|
<!-- 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')">
|
||||||
|
</i>
|
||||||
|
|
||||||
|
<!-- default column -->
|
||||||
|
<i v-else-if="systemGeneratedData.includes(item)"
|
||||||
|
class="sn-icon sn-icon-check text-sn-sleepy-grey" :title="i18n.t('repositories.import_records.steps.step2.table.tableRow.defaultColumnTitle')">
|
||||||
|
</i>
|
||||||
|
|
||||||
|
<!-- user defined this column -->
|
||||||
|
<i v-else-if="this.selectedColumnType?.key && this.selectedColumnType?.value !== item"
|
||||||
|
class="sn-icon sn-icon-info text-sn-science-blue"
|
||||||
|
:title="`${i18n.t('repositories.import_records.steps.step2.table.tableRow.userDefinedColumnTitle')} ${this.selectedColumnType.value}`"></i>
|
||||||
|
|
||||||
|
<!-- error: can not import -->
|
||||||
|
<!-- <i v-else-if=""></i> -->
|
||||||
|
|
||||||
|
<!-- match not found -->
|
||||||
|
<!-- <i v-else-if=""></i> -->
|
||||||
|
|
||||||
|
<!-- do not import -->
|
||||||
|
<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 my-auto" :title="stepProps.exampleData[index]">{{ stepProps.exampleData[index] }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SecondStepTableRow',
|
||||||
|
emits: ['selection:changed'],
|
||||||
|
components: {
|
||||||
|
SelectDropdown
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
dropdownOptions: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
stepProps: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedColumnType: null,
|
||||||
|
systemGeneratedData: [
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.itemId'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.createdOn'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.addedBy'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.addedOn'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.archivedBy'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.archivedOn'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.updatedBy'),
|
||||||
|
this.i18n.t('repositories.import_records.steps.step2.table.tableRow.systemGeneratedData.updatedOn')]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeSelected(e) {
|
||||||
|
const value = this.stepProps.availableFields[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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="modal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
<div ref="modal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
||||||
<div class="modal-dialog" role="document" :class="[{'!w-[900px]' : showingInfo}, {'!w-[600px]' : !showingInfo}]">
|
<div class="modal-dialog" role="document" :class="[{'!w-[900px]' : showingInfo}, {'!w-fit' : !showingInfo}]">
|
||||||
<div class="modal-content !p-0 bg-sn-white w-full h-full flex" :class="[{'flex-row': showingInfo}, {'flex-col': !showingInfo}]">
|
<div class="modal-content !p-0 bg-sn-white w-full h-full flex" :class="[{'flex-row': showingInfo}, {'flex-col': !showingInfo}]">
|
||||||
<div id="body-container" class="flex flex-row w-full h-full">
|
<div id="body-container" class="flex flex-row w-full h-full">
|
||||||
<!-- info -->
|
<!-- info -->
|
||||||
<div id="info-part">
|
<div id="info-part" ref="infoPartRef">
|
||||||
<InfoComponent
|
<InfoComponent
|
||||||
v-if="showingInfo"
|
v-if="showingInfo"
|
||||||
:infoParams="infoParams"
|
:infoParams="infoParams"
|
||||||
|
@ -13,10 +13,11 @@
|
||||||
<!-- content -->
|
<!-- content -->
|
||||||
<div id="content-part" class="flex flex-col w-full p-6 gap-6">
|
<div id="content-part" class="flex flex-col w-full p-6 gap-6">
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div id="info-modal-header" class="flex flex-row h-fit w-full justify-between">
|
<div id="info-modal-header" class="flex flex-col h-fit w-full gap-2">
|
||||||
|
<div id="title-part" class="flex flex-row h-fit w-full justify-between">
|
||||||
<div id="title-with-help" class="flex flex-row gap-3">
|
<div id="title-with-help" class="flex flex-row gap-3">
|
||||||
<h3 class="modal-title text-sn-dark-grey">{{ title }}</h3>
|
<h3 class="modal-title text-sn-dark-grey">{{ title }}</h3>
|
||||||
<button class="btn btn-light btn-sm" @click="showingInfo = !showingInfo">
|
<button v-if="helpText" class="btn btn-light btn-sm" @click="showingInfo = !showingInfo">
|
||||||
<i class="sn-icon sn-icon-help-s"></i>
|
<i class="sn-icon sn-icon-help-s"></i>
|
||||||
{{ helpText }}
|
{{ helpText }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,6 +26,10 @@
|
||||||
<i class="sn-icon sn-icon-close"></i>
|
<i class="sn-icon sn-icon-close"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="subtitle-part" class="text-sn-dark-grey">
|
||||||
|
{{ subtitle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- main content -->
|
<!-- main content -->
|
||||||
<div id="info-modal-main-content" class="h-full">
|
<div id="info-modal-main-content" class="h-full">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -47,9 +52,13 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
helpText: {
|
helpText: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
infoParams: {
|
infoParams: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -64,7 +73,7 @@ export default {
|
||||||
components: { InfoComponent },
|
components: { InfoComponent },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showingInfo: true
|
showingInfo: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -253,7 +253,8 @@ export default {
|
||||||
value(newValue) {
|
value(newValue) {
|
||||||
this.newValue = newValue;
|
this.newValue = newValue;
|
||||||
},
|
},
|
||||||
isOpen() {
|
isOpen(newVal) {
|
||||||
|
this.$emit('isOpen', newVal);
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.setPosition();
|
this.setPosition();
|
||||||
|
@ -284,7 +285,7 @@ export default {
|
||||||
clear() {
|
clear() {
|
||||||
this.newValue = this.multiple ? [] : null;
|
this.newValue = this.multiple ? [] : null;
|
||||||
this.query = '';
|
this.query = '';
|
||||||
this.$emit('change', this.newValue, this.getLabels(this.newValue));
|
this.$emit('change', this.newValue, '');
|
||||||
},
|
},
|
||||||
close(e) {
|
close(e) {
|
||||||
if (e && e.target.closest('.sn-select-dropdown')) return;
|
if (e && e.target.closest('.sn-select-dropdown')) return;
|
||||||
|
|
|
@ -201,9 +201,9 @@ class Repository < RepositoryBase
|
||||||
new_repo
|
new_repo
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_records(sheet, mappings, user, can_edit_existing_items, should_overwrite_with_empty_cells)
|
def import_records(sheet, mappings, user, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
importer = RepositoryImportParser::Importer.new(sheet, mappings, user, self)
|
importer = RepositoryImportParser::Importer.new(sheet, mappings, user, self)
|
||||||
importer.run(can_edit_existing_items, should_overwrite_with_empty_cells)
|
importer.run(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assigned_rows(my_module)
|
def assigned_rows(my_module)
|
||||||
|
|
|
@ -72,7 +72,6 @@ class RepositoryChecklistValue < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: after ticket for tracking changes on checklist items
|
|
||||||
def update_data!(new_data, user, preview: false)
|
def update_data!(new_data, user, preview: false)
|
||||||
item_ids = new_data.is_a?(String) ? JSON.parse(new_data) : new_data
|
item_ids = new_data.is_a?(String) ? JSON.parse(new_data) : new_data
|
||||||
|
|
||||||
|
|
15
app/serializers/repository_cell_serializer.rb
Normal file
15
app/serializers/repository_cell_serializer.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RepositoryCellSerializer < ActiveModel::Serializer
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
attributes :id, :value, :changes
|
||||||
|
|
||||||
|
def changes
|
||||||
|
object.value.changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
object.value
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,9 +4,5 @@ class RepositoryRowSerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
attributes :id, :name, :code
|
attributes :id, :name, :code
|
||||||
|
has_many :repository_cells
|
||||||
def urls
|
|
||||||
{
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,16 @@
|
||||||
class RepositorySerializer < ActiveModel::Serializer
|
class RepositorySerializer < ActiveModel::Serializer
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
attributes :urls, :id, :team_id
|
attributes :urls, :id, :team_id, :repository_columns
|
||||||
|
|
||||||
|
def repository_columns
|
||||||
|
object.repository_columns.pluck(:id, :name, :data_type)
|
||||||
|
end
|
||||||
|
|
||||||
def urls
|
def urls
|
||||||
{
|
{
|
||||||
parse_sheet: parse_sheet_repository_path(object)
|
parse_sheet: parse_sheet_repository_path(object),
|
||||||
|
import_records: import_records_repository_path(object)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,22 +8,23 @@ module ImportRepository
|
||||||
@user = options.fetch(:user)
|
@user = options.fetch(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import!(can_edit_existing_items, should_overwrite_with_empty_cells)
|
def import!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
status = run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells)
|
status = run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
@temp_file.destroy
|
@temp_file.destroy
|
||||||
status
|
status
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells)
|
def run_import_actions(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
@temp_file.file.open do |temp_file|
|
@temp_file.file.open do |temp_file|
|
||||||
@repository.import_records(
|
@repository.import_records(
|
||||||
SpreadsheetParser.open_spreadsheet(temp_file),
|
SpreadsheetParser.open_spreadsheet(temp_file),
|
||||||
@mappings,
|
@mappings,
|
||||||
@user,
|
@user,
|
||||||
can_edit_existing_items,
|
can_edit_existing_items,
|
||||||
should_overwrite_with_empty_cells
|
should_overwrite_with_empty_cells,
|
||||||
|
preview
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,6 +67,7 @@ module RepositoryCsvExport
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
|
csv_row << row.row_consumption(row.stock_consumption) if add_consumption
|
||||||
csv << csv_row
|
csv << csv_row
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,14 +25,11 @@ module RepositoryImportParser
|
||||||
@repository_columns = @repository.repository_columns
|
@repository_columns = @repository.repository_columns
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(can_edit_existing_items, should_overwrite_with_empty_cells)
|
def run(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
fetch_columns
|
fetch_columns
|
||||||
return check_for_duplicate_columns if check_for_duplicate_columns
|
return check_for_duplicate_columns if check_for_duplicate_columns
|
||||||
|
|
||||||
# Used for developing preview changes (will be removed)
|
import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
preview = false
|
|
||||||
|
|
||||||
import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview: preview)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -66,10 +63,12 @@ module RepositoryImportParser
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview: false)
|
def import_rows!(can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
errors = false
|
errors = false
|
||||||
duplicate_ids = SpreadsheetParser.duplicate_ids(@sheet)
|
duplicate_ids = SpreadsheetParser.duplicate_ids(@sheet)
|
||||||
|
|
||||||
|
imported_rows = []
|
||||||
|
|
||||||
@repository.transaction do
|
@repository.transaction do
|
||||||
batch_counter = 0
|
batch_counter = 0
|
||||||
full_row_import_batch = []
|
full_row_import_batch = []
|
||||||
|
@ -79,7 +78,7 @@ module RepositoryImportParser
|
||||||
next if row.blank?
|
next if row.blank?
|
||||||
|
|
||||||
# Skip duplicates
|
# Skip duplicates
|
||||||
next if duplicate_ids.include?(row.first) && !preview
|
next if duplicate_ids.include?(row.first)
|
||||||
|
|
||||||
unless @header_skipped
|
unless @header_skipped
|
||||||
@header_skipped = true
|
@header_skipped = true
|
||||||
|
@ -98,7 +97,7 @@ module RepositoryImportParser
|
||||||
if index == @name_index
|
if index == @name_index
|
||||||
|
|
||||||
# check if row (inventory) already exists
|
# check if row (inventory) already exists
|
||||||
existing_row = RepositoryRow.find_by(id: incoming_row[0].gsub(RepositoryRow::ID_PREFIX, ''))
|
existing_row = RepositoryRow.includes(repository_cells: :value).find_by(id: incoming_row[0].gsub(RepositoryRow::ID_PREFIX, ''))
|
||||||
|
|
||||||
# if it doesn't exist create it
|
# if it doesn't exist create it
|
||||||
unless existing_row
|
unless existing_row
|
||||||
|
@ -122,13 +121,13 @@ module RepositoryImportParser
|
||||||
# otherwise add according to criteria
|
# otherwise add according to criteria
|
||||||
else
|
else
|
||||||
# if it does exist but shouldn't be edited, error out and break
|
# if it does exist but shouldn't be edited, error out and break
|
||||||
if existing_row && can_edit_existing_items == '0'
|
if existing_row && (can_edit_existing_items == false)
|
||||||
errors = true
|
errors = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# if it does exist and should be edited, update the existing row
|
# if it does exist and should be edited, update the existing row
|
||||||
if existing_row && can_edit_existing_items == '1'
|
if existing_row && (can_edit_existing_items == true)
|
||||||
# update the existing row with incoming row data
|
# update the existing row with incoming row data
|
||||||
new_full_row[:repository_row] = existing_row
|
new_full_row[:repository_row] = existing_row
|
||||||
end
|
end
|
||||||
|
@ -146,13 +145,14 @@ module RepositoryImportParser
|
||||||
|
|
||||||
next if batch_counter < IMPORT_BATCH_SIZE
|
next if batch_counter < IMPORT_BATCH_SIZE
|
||||||
|
|
||||||
import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview: preview)
|
# import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview: preview)
|
||||||
|
imported_rows += import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
full_row_import_batch = []
|
full_row_import_batch = []
|
||||||
batch_counter = 0
|
batch_counter = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# Import of the remaining rows
|
# Import of the remaining rows
|
||||||
import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview: preview) if full_row_import_batch.any?
|
imported_rows += import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview) if full_row_import_batch.any?
|
||||||
|
|
||||||
full_row_import_batch
|
full_row_import_batch
|
||||||
end
|
end
|
||||||
|
@ -162,13 +162,19 @@ module RepositoryImportParser
|
||||||
nr_of_added: @new_rows_added,
|
nr_of_added: @new_rows_added,
|
||||||
total_nr: @total_new_rows }
|
total_nr: @total_new_rows }
|
||||||
end
|
end
|
||||||
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows }
|
changes = ActiveModelSerializers::SerializableResource.new(
|
||||||
|
imported_rows,
|
||||||
|
each_serializer: RepositoryRowSerializer,
|
||||||
|
include: [:repository_cells]
|
||||||
|
).as_json[:included]
|
||||||
|
|
||||||
|
{ status: :ok, nr_of_added: @new_rows_added, total_nr: @total_new_rows, changes: changes }
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview: false)
|
def import_batch_to_database(full_row_import_batch, can_edit_existing_items, should_overwrite_with_empty_cells, preview)
|
||||||
skipped_rows = []
|
skipped_rows = []
|
||||||
|
|
||||||
full_row_import_batch.each do |full_row|
|
full_row_import_batch.map do |full_row|
|
||||||
# skip archived rows and rows that belong to other repositories
|
# skip archived rows and rows that belong to other repositories
|
||||||
if full_row[:repository_row].archived || full_row[:repository_row].repository_id != @repository.id
|
if full_row[:repository_row].archived || full_row[:repository_row].repository_id != @repository.id
|
||||||
skipped_rows << full_row[:repository_row]
|
skipped_rows << full_row[:repository_row]
|
||||||
|
@ -199,16 +205,16 @@ module RepositoryImportParser
|
||||||
@user.as_json(root: true, only: :settings).deep_symbolize_keys
|
@user.as_json(root: true, only: :settings).deep_symbolize_keys
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_cell = full_row[:repository_row].repository_cells.find_by(repository_column: column)
|
existing_cell = full_row[:repository_row].repository_cells.find { |c| c.repository_column_id == column.id }
|
||||||
|
|
||||||
next if cell_value.nil? && existing_cell.nil?
|
next if cell_value.nil? && existing_cell.nil?
|
||||||
|
|
||||||
if existing_cell
|
if existing_cell
|
||||||
# existing_cell present && !can_edit_existing_items
|
# existing_cell present && !can_edit_existing_items
|
||||||
next if can_edit_existing_items == '0'
|
next if can_edit_existing_items == false
|
||||||
|
|
||||||
# existing_cell present && can_edit_existing_items
|
# existing_cell present && can_edit_existing_items
|
||||||
if can_edit_existing_items == '1'
|
if can_edit_existing_items == true
|
||||||
# if incoming cell is not empty
|
# if incoming cell is not empty
|
||||||
case cell_value
|
case cell_value
|
||||||
|
|
||||||
|
@ -229,10 +235,10 @@ module RepositoryImportParser
|
||||||
end
|
end
|
||||||
|
|
||||||
# if incoming cell is empty && should_overwrite_with_empty_cells
|
# if incoming cell is empty && should_overwrite_with_empty_cells
|
||||||
existing_cell.value.destroy! if cell_value.nil? && should_overwrite_with_empty_cells == '1'
|
existing_cell.value.destroy! if cell_value.nil? && should_overwrite_with_empty_cells == true
|
||||||
|
|
||||||
# if incoming cell is empty && !should_overwrite_with_empty_cells
|
# if incoming cell is empty && !should_overwrite_with_empty_cells
|
||||||
next if cell_value.nil? && should_overwrite_with_empty_cells == '0'
|
next if cell_value.nil? && should_overwrite_with_empty_cells == false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# no existing_cell. Create a new one.
|
# no existing_cell. Create a new one.
|
||||||
|
@ -240,6 +246,8 @@ module RepositoryImportParser
|
||||||
cell_value.save!(validate: false)
|
cell_value.save!(validate: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
full_row[:repository_row]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2033,7 +2033,7 @@ en:
|
||||||
error_searching: "Error searching, please try again"
|
error_searching: "Error searching, please try again"
|
||||||
button_tooltip:
|
button_tooltip:
|
||||||
new: "Create new item"
|
new: "Create new item"
|
||||||
import: "Import"
|
import: "Update inventory"
|
||||||
filters: "Filters"
|
filters: "Filters"
|
||||||
search: "Quick search"
|
search: "Quick search"
|
||||||
filters:
|
filters:
|
||||||
|
@ -2192,11 +2192,12 @@ en:
|
||||||
import_records:
|
import_records:
|
||||||
update_inventory: 'Update inventory'
|
update_inventory: 'Update inventory'
|
||||||
steps:
|
steps:
|
||||||
step0:
|
step1:
|
||||||
id: 'step0'
|
id: 'step1'
|
||||||
icon: 'sn-icon-open'
|
icon: 'sn-icon-open'
|
||||||
label: 'Step 1'
|
label: 'Step 1'
|
||||||
title: 'Update inventory'
|
title: 'Update inventory'
|
||||||
|
subtitle: 'To add or edit items, export the inventory and reimport edited inventory.'
|
||||||
helpText: 'Help'
|
helpText: 'Help'
|
||||||
exportTitle: 'Export'
|
exportTitle: 'Export'
|
||||||
exportFullInvBtnText: 'Export full inventory'
|
exportFullInvBtnText: 'Export full inventory'
|
||||||
|
@ -2205,6 +2206,59 @@ en:
|
||||||
importBtnText: 'Import'
|
importBtnText: 'Import'
|
||||||
cancelBtnText: 'Cancel'
|
cancelBtnText: 'Cancel'
|
||||||
dragAndDropSupportingText: '.XLSX, .XLS or .CSV file'
|
dragAndDropSupportingText: '.XLSX, .XLS or .CSV file'
|
||||||
|
step2:
|
||||||
|
id: 'step2'
|
||||||
|
icon: 'sn-icon-open'
|
||||||
|
label: 'Step 2'
|
||||||
|
title: 'Mapping data'
|
||||||
|
subtitle: 'Match your imported columns with the columns in the SciNote inventory.'
|
||||||
|
selectNamePropertyError: 'Select Name attribute field to import your items.'
|
||||||
|
autoMappingText: 'Auto-mapping'
|
||||||
|
updateEmptyCellsText: 'Update empty cells'
|
||||||
|
onlyAddNewItemsText: 'Only add new items'
|
||||||
|
importedFileText: 'Imported file:'
|
||||||
|
cancelBtnText: 'Cancel'
|
||||||
|
confirmBtnText: 'Confirm'
|
||||||
|
importedIgnoredSection:
|
||||||
|
columnsTo: 'columns to'
|
||||||
|
import: 'import.'
|
||||||
|
columns: 'columns'
|
||||||
|
ignored: 'ignored.'
|
||||||
|
|
||||||
|
computedDropdownOptions:
|
||||||
|
name: 'Name'
|
||||||
|
RepositoryTextValue: 'Text'
|
||||||
|
RepositoryNumberValue: 'Number'
|
||||||
|
RepositoryAssetValue: 'File'
|
||||||
|
RepositoryChecklistValue: 'Checklist'
|
||||||
|
RepositoryDateRangeValue: 'Date range'
|
||||||
|
RepositoryDateTimeRangeValue: 'Date-time range'
|
||||||
|
RepositoryDateTimeValue: 'Date-time'
|
||||||
|
RepositoryDateValue: 'Date'
|
||||||
|
RepositoryListValue: 'List'
|
||||||
|
RepositoryStatusValue: 'Status'
|
||||||
|
RepositoryStockValue: 'Stock'
|
||||||
|
table:
|
||||||
|
tableRow:
|
||||||
|
defaultColumnTitle: 'Default column. Mapped as identifier.'
|
||||||
|
userDefinedColumnTitle: 'Column name does not match. Column will be imported as '
|
||||||
|
importedColumnTitle: 'Column will be imported.'
|
||||||
|
doNotImportColumnTitle: 'Column will not import.'
|
||||||
|
systemGeneratedData:
|
||||||
|
itemId: 'Item ID'
|
||||||
|
createdOn: 'Created on'
|
||||||
|
addedBy: 'Added by'
|
||||||
|
addedOn: 'Added on'
|
||||||
|
archivedBy: 'Archived by'
|
||||||
|
archivedOn: 'Archived on'
|
||||||
|
updatedBy: 'Updated by'
|
||||||
|
updatedOn: 'Updated on'
|
||||||
|
columnLabels:
|
||||||
|
number: 'No.'
|
||||||
|
importedColumns: 'Imported columns'
|
||||||
|
scinoteColumns: 'SciNote columns'
|
||||||
|
status: 'Status'
|
||||||
|
exampleData: 'Example data'
|
||||||
info_sidebar:
|
info_sidebar:
|
||||||
title: 'Guide for updating the inventory'
|
title: 'Guide for updating the inventory'
|
||||||
elements:
|
elements:
|
||||||
|
|
Loading…
Add table
Reference in a new issue