mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
853 lines
31 KiB
TypeScript
853 lines
31 KiB
TypeScript
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
* contact@kingsrook.com
|
|
* https://github.com/Kingsrook/
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
|
|
|
export type ValueType = "defaultValue" | "column";
|
|
|
|
/***************************************************************************
|
|
** model of a single field that's part of a bulk-load profile/mapping
|
|
***************************************************************************/
|
|
export class BulkLoadField
|
|
{
|
|
field: QFieldMetaData;
|
|
tableStructure: BulkLoadTableStructure;
|
|
|
|
valueType: ValueType;
|
|
columnIndex?: number;
|
|
headerName?: string = null;
|
|
defaultValue?: any = null;
|
|
doValueMapping: boolean = false;
|
|
|
|
wideLayoutIndexPath: number[] = [];
|
|
|
|
error: string = null;
|
|
warning: string = null;
|
|
|
|
key: string;
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
|
|
{
|
|
this.field = field;
|
|
this.tableStructure = tableStructure;
|
|
this.valueType = valueType;
|
|
this.columnIndex = columnIndex;
|
|
this.headerName = headerName;
|
|
this.defaultValue = defaultValue;
|
|
this.doValueMapping = doValueMapping;
|
|
this.wideLayoutIndexPath = wideLayoutIndexPath;
|
|
this.error = error;
|
|
this.warning = warning;
|
|
this.key = new Date().getTime().toString();
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public static clone(source: BulkLoadField): BulkLoadField
|
|
{
|
|
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning));
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getQualifiedName(): string
|
|
{
|
|
if (this.tableStructure.isMain)
|
|
{
|
|
return this.field.name;
|
|
}
|
|
|
|
return this.tableStructure.associationPath + "." + this.field.name;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getQualifiedNameWithWideSuffix(): string
|
|
{
|
|
let wideLayoutSuffix = "";
|
|
if (this.wideLayoutIndexPath.length > 0)
|
|
{
|
|
wideLayoutSuffix = "." + this.wideLayoutIndexPath.map(i => i + 1).join(".");
|
|
}
|
|
|
|
if (this.tableStructure.isMain)
|
|
{
|
|
return this.field.name + wideLayoutSuffix;
|
|
}
|
|
|
|
return this.tableStructure.associationPath + "." + this.field.name + wideLayoutSuffix;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getKey(): string
|
|
{
|
|
let wideLayoutSuffix = "";
|
|
if (this.wideLayoutIndexPath.length > 0)
|
|
{
|
|
wideLayoutSuffix = "." + this.wideLayoutIndexPath.map(i => i + 1).join(".");
|
|
}
|
|
|
|
if (this.tableStructure.isMain)
|
|
{
|
|
return this.field.name + wideLayoutSuffix + this.key;
|
|
}
|
|
|
|
return this.tableStructure.associationPath + "." + this.field.name + wideLayoutSuffix + this.key;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getQualifiedLabel(): string
|
|
{
|
|
let wideLayoutSuffix = "";
|
|
if (this.wideLayoutIndexPath.length > 0)
|
|
{
|
|
wideLayoutSuffix = " (" + this.wideLayoutIndexPath.map(i => i + 1).join(", ") + ")";
|
|
}
|
|
|
|
if (this.tableStructure.isMain)
|
|
{
|
|
return this.field.label + wideLayoutSuffix;
|
|
}
|
|
|
|
return this.tableStructure.label + ": " + this.field.label + wideLayoutSuffix;
|
|
}
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public isMany(): boolean
|
|
{
|
|
return this.tableStructure && this.tableStructure.isMany;
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** this is a type defined in qqq backend - a representation of a bulk-load
|
|
** table - e.g., how it fits into qqq - and of note - how child / association
|
|
** tables are nested too.
|
|
***************************************************************************/
|
|
export interface BulkLoadTableStructure
|
|
{
|
|
isMain: boolean;
|
|
isMany: boolean;
|
|
tableName: string;
|
|
label: string;
|
|
associationPath: string;
|
|
fields: QFieldMetaData[];
|
|
associations: BulkLoadTableStructure[];
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
** this is the internal data structure that the UI works with - but notably,
|
|
** is not how we send it to the backend or how backend saves profiles -- see
|
|
** BulkLoadProfile for that.
|
|
*******************************************************************************/
|
|
export class BulkLoadMapping
|
|
{
|
|
fields: { [qualifiedName: string]: BulkLoadField } = {};
|
|
fieldsByTablePrefix: { [prefix: string]: { [qualifiedFieldName: string]: BulkLoadField } } = {};
|
|
tablesByPath: { [path: string]: BulkLoadTableStructure } = {};
|
|
|
|
requiredFields: BulkLoadField[] = [];
|
|
additionalFields: BulkLoadField[] = [];
|
|
unusedFields: BulkLoadField[] = [];
|
|
|
|
valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {};
|
|
|
|
hasHeaderRow: boolean;
|
|
layout: string;
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
constructor(tableStructure: BulkLoadTableStructure)
|
|
{
|
|
if (tableStructure)
|
|
{
|
|
this.processTableStructure(tableStructure);
|
|
|
|
if (!tableStructure.associations)
|
|
{
|
|
this.layout = "FLAT";
|
|
}
|
|
}
|
|
|
|
this.hasHeaderRow = true;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
private processTableStructure(tableStructure: BulkLoadTableStructure)
|
|
{
|
|
const prefix = tableStructure.isMain ? "" : tableStructure.associationPath;
|
|
this.fieldsByTablePrefix[prefix] = {};
|
|
this.tablesByPath[prefix] = tableStructure;
|
|
|
|
for (let field of tableStructure.fields)
|
|
{
|
|
// todo delete this - backend should only give it to us if editable: if (field.isEditable)
|
|
{
|
|
const bulkLoadField = new BulkLoadField(field, tableStructure);
|
|
const qualifiedName = bulkLoadField.getQualifiedName();
|
|
this.fields[qualifiedName] = bulkLoadField;
|
|
this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField;
|
|
|
|
if (tableStructure.isMain && field.isRequired)
|
|
{
|
|
this.requiredFields.push(bulkLoadField);
|
|
}
|
|
else
|
|
{
|
|
this.unusedFields.push(bulkLoadField);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let associatedTableStructure of tableStructure.associations ?? [])
|
|
{
|
|
this.processTableStructure(associatedTableStructure);
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** take a saved bulk load profile - and convert it into a working bulkLoadMapping
|
|
** for the frontend to use!
|
|
***************************************************************************/
|
|
public static fromSavedProfileRecord(tableStructure: BulkLoadTableStructure, profileRecord: QRecord): BulkLoadMapping
|
|
{
|
|
const bulkLoadProfile = JSON.parse(profileRecord.values.get("mappingJson")) as BulkLoadProfile;
|
|
return BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile);
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** take a saved bulk load profile - and convert it into a working bulkLoadMapping
|
|
** for the frontend to use!
|
|
***************************************************************************/
|
|
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile): BulkLoadMapping
|
|
{
|
|
const bulkLoadMapping = new BulkLoadMapping(tableStructure);
|
|
|
|
if (bulkLoadProfile.version == "v1")
|
|
{
|
|
bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow;
|
|
bulkLoadMapping.layout = bulkLoadProfile.layout;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, //
|
|
// or it's an additional field, in which case, we'll go through the addField method to move what list it's in //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
function getBulkLoadFieldMovingFromUnusedToActiveIfNeeded(bulkLoadMapping: BulkLoadMapping, name: string): BulkLoadField
|
|
{
|
|
let wideIndex: number = null;
|
|
if (name.match(/,\d+$/))
|
|
{
|
|
wideIndex = Number(name.match(/\d+$/));
|
|
name = name.replace(/,\d+$/, "");
|
|
}
|
|
|
|
for (let field of bulkLoadMapping.requiredFields)
|
|
{
|
|
if (field.getQualifiedName() == name)
|
|
{
|
|
return (field);
|
|
}
|
|
}
|
|
|
|
for (let field of bulkLoadMapping.unusedFields)
|
|
{
|
|
if (field.getQualifiedName() == name)
|
|
{
|
|
const addedField = bulkLoadMapping.addField(field, wideIndex);
|
|
return (addedField);
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////
|
|
// loop over fields in the profile - adding them to the mapping //
|
|
//////////////////////////////////////////////////////////////////
|
|
for (let bulkLoadProfileField of ((bulkLoadProfile.fieldList ?? []) as BulkLoadProfileField[]))
|
|
{
|
|
const bulkLoadField = getBulkLoadFieldMovingFromUnusedToActiveIfNeeded(bulkLoadMapping, bulkLoadProfileField.fieldName);
|
|
if (!bulkLoadField)
|
|
{
|
|
console.log(`Couldn't find bulk-load-field by name from profile record [${bulkLoadProfileField.fieldName}]`);
|
|
continue;
|
|
}
|
|
|
|
if ((bulkLoadProfileField.columnIndex != null && bulkLoadProfileField.columnIndex != undefined) || (bulkLoadProfileField.headerName != null && bulkLoadProfileField.headerName != undefined))
|
|
{
|
|
bulkLoadField.valueType = "column";
|
|
bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping;
|
|
bulkLoadField.headerName = bulkLoadProfileField.headerName;
|
|
bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex;
|
|
|
|
if (bulkLoadProfileField.valueMappings)
|
|
{
|
|
bulkLoadMapping.valueMappings[bulkLoadProfileField.fieldName] = {};
|
|
for (let fileValue in bulkLoadProfileField.valueMappings)
|
|
{
|
|
////////////////////////////////////////////////////
|
|
// frontend wants string values here, so, string. //
|
|
////////////////////////////////////////////////////
|
|
bulkLoadMapping.valueMappings[bulkLoadProfileField.fieldName][String(fileValue)] = bulkLoadProfileField.valueMappings[fileValue];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bulkLoadField.valueType = "defaultValue";
|
|
bulkLoadField.defaultValue = bulkLoadProfileField.defaultValue;
|
|
}
|
|
}
|
|
|
|
return (bulkLoadMapping);
|
|
}
|
|
else
|
|
{
|
|
throw ("Unexpected version for bulk load profile: " + bulkLoadProfile.version);
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** take a working bulkLoadMapping from the frontend, and convert it to a
|
|
** BulkLoadProfile for the backend / for us to save.
|
|
***************************************************************************/
|
|
public toProfile(): { haveErrors: boolean, profile: BulkLoadProfile }
|
|
{
|
|
let haveErrors = false;
|
|
const profile = new BulkLoadProfile();
|
|
|
|
profile.version = "v1";
|
|
profile.hasHeaderRow = this.hasHeaderRow;
|
|
profile.layout = this.layout;
|
|
|
|
for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields])
|
|
{
|
|
let fullFieldName = (bulkLoadField.tableStructure.isMain ? "" : bulkLoadField.tableStructure.associationPath + ".") + bulkLoadField.field.name;
|
|
if (bulkLoadField.wideLayoutIndexPath != null && bulkLoadField.wideLayoutIndexPath != undefined && bulkLoadField.wideLayoutIndexPath.length)
|
|
{
|
|
fullFieldName += "," + bulkLoadField.wideLayoutIndexPath.join(".");
|
|
}
|
|
|
|
bulkLoadField.error = null;
|
|
if (bulkLoadField.valueType == "column")
|
|
{
|
|
if (bulkLoadField.columnIndex == undefined || bulkLoadField.columnIndex == null)
|
|
{
|
|
haveErrors = true;
|
|
bulkLoadField.error = "You must select a column.";
|
|
}
|
|
else
|
|
{
|
|
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping};
|
|
|
|
if (this.valueMappings[fullFieldName])
|
|
{
|
|
field.valueMappings = this.valueMappings[fullFieldName];
|
|
}
|
|
|
|
profile.fieldList.push(field);
|
|
}
|
|
}
|
|
else if (bulkLoadField.valueType == "defaultValue")
|
|
{
|
|
if (bulkLoadField.defaultValue == undefined || bulkLoadField.defaultValue == null || bulkLoadField.defaultValue == "")
|
|
{
|
|
haveErrors = true;
|
|
bulkLoadField.error = "A value is required.";
|
|
}
|
|
else
|
|
{
|
|
profile.fieldList.push({fieldName: fullFieldName, defaultValue: bulkLoadField.defaultValue});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {haveErrors, profile};
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public addField(bulkLoadField: BulkLoadField, specifiedWideIndex?: number): BulkLoadField
|
|
{
|
|
if (bulkLoadField.isMany() && this.layout == "WIDE")
|
|
{
|
|
let index: number;
|
|
if (specifiedWideIndex != null && specifiedWideIndex != undefined)
|
|
{
|
|
index = specifiedWideIndex;
|
|
}
|
|
else
|
|
{
|
|
///////////////////////////////////////////////
|
|
// find the max index for this field already //
|
|
///////////////////////////////////////////////
|
|
let maxIndex = -1;
|
|
for (let existingField of [...this.requiredFields, ...this.additionalFields])
|
|
{
|
|
if (existingField.getQualifiedName() == bulkLoadField.getQualifiedName())
|
|
{
|
|
const thisIndex = existingField.wideLayoutIndexPath[0];
|
|
if (thisIndex != null && thisIndex != undefined && thisIndex > maxIndex)
|
|
{
|
|
maxIndex = thisIndex;
|
|
}
|
|
}
|
|
}
|
|
index = maxIndex + 1;
|
|
}
|
|
|
|
const cloneField = BulkLoadField.clone(bulkLoadField);
|
|
cloneField.wideLayoutIndexPath = [index];
|
|
this.additionalFields.push(cloneField);
|
|
return (cloneField);
|
|
}
|
|
else
|
|
{
|
|
this.additionalFields.push(bulkLoadField);
|
|
return (bulkLoadField);
|
|
}
|
|
}
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public removeField(toRemove: BulkLoadField): void
|
|
{
|
|
const newAdditionalFields: BulkLoadField[] = [];
|
|
for (let bulkLoadField of this.additionalFields)
|
|
{
|
|
if (bulkLoadField.getQualifiedNameWithWideSuffix() != toRemove.getQualifiedNameWithWideSuffix())
|
|
{
|
|
newAdditionalFields.push(bulkLoadField);
|
|
}
|
|
}
|
|
|
|
this.additionalFields = newAdditionalFields;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public switchLayout(newLayout: string): void
|
|
{
|
|
const newAdditionalFields: BulkLoadField[] = [];
|
|
let anyChanges = false;
|
|
|
|
if ("WIDE" != newLayout)
|
|
{
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// if going to a layout other than WIDE, make sure there aren't any fields with a wideLayoutIndexPath //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const namesWhereOneWideLayoutIndexHasBeenFound: { [name: string]: boolean } = {};
|
|
for (let existingField of this.additionalFields)
|
|
{
|
|
if (existingField.wideLayoutIndexPath.length > 0)
|
|
{
|
|
const name = existingField.getQualifiedName();
|
|
if (namesWhereOneWideLayoutIndexHasBeenFound[name])
|
|
{
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// in this case, we're on like the 2nd or 3rd instance of, say, Line Item: SKU - so - just discard it. //
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
anyChanges = true;
|
|
}
|
|
else
|
|
{
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// else, this is the 1st instance of, say, Line Item: SKU - so mark that we've found it - and keep this field //
|
|
// (that is, put it in the new array), but with no index path //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
namesWhereOneWideLayoutIndexHasBeenFound[name] = true;
|
|
const newField = BulkLoadField.clone(existingField);
|
|
newField.wideLayoutIndexPath = [];
|
|
newAdditionalFields.push(newField);
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//////////////////////////////////////////////////////
|
|
// else, non-wide-path fields, just get added as-is //
|
|
//////////////////////////////////////////////////////
|
|
newAdditionalFields.push(existingField);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
// if going to WIDE layout, then any field from a child table needs a wide-layout-index-path //
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
for (let existingField of this.additionalFields)
|
|
{
|
|
if (existingField.tableStructure.isMain)
|
|
{
|
|
////////////////////////////////////////////
|
|
// fields from main table come over as-is //
|
|
////////////////////////////////////////////
|
|
newAdditionalFields.push(existingField);
|
|
}
|
|
else
|
|
{
|
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
// fields from child tables get a wideLayoutIndexPath (and we're assuming just 1 for each) //
|
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
const newField = BulkLoadField.clone(existingField);
|
|
newField.wideLayoutIndexPath = [0];
|
|
newAdditionalFields.push(newField);
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyChanges)
|
|
{
|
|
this.additionalFields = newAdditionalFields;
|
|
}
|
|
|
|
this.layout = newLayout;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getFieldsForColumnIndex(i: number): BulkLoadField[]
|
|
{
|
|
const rs: BulkLoadField[] = [];
|
|
|
|
for (let field of [...this.requiredFields, ...this.additionalFields])
|
|
{
|
|
if (field.valueType == "column" && field.columnIndex == i)
|
|
{
|
|
rs.push(field);
|
|
}
|
|
}
|
|
|
|
return (rs);
|
|
}
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public handleChangeToHasHeaderRow(newValue: any, fileDescription: FileDescription)
|
|
{
|
|
const newRequiredFields: BulkLoadField[] = [];
|
|
let anyChangesToRequiredFields = false;
|
|
|
|
const newAdditionalFields: BulkLoadField[] = [];
|
|
let anyChangesToAdditionalFields = false;
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// if we're switching to have header-rows enabled, then make sure that no columns w/ duplicated headers are selected //
|
|
// strategy to do this: build new lists of both required & additional fields - and track if we had to change any //
|
|
// column indexes (set to null) - add a warning to them, and only replace the arrays if there were changes. //
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
if (newValue)
|
|
{
|
|
for (let field of this.requiredFields)
|
|
{
|
|
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
|
{
|
|
const newField = BulkLoadField.clone(field);
|
|
newField.columnIndex = null;
|
|
newField.warning = "This field was assigned to a column with a duplicated header"
|
|
newRequiredFields.push(newField);
|
|
anyChangesToRequiredFields = true;
|
|
}
|
|
else
|
|
{
|
|
newRequiredFields.push(field);
|
|
}
|
|
}
|
|
|
|
for (let field of this.additionalFields)
|
|
{
|
|
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
|
{
|
|
const newField = BulkLoadField.clone(field);
|
|
newField.columnIndex = null;
|
|
newField.warning = "This field was assigned to a column with a duplicated header"
|
|
newAdditionalFields.push(newField);
|
|
anyChangesToAdditionalFields = true;
|
|
}
|
|
else
|
|
{
|
|
newAdditionalFields.push(field);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (anyChangesToRequiredFields)
|
|
{
|
|
this.requiredFields = newRequiredFields;
|
|
}
|
|
|
|
if (anyChangesToAdditionalFields)
|
|
{
|
|
this.additionalFields = newAdditionalFields;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** meta-data about the file that the user uploaded
|
|
***************************************************************************/
|
|
export class FileDescription
|
|
{
|
|
headerValues: string[];
|
|
headerLetters: string[];
|
|
bodyValuesPreview: string[][];
|
|
|
|
duplicateHeaderIndexes: boolean[];
|
|
|
|
// todo - just get this from the profile always - it's not part of the file per-se
|
|
hasHeaderRow: boolean = true;
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
constructor(headerValues: string[], headerLetters: string[], bodyValuesPreview: string[][])
|
|
{
|
|
this.headerValues = headerValues;
|
|
this.headerLetters = headerLetters;
|
|
this.bodyValuesPreview = bodyValuesPreview;
|
|
|
|
this.duplicateHeaderIndexes = [];
|
|
const usedLabels: { [label: string]: boolean } = {};
|
|
for (let i = 0; i < headerValues.length; i++)
|
|
{
|
|
const label = headerValues[i];
|
|
if (usedLabels[label])
|
|
{
|
|
this.duplicateHeaderIndexes[i] = true;
|
|
}
|
|
usedLabels[label] = true;
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public setHasHeaderRow(hasHeaderRow: boolean)
|
|
{
|
|
this.hasHeaderRow = hasHeaderRow;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getColumnNames(): string[]
|
|
{
|
|
if (this.hasHeaderRow)
|
|
{
|
|
return this.headerValues;
|
|
}
|
|
else
|
|
{
|
|
return this.headerLetters.map(l => `Column ${l}`);
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public getPreviewValues(columnIndex: number, fieldType?: QFieldType): string[]
|
|
{
|
|
if (columnIndex == undefined)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
function getTypedValue(value: any): string
|
|
{
|
|
if (value == null)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// this was useful at one point in time when we had an object coming back for xlsx files with many different data types //
|
|
// we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case //
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
if (value && value.string)
|
|
{
|
|
switch (fieldType)
|
|
{
|
|
case QFieldType.BOOLEAN:
|
|
{
|
|
return value.bool;
|
|
}
|
|
|
|
case QFieldType.STRING:
|
|
case QFieldType.TEXT:
|
|
case QFieldType.HTML:
|
|
case QFieldType.PASSWORD:
|
|
{
|
|
return value.string;
|
|
}
|
|
|
|
case QFieldType.INTEGER:
|
|
case QFieldType.LONG:
|
|
{
|
|
return value.integer;
|
|
}
|
|
case QFieldType.DECIMAL:
|
|
{
|
|
return value.decimal;
|
|
}
|
|
case QFieldType.DATE:
|
|
{
|
|
return value.date;
|
|
}
|
|
case QFieldType.TIME:
|
|
{
|
|
return value.time;
|
|
}
|
|
case QFieldType.DATE_TIME:
|
|
{
|
|
return value.dateTime;
|
|
}
|
|
case QFieldType.BLOB:
|
|
return ""; // !!
|
|
}
|
|
}
|
|
|
|
return (`${value}`);
|
|
}
|
|
|
|
const valueArray: string[] = [];
|
|
|
|
if (!this.hasHeaderRow)
|
|
{
|
|
const typedValue = getTypedValue(this.headerValues[columnIndex]);
|
|
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
|
}
|
|
|
|
for (let value of this.bodyValuesPreview[columnIndex])
|
|
{
|
|
const typedValue = getTypedValue(value);
|
|
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
|
}
|
|
|
|
return (valueArray);
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
** this (BulkLoadProfile & ...Field) is the model of what we save, and is
|
|
** also what we submit to the backend during the process.
|
|
***************************************************************************/
|
|
export class BulkLoadProfile
|
|
{
|
|
version: string;
|
|
fieldList: BulkLoadProfileField[] = [];
|
|
hasHeaderRow: boolean;
|
|
layout: string;
|
|
}
|
|
|
|
type BulkLoadProfileField =
|
|
{
|
|
fieldName: string,
|
|
columnIndex?: number,
|
|
headerName?: string,
|
|
defaultValue?: any,
|
|
doValueMapping?: boolean,
|
|
valueMappings?: { [fileValue: string]: any }
|
|
};
|
|
|
|
|
|
/***************************************************************************
|
|
** In the bulk load forms, we have some forward-ref callback functions, and
|
|
** they like to capture/retain a reference when those functions get defined,
|
|
** so we had some trouble updating objects in those functions.
|
|
**
|
|
** We "solved" this by creating instances of this class, which get captured,
|
|
** so then we can replace the wrapped object, and have a better time...
|
|
***************************************************************************/
|
|
export class Wrapper<T>
|
|
{
|
|
t: T;
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
constructor(t: T)
|
|
{
|
|
this.t = t;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public get(): T
|
|
{
|
|
return this.t;
|
|
}
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
public set(t: T)
|
|
{
|
|
this.t = t;
|
|
}
|
|
}
|
|
|