Существует несколько стандартных вариантов для загрузки CSV-файла и создания записей, но иногда, в зависимости от бизнес-требований, вам потребуется индивидуальное и интуитивно понятное решение для загрузки файла и запуска пользовательского процесса для создания/обновления записей. В этом посте рассказывается, как загрузить CSV, проанализировать содержимое файла и создать записи учетной записи, а в конце отобразить простой отзыв для пользователя. Это очень простой LWC для разбора CSV-файла и создания записей, и он может стать отправной точкой для более надежного решения.

Я видел несколько сообщений, объясняющих, как анализировать файлы CSV, и были разные решения: анализ CSV с использованием метода Apex, компонентов ауры, LWC, методов Javascript, выполняющих некоторые простые операции анализа. В нашем случае мы будем использовать существующую библиотеку для разбора файла и облегчения нашей жизни: Papa Parse. Эта библиотека имеет хорошие методы и параметры конфигурации для анализа CSV-файла. Вы можете выбрать, что будет символом-разделителем, есть ли у файла заголовок или нет, заключены ли столбцы в кавычки, какой символ и так далее.

Так как эта библиотека принимает на вход объект Файл, мы будем использовать Lightning-ввод с типом файл, чтобы получить файл от пользователя. После выбора файла синтаксический анализатор Papa проанализирует файл и вернет список объектов, каждый из которых представляет собой строку из файла. Для использования библиотеки необходимо загрузить файл .js как статический ресурс и импортировать его в LWC:

import {LightningElement, track} from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import PARSER from '@salesforce/resourceUrl/PapaParse';
export default class ImportAccounts extends LightningElement {
    parserInitialized = false;
renderedCallback() {
        if(!this.parserInitialized){
            loadScript(this, PARSER)
                .then(() => {
                    this.parserInitialized = true;
                })
                .catch(error => console.error(error));
        }
    }
}

Вот HTML-часть LWC:

<template>
    <lightning-card title="Import Accounts" icon-name="standard:account">
        <lightning-spinner if:true={loading}></lightning-spinner>
        <div class="slds-p-around_medium">
            <lightning-input type="file"
                             label="CSV file"
                             multiple="false"
                             accept=".csv"
                             onchange={handleInputChange}></lightning-input>
        </div>
        <template if:true={rows.length}>
            <div class="slds-p-around_medium">
                <lightning-datatable key-field="key"
                                     hide-checkbox-column
                                     data={rows}
                                     columns={columns}></lightning-datatable>
                <div class="slds-p-around_small slds-align_absolute-center">
                    <lightning-button variant="neutral"
                                      label="Cancel"
                                      title="Cancel" onclick={cancel}
                                      class="slds-m-left_x-small"></lightning-button>
                    <lightning-button variant="brand"
                                      label="Create"
                                      title="Create" onclick={createAccounts}
                                      class="slds-m-left_x-small"></lightning-button>
                </div>
            </div>
        </template>
    </lightning-card>
</template>

При выборе файла будет выполняться следующая функция для разбора CSV-файла:

handleInputChange(event){
        if(event.target.files.length > 0){
            const file = event.target.files[0];
            this.loading = true;
            Papa.parse(file, {
                quoteChar: '"',
                header: 'true',
                complete: (results) => {
                    this._rows = results.data;
                    this.loading = false;
                },
                error: (error) => {
                    console.error(error);
                    this.loading = false;
                }
            })
        }
    }

Атрибут complete – это функция обратного вызова, которая получает массив объектов со строками из CSV-файла. При этом у нас есть геттер для отображения строк в таблице данных молнии, чтобы пользователь мог предварительно просмотреть строки. Мы не ограничиваем количество отображаемых записей, но рассмотрите возможность сделать это, если вы собираетесь иметь дело с большим количеством записей. Добытчик:

get rows(){
    if(this._rows){
        return this._rows.map((row, index) => {
            row.key = index;
            if(this.results[index]){
                row.result = this.results[index].id || this.results[index].error;
            }
            return row;
        })
    }
    return [];
}

Не обращайте внимания на if(this.results[index]) на данный момент, его объяснение будет позже. Существует кнопка для создания записей учетной записи, загружаемых из CSV и отображаемых в таблице данных. Он вызовет следующую функцию:

