<template> <div> <div class="control"> <label class="control__label">Choose CSV file</label> <div class="control__description"> You can import an existing CSV by uploading the .CSV file with tabular data. Most spreadsheet applications will allow you to export your spreadsheet as a .CSV file. </div> <div class="control__elements"> <div class="file-upload"> <input v-show="false" ref="file" type="file" accept=".csv" @change="select($event)" /> <a class="button button--large button--ghost file-upload__button" @click.prevent="$refs.file.click($event)" > <i class="fas fa-cloud-upload-alt"></i> Choose CSV file </a> <div class="file-upload__file">{{ filename }}</div> </div> <div v-if="$v.filename.$error" class="error"> This field is required. </div> </div> </div> <div v-if="filename !== ''" class="row"> <div class="col col-4"> <div class="control"> <label class="control__label">Column separator</label> <div class="control__elements"> <Dropdown v-model="columnSeparator" @input="reload()"> <DropdownItem name="auto detect" value="auto"></DropdownItem> <DropdownItem name="," value=","></DropdownItem> <DropdownItem name=";" value=";"></DropdownItem> <DropdownItem name="|" value="|"></DropdownItem> <DropdownItem name="<tab>" value="\t"></DropdownItem> <DropdownItem name="record separator (30)" :value="String.fromCharCode(30)" ></DropdownItem> <DropdownItem name="unit separator (31)" :value="String.fromCharCode(31)" ></DropdownItem> </Dropdown> </div> </div> </div> <div class="col col-8"> <div class="control"> <label class="control__label">Encoding</label> <div class="control__elements"> <CharsetDropdown v-model="encoding" @input="reload()" ></CharsetDropdown> </div> </div> </div> </div> <div v-if="filename !== ''" class="row"> <div class="col col-6"> <div class="control"> <label class="control__label">First row is header</label> <div class="control__elements"> <Checkbox v-model="values.firstRowHeader" @input="reload()" >yes</Checkbox > </div> </div> </div> </div> <div v-if="error !== ''" class="alert alert--error alert--has-icon margin-top-1" > <div class="alert__icon"> <i class="fas fa-exclamation"></i> </div> <div class="alert__title">Something went wrong</div> <p class="alert__content"> {{ error }} </p> </div> <TableImporterPreview v-if="error === '' && Object.keys(preview).length !== 0" :preview="preview" ></TableImporterPreview> </div> </template> <script> import Papa from 'papaparse' import { required } from 'vuelidate/lib/validators' import form from '@baserow/modules/core/mixins/form' import CharsetDropdown from '@baserow/modules/core/components/helpers/CharsetDropdown' import importer from '@baserow/modules/database/mixins/importer' import TableImporterPreview from '@baserow/modules/database/components/table/TableImporterPreview' export default { name: 'TableCSVImporter', components: { TableImporterPreview, CharsetDropdown }, mixins: [form, importer], data() { return { values: { data: '', firstRowHeader: true, }, filename: '', columnSeparator: 'auto', encoding: 'utf-8', error: '', rawData: null, preview: {}, } }, validations: { values: { data: { required }, }, filename: { required }, }, methods: { /** * Method that is called when a file has been chosen. It will check if the file is * not larger than 15MB. Otherwise it will take a long time and possibly a crash * if so many entries have to be loaded into memory. If the file is valid, the * contents will be loaded into memory and the reload method will be called which * parses the content. */ select(event) { if (event.target.files.length === 0) { return } const file = event.target.files[0] const maxSize = 1024 * 1024 * 15 if (file.size > maxSize) { this.filename = '' this.values.data = '' this.error = 'The maximum file size is 15MB.' this.preview = {} this.$emit('input', this.value) } else { this.filename = file.name const reader = new FileReader() reader.addEventListener('load', (event) => { this.rawData = event.target.result this.reload() }) reader.readAsArrayBuffer(event.target.files[0]) } }, /** * Parses the raw data with the user configured delimiter. If all looks good the * data is stored as a string because all the entries don't have to be reactive. * Also a small preview will be generated. If something goes wrong, for example * when the CSV doesn't have any entries the appropriate error will be shown. */ reload() { const decoder = new TextDecoder(this.encoding) const decodedData = decoder.decode(this.rawData) const limit = this.$env.INITIAL_TABLE_DATA_LIMIT const count = decodedData.split(/\r\n|\r|\n/).length if (limit !== null && count > limit) { this.values.data = '' this.error = `It is not possible to import more than ${limit} rows.` this.preview = {} return } Papa.parse(decodedData, { delimiter: this.columnSeparator === 'auto' ? '' : this.columnSeparator, complete: (data) => { if (data.data.length === 0) { // We need at least a single entry otherwise the user has probably chosen // a wrong file. this.values.data = '' this.error = 'This CSV file is empty.' this.preview = {} } else { // If parsed successfully and it is not empty then the initial data can be // prepared for creating the table. We store the data stringified because // it doesn't need to be reactive. this.values.data = JSON.stringify(data.data) this.error = '' this.preview = this.getPreview( data.data, this.values.firstRowHeader ) } }, error(error) { // Papa parse has resulted in an error which we need to display to the user. // All previously loaded data will be removed. this.values.data = '' this.error = error.errors[0].message this.preview = {} }, }) }, }, } </script>