/** * Mixin that introduces helper methods for the importer form component. */ import { RESERVED_BASEROW_FIELD_NAMES, MAX_FIELD_NAME_LENGTH, } from '@baserow/modules/database/utils/constants' const IMPORT_PREVIEW_MAX_ROW_COUNT = 6 export default { props: { mapping: { type: Object, required: false, default: () => { return {} }, }, }, data() { return { fileLoadingProgress: 0, state: null, error: '', values: {}, } }, computed: { stateTitle() { return this.$t(`importer.${this.state}`) }, }, methods: { resetImporterState() { this.state = null this.error = '' this.values = {} this.$emit('getData', null) this.$emit('data', { header: [], previewData: [] }) }, handleImporterError(error) { this.resetImporterState() this.fileLoadingProgress = 0 this.error = error }, /** * Adds a header of Field 1, Field 2, etc. if the header is empty, * otherwise checks that the existing header has valid and non duplicate field * names. If there are invalid or duplicate field names they will be replaced with * valid unique field names instead. * * @param header An array starting of the column name. * @param data An array starting with a header row if firstRowHeader is true, * followed by rows of data. * @return {*} An updated data object with the first row being a valid unique * header row. */ prepareHeader(header = [], data) { const columnCount = Math.max.apply( null, data.map((entry) => entry.length) ) // If the first row is not the header, a header containing columns named // 'Field N' needs to be generated. if (!header || header.length === 0) { const newHead = [] for (let i = 1; i <= columnCount; i++) { newHead.push(this.$t(`importer.fieldDefaultName`, { count: i })) } return newHead } else { // The header row might not be long enough to cover all columns, ensure it does // first. const newHead = header.map((value) => `${value}`) for (let i = newHead.length; i < columnCount; i++) { newHead.push(this.$t(`importer.fieldDefaultName`, { count: i })) } return this.makeHeaderUniqueAndValid(newHead) } }, /** * Fills the row with a minimum amount of empty columns. */ fill(row, maxLength) { for (let i = row.length; i < maxLength; i++) { row.push('') } return row }, /** * Generates an object that can used to render a quick preview of the provided * data. */ getPreview(head, data) { const rows = data.slice(0, IMPORT_PREVIEW_MAX_ROW_COUNT) const columns = Math.max.apply( null, data.map((entry) => entry.length) ) rows.map((row) => this.fill(row, columns)) return rows }, /** * Find the next un-unused column not present or used yet in the nextFreeIndexMap. * Will append a number to the returned columnName if it is taken, where that * number ensures the returned name is unique. Will respect the maximum allowed * field name length. Finally this function will update * the nextFreeIndexMap so future calls will not use any columns returned by * this function. * @param originalColumnName The column name to find the next free unique value for. * @param nextFreeIndexMap A map of column name to next free starting index. * @param startingIndex The starting index to start from if no index is found in * the map. * @return {string} A column name possibly postfixed with a number to ensure it * is unique. */ findNextFreeName(originalColumnName, nextFreeIndexMap, startingIndex) { let i = nextFreeIndexMap.get(originalColumnName) || startingIndex while (true) { const suffixToAppend = ` ${i}` let nextColumnNameToCheck // If appending a number to the columnName in order to make it // unique will return a string that is longer than the maximum // allowed field name length, we need to further slice the // columnName as to not go above the maximum allowed length. if ( originalColumnName.length + suffixToAppend.length > MAX_FIELD_NAME_LENGTH ) { nextColumnNameToCheck = `${originalColumnName.slice( 0, -suffixToAppend.length )}${suffixToAppend}` } else { nextColumnNameToCheck = `${originalColumnName}${suffixToAppend}` } if (!nextFreeIndexMap.has(nextColumnNameToCheck)) { nextFreeIndexMap.set(originalColumnName, i + 1) return nextColumnNameToCheck } i++ } }, /** * Given a column name this function will return a new name which is guaranteed * to be unique and valid. If the originally provided name is unique and valid * then it will be returned untouched. * * @param column The column name to check. * @param nextFreeIndexMap A map of column names to an index number. A value of 0 * indicates that the key is a column name which exists in the table but has not * yet been returned yet. A number higher than 0 indicates that the column has * already occurred and the index needs to be appended to the name to generate a * new unique column name. * @return {string|*} A valid unique column name. */ makeColumnNameUniqueAndValidIfNotAlready(column, nextFreeIndexMap) { if (column === '') { return this.findNextFreeName('Field', nextFreeIndexMap, 1) } else if (RESERVED_BASEROW_FIELD_NAMES.includes(column)) { return this.findNextFreeName(column, nextFreeIndexMap, 2) } else if (nextFreeIndexMap.get(column) > 0) { return this.findNextFreeName(column, nextFreeIndexMap, 2) } else { nextFreeIndexMap.set(column, 2) return column } }, /** * Ensures that the uploaded field names are unique, non blank, don't exceed * the maximum field name length and don't use any reserved Baserow field names. * @param {*[]} head An array of field names to be checked. * @return A new array of field names which are guaranteed to be unique and valid. */ makeHeaderUniqueAndValid(head) { const nextFreeIndexMap = new Map() for (let i = 0; i < head.length; i++) { const truncatedColumn = head[i].trim().slice(0, MAX_FIELD_NAME_LENGTH) nextFreeIndexMap.set(truncatedColumn, 0) } const uniqueAndValidHeader = [] for (let i = 0; i < head.length; i++) { const column = head[i] const trimmedColumn = column.trim() const truncatedColumn = trimmedColumn.slice(0, MAX_FIELD_NAME_LENGTH) const uniqueValidName = this.makeColumnNameUniqueAndValidIfNotAlready( truncatedColumn, nextFreeIndexMap ) uniqueAndValidHeader.push(uniqueValidName) } return uniqueAndValidHeader }, }, }