createAccounts(){
    const accountsToCreate = this.rows.map(row => {
        const fields = {};
        fields[NAME_FIELD.fieldApiName] = row.AccountName;
        fields[DESCRIPTION_FIELD.fieldApiName] = row.Description;
        const recordInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
        return createRecord(recordInput);
    });
    if(accountsToCreate.length){
        this.loading = true;
        Promise.allSettled(accountsToCreate)
            .then(results => this._results = results)
            .catch(error => console.error(error))
            .finally(() => this.loading = false);
    }
}

Здесь мы используем createRecordиз Lightning/uiRecordApiдля создания записей учетной записи и Promise.allSettledдля создания всех записей и получения результатов, которые будут отображаться в таблице данных вместе с записями. Вы должны были заметить, что мы используем (и определяем) только два столбца для создания записей (поля «Имя» и «Описание»). Вы можете изменить эту часть, чтобы принять больше столбцов, если это необходимо. В будущем я улучшу этот компонент, чтобы пользователь мог настраивать столбцы.

Мы определили геттер для преобразования результатов нужным нам образом, а затем использовали эту информацию вместе со строками, чтобы данные отображались пользователю. Для успешно созданных записей мы отображаем идентификаторы записей. При возникновении ошибки мы показываем сообщение об ошибке.

get results(){
    if(this._results){
        return this._results.map(r => {
            const result = {};
            result.success = r.status === 'fulfilled';
            result.id = result.success ? r.value.id : undefined;
            result.error = !result.success ? r.reason.body.message : undefined;
            return result;
        });
    }
    return [];
}

Вот весь контроллер JS:

import {LightningElement, track} from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import { createRecord } from 'lightning/uiRecordApi';
import PARSER from '@salesforce/resourceUrl/PapaParse';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import DESCRIPTION_FIELD from '@salesforce/schema/Account.Description';
export default class ImportAccounts extends LightningElement {
    parserInitialized = false;
    loading = false;
    @track _results;
    @track _rows;
    get columns(){
        const columns = [
            { label: 'Account Name', fieldName: 'AccountName' },
            { label: 'Description', fieldName: 'Description' }
        ];
        if(this.results.length){
            columns.push({ label: 'Result',fieldName: 'result' });
        }
        return columns;
    }
    get rows(){
        if(this._rows){
            return this._rows.map((row, index) => {
                row.key = index;
                if(this.results[index]){
                    row.result = this.results[index].id || this.results[index].error;
                }
                return row;
            })
        }
        return [];
    }
    get results(){
        if(this._results){
            return this._results.map(r => {
                const result = {};
                result.success = r.status === 'fulfilled';
                result.id = result.success ? r.value.id : undefined;
                result.error = !result.success ? r.reason.body.message : undefined;
                return result;
            });
        }
        return [];
    }
    renderedCallback() {
        if(!this.parserInitialized){
            loadScript(this, PARSER)
                .then(() => {
                    this.parserInitialized = true;
                })
                .catch(error => console.error(error));
        }
    }
    handleInputChange(event){
        if(event.target.files.length > 0){
            const file = event.target.files[0];
            this.loading = true;
            Papa.parse(file, {
                quoteChar: '"',
                header: 'true',
                complete: (results) => {
                    this._rows = results.data;
                    this.loading = false;
                },
                error: (error) => {
                    console.error(error);
                    this.loading = false;
                }
            })
        }
    }
    createAccounts(){
        const accountsToCreate = this.rows.map(row => {
            const fields = {};
            fields[NAME_FIELD.fieldApiName] = row.AccountName;
            fields[DESCRIPTION_FIELD.fieldApiName] = row.Description;
            const recordInput = { apiName: ACCOUNT_OBJECT.objectApiName, fields };
            return createRecord(recordInput);
        });
        if(accountsToCreate.length){
            this.loading = true;
            Promise.allSettled(accountsToCreate)
                .then(results => this._results = results)
                .catch(error => console.error(error))
                .finally(() => this.loading = false);
        }
    }
    cancel(){
        this._rows = undefined;
        this._results = undefined;
    }
}

В своем следующем сообщении в блоге я улучшу этот компонент, чтобы динамически получать поля из объекта Account и позволять пользователю сопоставлять столбцы из CSV-файла с полями. Быть в курсе!