UNPKG

@websolutespa/payload-plugin-bowl

Version:

Bowl PayloadCms plugin of the BOM Repository

463 lines (462 loc) 19.9 kB
'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