mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-26 17:51:13 +08:00
Merge pull request #7451 from lasniscinote/gl_SCI_10578
(dev) Create 'update inventory' modal first step [SCI-10578]
This commit is contained in:
commit
745c52a158
11 changed files with 462 additions and 46 deletions
|
@ -53,13 +53,21 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
|
||||
@display_edit_button = can_create_repository_rows?(@repository)
|
||||
@display_delete_button = can_delete_repository_rows?(@repository)
|
||||
@display_duplicate_button = can_create_repository_rows?(@repository)
|
||||
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
current_team_switch(@repository.team) unless @repository.shared_with?(current_team)
|
||||
@display_edit_button = can_create_repository_rows?(@repository)
|
||||
@display_delete_button = can_delete_repository_rows?(@repository)
|
||||
@display_duplicate_button = can_create_repository_rows?(@repository)
|
||||
@snapshot_provisioning = @repository.repository_snapshots.provisioning.any?
|
||||
|
||||
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
|
||||
@busy_printer = LabelPrinter.where.not(current_print_job_ids: []).first
|
||||
end
|
||||
format.json do
|
||||
# render serialized repository json
|
||||
render json: @repository, serializer: RepositorySerializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def table_toolbar
|
||||
|
@ -281,13 +289,9 @@ class RepositoriesController < ApplicationController
|
|||
session: session
|
||||
)
|
||||
if parsed_file.too_large?
|
||||
repository_response(t('general.file.size_exceeded',
|
||||
file_size: Rails.configuration.x.file_max_size_mb))
|
||||
return render json: { error: t('general.file.size_exceeded', file_size: Rails.configuration.x.file_max_size_mb) }, status: :unprocessable_entity
|
||||
elsif parsed_file.has_too_many_rows?
|
||||
repository_response(
|
||||
t('repositories.import_records.error_message.items_limit',
|
||||
items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT)
|
||||
)
|
||||
return render json: { error: t('repositories.import_records.error_message.items_limit', items_size: Constants::IMPORT_REPOSITORY_ITEMS_LIMIT) }, status: :unprocessable_entity
|
||||
else
|
||||
sheet = SpreadsheetParser.open_spreadsheet(import_params[:file])
|
||||
duplicate_ids = SpreadsheetParser.duplicate_ids(sheet)
|
||||
|
@ -298,22 +302,22 @@ class RepositoriesController < ApplicationController
|
|||
@import_data = parsed_file.data
|
||||
|
||||
if @import_data.header.blank? || @import_data.columns.blank?
|
||||
return repository_response(t('repositories.parse_sheet.errors.empty_file'))
|
||||
return render json: { error: t('repositories.parse_sheet.errors.empty_file') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if (@temp_file = parsed_file.generate_temp_file)
|
||||
render json: {
|
||||
html: render_to_string(partial: 'repositories/parse_records_modal', formats: :html)
|
||||
import_data: @import_data,
|
||||
temp_file: @temp_file
|
||||
}
|
||||
else
|
||||
repository_response(t('repositories.parse_sheet.errors.temp_file_failure'))
|
||||
return render json: { error: t('repositories.parse_sheet.errors.temp_file_failure') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
repository_response(t('repositories.parse_sheet.errors.invalid_file',
|
||||
encoding: ''.encoding))
|
||||
return render json: { error: t('repositories.parse_sheet.errors.invalid_file', encoding: ''.encoding) }, status: :unprocessable_entity
|
||||
rescue TypeError
|
||||
repository_response(t('repositories.parse_sheet.errors.invalid_extension'))
|
||||
return render json: { error: t('repositories.parse_sheet.errors.invalid_extension') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -326,7 +330,7 @@ class RepositoriesController < ApplicationController
|
|||
should_overwrite_with_empty_cells = params[:overwrite_with_empty_cells]
|
||||
|
||||
# Check if there exist mapping for repository record (it's mandatory)
|
||||
if import_params[:mappings].value?('-1')
|
||||
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
|
||||
import_records = repostiory_import_actions
|
||||
status = import_records.import!(can_edit_existing_items, should_overwrite_with_empty_cells)
|
||||
|
||||
|
|
|
@ -54,9 +54,7 @@ const app = createApp({
|
|||
// Info modal
|
||||
infoParams: {
|
||||
title: 'Guide for updating the inventory',
|
||||
modalTitle: 'Update inventory',
|
||||
helpText: 'Help',
|
||||
steps: [
|
||||
elements: [
|
||||
{
|
||||
id: 'el1',
|
||||
icon: 'sn-icon-export',
|
||||
|
|
|
@ -1,26 +1,91 @@
|
|||
<template>
|
||||
<infoModal ref="modal" :startHidden="true" :infoParams="{}" :title="'UPDATE'" :helpText="'HELP ME'">
|
||||
UPDAE
|
||||
</infoModal>
|
||||
<InfoModal ref="modal"
|
||||
:startHidden="true"
|
||||
:infoParams="infoParams"
|
||||
:title="steps[activeStep].title"
|
||||
:helpText="steps[activeStep].helpText">
|
||||
<component
|
||||
:is="steps[activeStep].component"
|
||||
@step:next="proceedToNext"
|
||||
@step:back="activeStep -= 1"
|
||||
:stepData="stepData"
|
||||
/>
|
||||
</InfoModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import { shallowRef } from 'vue';
|
||||
import InfoModal from '../../shared/info_modal.vue';
|
||||
import FirstStep from './import/first_step.vue';
|
||||
|
||||
export default {
|
||||
name: 'ImportRepositoryModal',
|
||||
components: {InfoModal},
|
||||
components: { InfoModal, FirstStep },
|
||||
props: {
|
||||
repositoryUrl: String, required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
infoParams: {}
|
||||
activeStep: 0,
|
||||
steps: [
|
||||
{
|
||||
id: I18n.t('repositories.import_records.steps.step0.id'),
|
||||
icon: I18n.t('repositories.import_records.steps.step0.icon'),
|
||||
label: I18n.t('repositories.import_records.steps.step0.label'),
|
||||
title: I18n.t('repositories.import_records.steps.step0.title'),
|
||||
helpText: I18n.t('repositories.import_records.steps.step0.helpText'),
|
||||
component: shallowRef(FirstStep)
|
||||
}
|
||||
],
|
||||
infoParams: {
|
||||
title: I18n.t('repositories.import_records.info_sidebar.title'),
|
||||
elements: [
|
||||
{
|
||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element0.id'),
|
||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element0.icon'),
|
||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element0.label'),
|
||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element0.subtext')
|
||||
},
|
||||
{
|
||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element1.id'),
|
||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element1.icon'),
|
||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element1.label'),
|
||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element1.subtext')
|
||||
},
|
||||
{
|
||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element2.id'),
|
||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element2.icon'),
|
||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element2.label'),
|
||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element2.subtext')
|
||||
},
|
||||
{
|
||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element3.id'),
|
||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element3.icon'),
|
||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element3.label'),
|
||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element3.subtext')
|
||||
},
|
||||
{
|
||||
id: I18n.t('repositories.import_records.info_sidebar.elements.element4.id'),
|
||||
icon: I18n.t('repositories.import_records.info_sidebar.elements.element4.icon'),
|
||||
label: I18n.t('repositories.import_records.info_sidebar.elements.element4.label'),
|
||||
subtext: I18n.t('repositories.import_records.info_sidebar.elements.element4.subtext'),
|
||||
linkTo: 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() {
|
||||
window.importRepositoryModalComponent = this;
|
||||
},
|
||||
|
@ -29,6 +94,11 @@ export default {
|
|||
methods: {
|
||||
open() {
|
||||
this.$refs.modal.open();
|
||||
},
|
||||
proceedToNext(data) {
|
||||
console.log('incoming data', data);
|
||||
this.stepData = data;
|
||||
this.activeStep += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
175
app/javascript/vue/repositories/modals/import/first_step.vue
Normal file
175
app/javascript/vue/repositories/modals/import/first_step.vue
Normal file
|
@ -0,0 +1,175 @@
|
|||
<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.step0.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.step0.exportFullInvBtnText') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step0.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.step0.importBtnText') }}
|
||||
</h3>
|
||||
<DragAndDropUpload
|
||||
@file:dropped="uploadFile"
|
||||
@file:error="handleError"
|
||||
@file:error:clear="this.error = null"
|
||||
:supportingText="`${i18n.t('repositories.import_records.steps.step0.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.step0.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'],
|
||||
components: {
|
||||
DragAndDropUpload
|
||||
},
|
||||
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 the necessary data
|
||||
if (parsedSheetResponse) {
|
||||
const {
|
||||
header: columnNames,
|
||||
available_fields: availableFields,
|
||||
columns: exampleData
|
||||
} = parsedSheetResponse.data.import_data;
|
||||
this.$emit('step:next', { columnNames, availableFields, exampleData });
|
||||
}
|
||||
},
|
||||
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>
|
125
app/javascript/vue/shared/drag_and_drop_upload.vue
Normal file
125
app/javascript/vue/shared/drag_and_drop_upload.vue
Normal file
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div
|
||||
ref="dragAndDropUpload"
|
||||
@drop.prevent="dropFile"
|
||||
@dragenter.prevent="dragEnter($event)"
|
||||
@dragleave.prevent="dragLeave($event)"
|
||||
@dragover.prevent
|
||||
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue"
|
||||
>
|
||||
<div id="centered-content" class="flex flex-col gap-4 items-center h-fit w-fit m-auto">
|
||||
<!-- icon -->
|
||||
<i class="sn-icon sn-icon-import text-sn-dark-grey"></i>
|
||||
|
||||
<!-- text section -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sn-dark-grey">
|
||||
<span class="text-sn-science-blue hover:cursor-pointer" @click="handleImportClick">
|
||||
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
||||
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
||||
</div>
|
||||
<div class="text-sn-grey">
|
||||
{{ supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hidden input for importing via 'Import' click -->
|
||||
<input type="file" ref="fileInput" style="display: none" @change="handleFileSelect">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DragAndDropUpload',
|
||||
props: {
|
||||
supportingText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
supportedFormats: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['file:dropped', 'file:error'],
|
||||
data() {
|
||||
return {
|
||||
draggingFile: false,
|
||||
uploading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
validateFile(file) {
|
||||
// check if it's a single file
|
||||
if (file.length > 1) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.notSingleFileError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's a correct file type
|
||||
const fileExtension = file.name.split('.')[1];
|
||||
if (!this.supportedFormats.includes(fileExtension)) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.wrongFileTypeError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if file is not empty
|
||||
if (!file.size > 0) {
|
||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.emptyFileError');
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's conforming to size limit
|
||||
if (file.size > GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB * 1024 * 1024) {
|
||||
const error = `${I18n.t('repositories.import_records.dragAndDropUpload.fileTooLargeError')} ${GLOBAL_CONSTANTS.FILE_MAX_SIZE_MB}`;
|
||||
this.$emit('file:error', error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
dragEnter(e) {
|
||||
// Detect if dragged element is a file
|
||||
// https://stackoverflow.com/a/8494918
|
||||
const dt = e.dataTransfer;
|
||||
if (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))) {
|
||||
this.draggingFile = true;
|
||||
}
|
||||
},
|
||||
dragLeave() {
|
||||
this.draggingFile = false;
|
||||
},
|
||||
dropFile(e) {
|
||||
if (e.dataTransfer && e.dataTransfer.files.length) {
|
||||
this.draggingFile = false;
|
||||
this.uploading = true;
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
|
||||
const fileIsValid = this.validateFile(droppedFile);
|
||||
|
||||
// successful drop
|
||||
if (fileIsValid) {
|
||||
this.$emit('file:dropped', droppedFile);
|
||||
this.$emit('file:error:clear');
|
||||
}
|
||||
}
|
||||
},
|
||||
handleImportClick() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
const fileIsValid = this.validateFile(file);
|
||||
|
||||
if (fileIsValid) {
|
||||
this.$emit('file:dropped', file);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="!w-[300px] rounded bg-sn-super-light-grey gap-4 p-6 flex flex-col h-full">
|
||||
<div id="info-component-header">
|
||||
<h3 class="modal-title text-sn-dark-grey">{{ params.title }}</h3>
|
||||
<h3 class="modal-title text-sn-dark-grey">{{ infoParams.title }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-row h-fit" v-for="(element, _index) in params.elements" :key="element.id">
|
||||
<div class="grid grid-flow-row h-fit" v-for="(element, _index) in infoParams.elements" :key="element.id">
|
||||
<a v-if="element.linkTo" :href="element.linkTo" target="_blank" class="flex flex-row gap-3 w-fit text-sn-blue hover:no-underline hover:text-sn-blue-hover">
|
||||
<button class="btn btn-secondary btn-sm icon-btn hover:!border-sn-light-grey">
|
||||
<i :class="element.icon" class="h-fit"></i>
|
||||
<i :class="`sn-icon ${element.icon}`" class="h-fit size-9"></i>
|
||||
</button>
|
||||
<div class="flex flex-col gap-2 w-fit">
|
||||
<div class="text-sn-blue font-bold hover:text-sn-blue-hover my-auto">{{ element.label }}</div>
|
||||
|
@ -30,7 +30,7 @@
|
|||
export default {
|
||||
name: 'InfoComponent',
|
||||
props: {
|
||||
params: {
|
||||
infoParams: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<div id="body-container" class="flex flex-row w-full h-full">
|
||||
<!-- info -->
|
||||
<div id="info-part">
|
||||
<info-component
|
||||
<InfoComponent
|
||||
v-if="showingInfo"
|
||||
:params="this.infoParams"
|
||||
:infoParams="infoParams"
|
||||
/>
|
||||
</div>
|
||||
<!-- content -->
|
||||
<div id="content-part" class="flex flex-col w-full p-6 gap-3">
|
||||
<div id="content-part" class="flex flex-col w-full p-6 gap-6">
|
||||
<!-- header -->
|
||||
<div id="info-modal-header" class="flex flex-row h-fit w-full justify-between">
|
||||
<div id="title-with-help" class="flex flex-row gap-3">
|
||||
|
@ -26,7 +26,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<!-- main content -->
|
||||
<div id="info-modal-main-content">
|
||||
<div id="info-modal-main-content" class="h-full">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,10 +44,12 @@ export default {
|
|||
name: 'InfoModal',
|
||||
props: {
|
||||
title: {
|
||||
type: String, required: true
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
helpText: {
|
||||
type: String, required: true
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
infoParams: {
|
||||
type: Object,
|
||||
|
@ -59,9 +61,7 @@ export default {
|
|||
}
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
components: {
|
||||
'info-component': InfoComponent
|
||||
},
|
||||
components: { InfoComponent },
|
||||
data() {
|
||||
return {
|
||||
showingInfo: true
|
||||
|
|
|
@ -21,5 +21,5 @@ export default {
|
|||
this.$emit('open');
|
||||
$(this.$refs.modal).modal('show');
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
13
app/serializers/repository_serializer.rb
Normal file
13
app/serializers/repository_serializer.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositorySerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :urls, :id, :team_id
|
||||
|
||||
def urls
|
||||
{
|
||||
parse_sheet: parse_sheet_repository_path(object)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -12,13 +12,16 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div ref="infoModal">
|
||||
<div ref="infoModalWrapper">
|
||||
<button @click="showInfo = true" class="btn btn-primary">Show Info Modal</button>
|
||||
<info-modal
|
||||
v-if="showInfo"
|
||||
@close="showInfo = false"
|
||||
:infoParams="infoParams"
|
||||
>
|
||||
ref="modal"
|
||||
:start-hidden="false"
|
||||
:info-params="infoParams"
|
||||
title="Some title"
|
||||
help-text="Help">
|
||||
<div>I am a component that gets consumed by the slot</div>
|
||||
</info-modal>
|
||||
</div>
|
||||
|
|
|
@ -2135,29 +2135,57 @@ en:
|
|||
list_error: "%{key}: %{val}"
|
||||
import_records:
|
||||
update_inventory: 'Update inventory'
|
||||
steps:
|
||||
step0:
|
||||
id: 'step0'
|
||||
icon: 'sn-icon-open'
|
||||
label: 'Step 1'
|
||||
title: 'Update inventory'
|
||||
helpText: 'Help'
|
||||
exportTitle: 'Export'
|
||||
exportFullInvBtnText: 'Export full inventory'
|
||||
exportEmptyInvBtnText: 'Export empty inventory'
|
||||
importTitle: 'Import'
|
||||
importBtnText: 'Import'
|
||||
cancelBtnText: 'Cancel'
|
||||
dragAndDropSupportingText: '.XLSX, .XLS or .CSV file'
|
||||
info_sidebar:
|
||||
title: 'Guide for updating the inventory'
|
||||
elements:
|
||||
element0:
|
||||
id: 'el0'
|
||||
icon: 'sn-icon-export'
|
||||
label: 'Export inventory'
|
||||
subtext: "Before making edits, we advise you to export the latest inventory information. If you're only adding new items, consider exporting empty inventory."
|
||||
element1:
|
||||
id: 'el1'
|
||||
icon: 'sn-icon-edit'
|
||||
label: 'Edit your data'
|
||||
subtext: 'Make sure to include header names in first row, followed by item data.'
|
||||
element2:
|
||||
id: 'el2'
|
||||
icon: 'sn-icon-import'
|
||||
label: 'Import new or update items'
|
||||
subtext: 'Upload your data using .xlsx, .csv or .txt files.'
|
||||
element3:
|
||||
id: 'el3'
|
||||
icon: 'sn-icon-tables'
|
||||
label: 'Merge your data'
|
||||
subtext: 'Complete the process by merging the columns you want to update.'
|
||||
element4:
|
||||
id: 'el4'
|
||||
icon: 'sn-icon-open'
|
||||
label: 'Learn more'
|
||||
linkTo: 'https://knowledgebase.scinote.net/en/knowledge/how-to-add-items-to-an-inventory'
|
||||
dragAndDropUpload:
|
||||
notSingleFileError: 'Single file import only. Please import one file at a time.'
|
||||
wrongFileTypeError: 'The file has invalid extension (.csv, .xlsx, .txt or .tsv.)'
|
||||
emptyFileError: 'You have uploaded empty file. There is not much to import.'
|
||||
fileTooLargeError: 'File too large. Max file size limit is'
|
||||
importText:
|
||||
firstPart: 'Import'
|
||||
secondPart: 'or drag and drop'
|
||||
|
||||
import: 'Import'
|
||||
no_header_name: 'No column name'
|
||||
success_flash: "%{number_of_rows} of %{total_nr} new item(s) successfully imported."
|
||||
|
|
Loading…
Reference in a new issue