@websolutespa/payload-plugin-bowl
Version:
Bowl PayloadCms plugin of the BOM Repository
463 lines (462 loc) • 19.9 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { MinimalTemplate } from '@payloadcms/next/templates';
import { Button, CheckboxInput, SelectInput, XIcon, toast, useAuth, useConfig, useLocale, useModal, useTranslation } from '@payloadcms/ui';
import { getRouteResolver } from '@websolutespa/payload-utils';
import * as Papa from 'papaparse';
import React, { useCallback, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { ImportLogInvalidTypes, ImportLogType, ImportMode } from '../../core/api/types';
import './ImportModal.scss';
const INITIAL_DATA = {
importMode: ImportMode.Append,
dataParsing: false
};
const baseClass = 'import-modal';
export const ImportModal = ({ parser, slug })=>{
const { closeAllModals } = useModal();
const { config, getEntityConfig } = useConfig();
const collectionConfig = getEntityConfig({
collectionSlug: slug
});
const routeResolver = getRouteResolver(config);
const collectionUrl = routeResolver.api(`/${collectionConfig.slug}?pagination=false&depth=0`);
// !!! check translations
const { t } = useTranslation();
const format = (text, ...rest)=>{
return text.replace(/\{(\d)\}/g, (m, g)=>{
const i = parseInt(g);
return i >= 0 && i < rest.length ? String(rest[i]) : '';
});
};
const { code: locale } = useLocale();
const user = useAuth();
const [dirty, setDirty] = useState(false);
const [valid, setValid] = useState(false);
const [error, setError] = useState();
const [name, setName] = useState(null);
const [csv, setCSV] = useState(null);
const [keys, setKeys] = useState(null);
const [data, setData] = useState(null);
const [items, setItems] = useState([]);
const [logs, setLogs] = useState([]);
const [importMode, setImportMode] = useState(INITIAL_DATA.importMode);
const [dataParsing, setDataParsing] = useState(INITIAL_DATA.dataParsing);
const importModes = Object.entries(ImportMode).map(([k, v])=>({
value: v,
label: t(`importExport:mode${k}`)
}));
const onSelectDidChange = (option)=>{
// console.log('onSelectDidChange', option);
if (Array.isArray(option)) {
return;
}
if (importMode !== option.value) {
setValid(false);
setImportMode(option.value);
}
};
const onCheckboxkDidChange = (event)=>{
// console.log('onCheckboxkDidChange', value);
setValid(false);
setDataParsing(!dataParsing);
};
const validate = useCallback(async (keys, data)=>{
try {
const messageByType = (key, type)=>{
let message;
switch(type){
case ImportLogType.Required:
message = format(t('importExport:errorRequired'), key);
break;
case ImportLogType.Optional:
message = format(t('importExport:errorOptional'), key);
break;
case ImportLogType.Invalid:
message = format(t('importExport:errorInvalid'), key);
break;
case ImportLogType.Duplicate:
message = format(t('importExport:errorDuplicate'), key);
break;
case ImportLogType.Unexpected:
message = format(t('importExport:errorUnexpected'), key);
break;
}
return message;
};
const addLog = (logs, key, type, invalid)=>{
const message = messageByType(key, type);
const id = invalid ? `${key}-${type}-${invalid}` : `${key}-${type}`;
const log = once(logs, {
id,
key,
type,
message,
invalid,
count: 0
});
log.count++;
return log;
};
const itemOptionalKeys = [];
const itemKeys = keys;
// collecting model fields & keys
const fields = collectionConfig.fields.filter((x)=>'name' in x && x['name'] !== undefined && x.type !== 'ui');
const fieldKeys = fields.map((x)=>x['name']);
const requiredFields = fields.filter((x)=>{
if (x['name'] === 'id' && importMode === ImportMode.Update) {
return true;
} else {
return 'required' in x && x['required'] === true;
}
});
const uniqueFields = fields.filter((x)=>x['unique'] === true);
const requiredFieldKeys = requiredFields.map((x)=>x['name']);
const optionalFieldKeys = fieldKeys.filter((x)=>!requiredFieldKeys.includes(x));
const uniqueFieldKeys = uniqueFields.map((x)=>x['name']);
let items = [];
// parse values
const valueParsers = itemKeys.map((key)=>{
let parseValue = (item)=>item[key];
if (fieldKeys.includes(key)) {
const field = collectionConfig.fields.find((x)=>'name' in x && x['name'] === key);
if (field) {
switch(field.type){
case 'checkbox':
parseValue = (item)=>item[key] !== undefined ? Boolean(item[key]) : item[key];
break;
case 'date':
parseValue = (item)=>item[key] !== undefined ? new Date(item[key]) : item[key];
break;
case 'number':
parseValue = (item)=>item[key] !== undefined ? Number(item[key]) : item[key];
break;
}
}
}
return parseValue;
});
for (const row of data){
const item = {};
itemKeys.forEach((key, i)=>{
const parser = valueParsers[i];
if (parser) {
item[key] = parser(row);
}
});
items.push(item);
}
if (dataParsing && typeof parser === 'function') {
items = parser(items);
}
// console.log('data', data);
// console.log('items', items);
// collecting stored values
const httpResponse = await fetch(collectionUrl, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!httpResponse.ok) {
throw httpResponse;
}
const storedItems = await httpResponse.json();
// console.log('rows', rows, 'data', result.data);
// collecting logs
const logs = [];
// required fields
requiredFieldKeys.forEach((key)=>{
if (!itemKeys.includes(key) || itemOptionalKeys.includes(key)) {
addLog(logs, key, ImportLogType.Required);
}
});
// optional fields
optionalFieldKeys.forEach((key)=>{
if (!itemKeys.includes(key)) {
addLog(logs, key, ImportLogType.Optional);
}
});
// deep check values
for (const key of itemKeys){
if (fieldKeys.includes(key)) {
const field = collectionConfig.fields.find((x)=>'name' in x && x['name'] === key);
if (field) {
const validate = 'validate' in field ? field['validate'] : undefined;
const values = [];
const storedValues = uniqueFieldKeys.includes(key) ? storedItems.map((x)=>x[key]) : [];
for (const item of items){
const value = item[key];
// validate field
if (typeof validate === 'function') {
const validation = await validate(value, {
data: item,
siblingData: item,
operation: 'create',
user,
t
});
if (validation !== true) {
// console.log(key, value, validation, item);
if (logs.find((x)=>x.key === 'key' && x.type === ImportLogType.Invalid) === undefined) {
addLog(logs, key, ImportLogType.Invalid, validation);
}
}
}
// unique field
if (uniqueFieldKeys.includes(key) && (values.includes(value) || (importMode === ImportMode.Append ? storedValues.includes(value) : false))) {
addLog(logs, key, ImportLogType.Duplicate, String(value));
}
values.push(value);
}
}
} else {
addLog(logs, key, ImportLogType.Unexpected);
}
}
const sortOrder = Object.values(ImportLogType);
logs.sort((a, b)=>{
return sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type);
});
setItems(items);
setLogs(logs);
const errors = logs.filter((x)=>ImportLogInvalidTypes.includes(x.type));
if (errors.length === 0) {
setValid(true);
}
// console.log(logs, items, itemKeys, itemOptionalKeys, fieldKeys, requiredFieldKeys, optionalFieldKeys);
} catch (error) {
console.log('parse error', error);
setError(error);
}
}, [
collectionUrl,
importMode,
dataParsing,
parser,
collectionConfig.fields,
t,
user
]);
useEffect(()=>{
if (keys && data) {
validate(keys, data);
}
}, [
keys,
data,
importMode,
validate
]);
const onDrop = useCallback((acceptedFiles)=>{
const acceptedFile = acceptedFiles.find(()=>true);
if (acceptedFile) {
const reader = new FileReader();
reader.onabort = ()=>console.log('file reading was aborted');
reader.onerror = ()=>console.log('file reading has failed');
reader.onload = ()=>{
// Do whatever you want with the file contents
const csv = reader.result;
// console.log('csv', csv);
// parsing csv to { data:[][] }
const result = Papa.parse(csv);
const resultData = result.data;
// collecting imported keys & data
let keys = [];
const data = resultData.reduce((p, c, i)=>{
if (i === 0) {
keys = c;
} else {
const item = {};
keys.forEach((key, k)=>{
if (c[k] !== undefined) {
item[key] = c[k];
}
});
p.push(item);
}
return p;
}, []);
setName(acceptedFile.name);
setCSV(csv);
setKeys(keys);
setData(data);
setDirty(true);
};
// reader.readAsArrayBuffer(file);
reader.readAsText(acceptedFile);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'text/csv': []
},
maxSize: 1048576 * 20,
maxFiles: 1,
onDrop
});
const onSubmit = async ()=>{
// console.log('ImportModal.onSubmit');
try {
const url = routeResolver.api(`/${collectionConfig.slug}/import?locale=${locale}&mode=${importMode}`);
const httpResponse = await fetch(url, {
method: 'POST',
body: JSON.stringify({
items
}),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!httpResponse.ok) {
throw httpResponse;
}
const response = await httpResponse.json();
// console.log('ImportModal.onSubmit', response);
toast.success(t('importExport:success'));
closeAllModals();
} catch (error) {
console.log('ImportModal.onSubmit.error', error);
toast.error(format(t('importExport:failure'), collectionConfig.slug));
}
};
const onClose = ()=>{
// console.log('onClose');
closeAllModals();
};
const onClear = ()=>{
// console.log('onClear');
setValid(false);
setDirty(false);
setName(null);
setCSV(null);
setKeys(null);
setData(null);
setItems([]);
setLogs([]);
};
return /*#__PURE__*/ _jsxs(MinimalTemplate, {
children: [
/*#__PURE__*/ _jsxs("header", {
className: `${baseClass}__header`,
children: [
/*#__PURE__*/ _jsx("h3", {
children: t('importExport:modalTitle')
}),
/*#__PURE__*/ _jsx(Button, {
buttonStyle: "none",
onClick: onClose,
children: /*#__PURE__*/ _jsx(XIcon, {})
})
]
}),
/*#__PURE__*/ _jsx("main", {
className: `${baseClass}__main`,
children: /*#__PURE__*/ _jsxs("form", {
onSubmit: (e)=>{
e.preventDefault();
onSubmit();
},
children: [
/*#__PURE__*/ _jsxs("div", {
className: "render-fields",
children: [
/*#__PURE__*/ _jsx(SelectInput, {
// isClearable={viewType === 'list'}
label: t('importExport:importMode'),
name: "importMode",
path: 'importMode',
options: importModes,
onChange: onSelectDidChange,
value: importMode
}),
typeof parser === 'function' && /*#__PURE__*/ _jsx(CheckboxInput, {
label: t('importExport:enableDataParsing'),
id: "dataParsing",
name: "dataParsing",
checked: dataParsing,
onToggle: onCheckboxkDidChange
})
]
}),
data ? /*#__PURE__*/ _jsxs(_Fragment, {
children: [
/*#__PURE__*/ _jsx("div", {
dangerouslySetInnerHTML: {
__html: format(t('importExport:records'), items.length, name)
}
}),
logs.length > 0 && /*#__PURE__*/ _jsx("ul", {
className: `${baseClass}__logs`,
children: logs.map((log, i)=>/*#__PURE__*/ _jsxs("li", {
className: `${baseClass}__log ${baseClass}__log--${log.type}`,
children: [
log.count,
" ",
log.message,
" ",
/*#__PURE__*/ _jsx("i", {
children: log.invalid
})
]
}, `log-${i}`))
})
]
}) : /*#__PURE__*/ _jsxs("div", {
className: `${baseClass}__dropzone`,
...getRootProps(),
children: [
/*#__PURE__*/ _jsx("input", {
className: `${baseClass}__input`,
...getInputProps()
}),
isDragActive ? /*#__PURE__*/ _jsx("div", {
children: t('importExport:dropTitle')
}) : /*#__PURE__*/ _jsx("div", {
children: t('importExport:dropAbstract')
})
]
}),
error && /*#__PURE__*/ _jsxs(_Fragment, {
children: [
/*#__PURE__*/ _jsx("div", {
children: t('importExport:parseError')
}),
error.message && /*#__PURE__*/ _jsx("div", {
children: error.message
})
]
}),
/*#__PURE__*/ _jsxs("div", {
className: `${baseClass}__foot`,
children: [
dirty && /*#__PURE__*/ _jsx(Button, {
type: "button",
buttonStyle: "secondary",
onClick: onClear,
children: t('importExport:clear')
}),
/*#__PURE__*/ _jsx(Button, {
type: "submit",
disabled: !valid,
children: t('importExport:submit')
})
]
})
]
})
})
]
});
};
function once(items, item) {
const exhistingItem = items.find((x)=>x.id === item.id);
if (exhistingItem) {
return exhistingItem;
}
items.push(item);
return item;
}
//# sourceMappingURL=ImportModal.js.map