react-csv-importer
Advanced tools
Comparing version 0.3.0 to 0.4.0
// Generated by dts-bundle-generator v5.4.0 | ||
/// <reference types="mocha" /> | ||
/// <reference types="papaparse" /> | ||
@@ -4,0 +5,0 @@ /// <reference types="prop-types" /> |
@@ -220,2 +220,5 @@ module.exports = | ||
// CONCATENATED MODULE: ./src/components/ImporterProps.ts | ||
// EXTERNAL MODULE: external "react" | ||
@@ -573,2 +576,46 @@ var external_react_ = __webpack_require__(0); | ||
// CONCATENATED MODULE: ./src/components/ColumnPreview.tsx | ||
// spreadsheet-style column code computation (A, B, ..., Z, AA, AB, ..., etc) | ||
function generateColumnCode(value) { | ||
// ignore dummy index | ||
if (value < 0) { | ||
return ''; | ||
} | ||
// first, determine how many base-26 letters there should be | ||
// (because the notation is not purely positional) | ||
let digitCount = 1; | ||
let base = 0; | ||
let next = 26; | ||
while (next <= value) { | ||
digitCount += 1; | ||
base = next; | ||
next = next * 26 + 26; | ||
} | ||
// then, apply normal positional digit computation on remainder above base | ||
let remainder = value - base; | ||
const digits = []; | ||
while (digits.length < digitCount) { | ||
const lastDigit = remainder % 26; | ||
remainder = Math.floor((remainder - lastDigit) / 26); // applying floor just in case | ||
// store ASCII code, with A as 0 | ||
digits.unshift(65 + lastDigit); | ||
} | ||
return String.fromCharCode.apply(null, digits); | ||
} | ||
// prepare spreadsheet-like column display information for given raw data preview | ||
function generatePreviewColumns(preview) { | ||
const columnStubs = [...new Array(preview.firstRows[0].length)]; | ||
return columnStubs.map((empty, index) => { | ||
const values = preview.firstRows.map((row) => row[index] || ''); | ||
const dataValues = [...values]; | ||
const headerValue = preview.hasHeaders ? values.shift() : undefined; | ||
return { | ||
index, | ||
code: generateColumnCode(index), | ||
header: headerValue, | ||
values: dataValues | ||
}; | ||
}); | ||
} | ||
// EXTERNAL MODULE: external "react-use-gesture" | ||
@@ -580,3 +627,3 @@ var external_react_use_gesture_ = __webpack_require__(4); | ||
function useColumnDragState(fields, onTouched) { | ||
function useColumnDragState(fields, initialAssignments, onTouched) { | ||
// wrap in ref to avoid re-triggering | ||
@@ -586,3 +633,3 @@ const onTouchedRef = Object(external_react_["useRef"])(onTouched); | ||
const [dragState, setDragState] = Object(external_react_["useState"])(null); | ||
const [fieldAssignments, setFieldAssignments] = Object(external_react_["useState"])({}); | ||
const [fieldAssignments, setFieldAssignments] = Object(external_react_["useState"])(initialAssignments); | ||
// make sure there are no extra fields | ||
@@ -720,3 +767,3 @@ Object(external_react_["useEffect"])(() => { | ||
// @todo sort out "grabbing" cursor state (does not work with pointer-events:none) | ||
const ColumnDragCard_ColumnDragCard = ({ hasHeaders, column: optionalColumn, rowCount = PREVIEW_ROW_COUNT, hasError, isShadow, isDraggable, isDragged, isDropIndicator }) => { | ||
const ColumnDragCard_ColumnDragCard = ({ column: optionalColumn, rowCount = PREVIEW_ROW_COUNT, hasError, isAssigned, isShadow, isDraggable, isDragged, isDropIndicator }) => { | ||
const isDummy = !optionalColumn; | ||
@@ -728,4 +775,4 @@ const column = Object(external_react_["useMemo"])(() => optionalColumn || { | ||
}, [optionalColumn]); | ||
const headerValue = hasHeaders ? column.values[0] : undefined; | ||
const dataValues = column.values.slice(hasHeaders ? 1 : 0, rowCount); | ||
const headerValue = column.header; | ||
const dataValues = column.values.slice(headerValue === undefined ? 0 : 1, rowCount); | ||
return ( | ||
@@ -738,3 +785,3 @@ // not changing variant dynamically because it causes a height jump | ||
column.code)), | ||
isDummy ? '\u00a0' : external_react_default.a.createElement("b", { "aria-hidden": true }, column.code)), | ||
isDummy || isAssigned ? '\u00a0' : external_react_default.a.createElement("b", { "aria-hidden": true }, column.code)), | ||
headerValue !== undefined ? (external_react_default.a.createElement("div", { className: "CSVImporter_ColumnDragCard__cardValue", "data-header": true }, headerValue || '\u00a0')) : null, | ||
@@ -752,3 +799,3 @@ external_react_default.a.createElement("div", { role: "text" }, dataValues.map((value, valueIndex) => (external_react_default.a.createElement("div", { key: valueIndex, className: "CSVImporter_ColumnDragCard__cardValue" }, value || '\u00a0')))))); | ||
const ColumnDragObject_ColumnDragObject = ({ hasHeaders, dragState }) => { | ||
const ColumnDragObject_ColumnDragObject = ({ dragState }) => { | ||
const referenceBoxRef = Object(external_react_["useRef"])(null); | ||
@@ -760,3 +807,3 @@ // @todo wrap in a no-events overlay to clip against screen edges | ||
external_react_default.a.createElement("div", { className: "CSVImporter_ColumnDragObject__holder" }, | ||
external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { hasHeaders: hasHeaders, column: dragState.column, isDragged: true }))), document.body) | ||
external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { column: dragState.column, isDragged: true }))), document.body) | ||
: null; | ||
@@ -809,3 +856,3 @@ // set up initial position | ||
// @todo readable status text if not mouse-drag | ||
const SourceBox = ({ hasHeaders, column, fieldAssignments, dragState, eventBinder, onSelect, onUnassign }) => { | ||
const SourceBox = ({ column, fieldAssignments, dragState, eventBinder, onSelect, onUnassign }) => { | ||
const isDragged = dragState ? column === dragState.column : false; | ||
@@ -819,3 +866,3 @@ const isAssigned = Object(external_react_["useMemo"])(() => Object.keys(fieldAssignments).some((fieldName) => fieldAssignments[fieldName] === column.index), [fieldAssignments, column]); | ||
external_react_default.a.createElement("div", Object.assign({}, (isAssigned ? {} : eventHandlers)), | ||
external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { hasHeaders: hasHeaders, column: column, isShadow: isDragged || isAssigned, isDraggable: !dragState && !isDragged && !isAssigned })), | ||
external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { column: column, isAssigned: isAssigned, isShadow: isDragged || isAssigned, isDraggable: !dragState && !isDragged && !isAssigned })), | ||
external_react_default.a.createElement("div", { className: "CSVImporter_ColumnDragSourceArea__boxAction" }, isAssigned ? (external_react_default.a.createElement(IconButton_IconButton, { key: "clear" // key-prop helps clear focus on click | ||
@@ -832,3 +879,3 @@ , label: "Clear column assignment", small: true, type: "replay", onClick: () => { | ||
// @todo current page indicator (dots) | ||
const ColumnDragSourceArea_ColumnDragSourceArea = ({ hasHeaders, columns, fieldAssignments, dragState, eventBinder, onSelect, onUnassign }) => { | ||
const ColumnDragSourceArea_ColumnDragSourceArea = ({ columns, fieldAssignments, dragState, eventBinder, onSelect, onUnassign }) => { | ||
const [page, setPage] = Object(external_react_["useState"])(0); | ||
@@ -840,3 +887,3 @@ const [pageChanged, setPageChanged] = Object(external_react_["useState"])(false); | ||
.slice(start, start + SOURCES_PAGE_SIZE) | ||
.map((column, columnIndex) => (external_react_default.a.createElement(SourceBox, { key: columnIndex, hasHeaders: hasHeaders, column: column, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: eventBinder, onSelect: onSelect, onUnassign: onUnassign }))); | ||
.map((column, columnIndex) => (external_react_default.a.createElement(SourceBox, { key: columnIndex, column: column, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: eventBinder, onSelect: onSelect, onUnassign: onUnassign }))); | ||
while (pageContents.length < SOURCES_PAGE_SIZE) { | ||
@@ -877,3 +924,3 @@ pageContents.push(external_react_default.a.createElement("div", { key: pageContents.length, className: "CSVImporter_ColumnDragSourceArea__pageFiller" })); | ||
const TargetBox = ({ hasHeaders, field, touched, assignedColumn, dragState, eventBinder, onHover, onAssign, onUnassign }) => { | ||
const TargetBox = ({ field, touched, assignedColumn, dragState, eventBinder, onHover, onAssign, onUnassign }) => { | ||
const mouseHoverHandlers = dragState && dragState.pointerStartInfo | ||
@@ -895,10 +942,10 @@ ? { | ||
if (sourceColumn) { | ||
return (external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { hasHeaders: hasHeaders, rowCount: 3, column: sourceColumn, isDropIndicator: true })); | ||
return (external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { rowCount: 3, column: sourceColumn, isDropIndicator: true })); | ||
} | ||
if (assignedColumn) { | ||
return (external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { hasHeaders: hasHeaders, rowCount: 3, column: assignedColumn, isShadow: isReDragged, isDraggable: !isReDragged })); | ||
return (external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { rowCount: 3, column: assignedColumn, isShadow: isReDragged, isDraggable: !isReDragged })); | ||
} | ||
const hasError = touched && !field.isOptional; | ||
return (external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { hasHeaders: hasHeaders, rowCount: 3, hasError: hasError })); | ||
}, [field, hasHeaders, touched, assignedColumn, sourceColumn, isReDragged]); | ||
return external_react_default.a.createElement(ColumnDragCard_ColumnDragCard, { rowCount: 3, hasError: hasError }); | ||
}, [field, touched, assignedColumn, sourceColumn, isReDragged]); | ||
// @todo mouse cursor changes to reflect draggable state | ||
@@ -917,6 +964,6 @@ return (external_react_default.a.createElement("section", Object.assign({ className: "CSVImporter_ColumnDragTargetArea__box", "aria-label": `${field.label} (${field.isOptional ? 'optional' : 'required'})` }, mouseHoverHandlers), | ||
}; | ||
const ColumnDragTargetArea_ColumnDragTargetArea = ({ fields, columns, hasHeaders, fieldTouched, fieldAssignments, dragState, eventBinder, onHover, onAssign, onUnassign }) => { | ||
const ColumnDragTargetArea_ColumnDragTargetArea = ({ fields, columns, fieldTouched, fieldAssignments, dragState, eventBinder, onHover, onAssign, onUnassign }) => { | ||
return (external_react_default.a.createElement("section", { className: "CSVImporter_ColumnDragTargetArea", "aria-label": "Target fields" }, fields.map((field) => { | ||
const assignedColumnIndex = fieldAssignments[field.name]; | ||
return (external_react_default.a.createElement(TargetBox, { key: field.name, hasHeaders: hasHeaders, field: field, touched: fieldTouched[field.name], assignedColumn: assignedColumnIndex !== undefined | ||
return (external_react_default.a.createElement(TargetBox, { key: field.name, field: field, touched: fieldTouched[field.name], assignedColumn: assignedColumnIndex !== undefined | ||
? columns[assignedColumnIndex] | ||
@@ -934,43 +981,45 @@ : null, dragState: dragState, eventBinder: eventBinder, onHover: onHover, onAssign: onAssign, onUnassign: onUnassign })); | ||
// spreadsheet-style column code computation (A, B, ..., Z, AA, AB, ..., etc) | ||
function generateColumnCode(value) { | ||
// ignore dummy index | ||
if (value < 0) { | ||
return ''; | ||
} | ||
// first, determine how many base-26 letters there should be | ||
// (because the notation is not purely positional) | ||
let digitCount = 1; | ||
let base = 0; | ||
let next = 26; | ||
while (next <= value) { | ||
digitCount += 1; | ||
base = next; | ||
next = next * 26 + 26; | ||
} | ||
// then, apply normal positional digit computation on remainder above base | ||
let remainder = value - base; | ||
const digits = []; | ||
while (digits.length < digitCount) { | ||
const lastDigit = remainder % 26; | ||
remainder = Math.floor((remainder - lastDigit) / 26); // applying floor just in case | ||
// store ASCII code, with A as 0 | ||
digits.unshift(65 + lastDigit); | ||
} | ||
return String.fromCharCode.apply(null, digits); | ||
} | ||
const ColumnPicker = ({ fields, preview, onAccept, onCancel }) => { | ||
const columns = Object(external_react_["useMemo"])(() => { | ||
return [...new Array(preview.firstRows[0].length)].map((empty, index) => { | ||
return { | ||
index, | ||
code: generateColumnCode(index), | ||
values: preview.firstRows.map((row) => row[index] || '') | ||
}; | ||
const columns = Object(external_react_["useMemo"])(() => generatePreviewColumns(preview), [ | ||
preview | ||
]); | ||
const initialAssignments = Object(external_react_["useMemo"])(() => { | ||
// prep insensitive/fuzzy match stems for known columns | ||
const columnStems = columns.map((column) => { | ||
const trimmed = column.header && column.header.trim(); | ||
if (!trimmed) { | ||
return undefined; | ||
} | ||
return trimmed.toLowerCase(); | ||
}); | ||
}, [preview]); | ||
// pre-assign corresponding fields | ||
const result = {}; | ||
const assignedColumnIndexes = []; | ||
fields.forEach((field) => { | ||
// find by field stem | ||
const fieldLabelStem = field.label.trim().toLowerCase(); // @todo consider normalizing other whitespace/non-letters | ||
const matchingColumnIndex = columnStems.findIndex((columnStem, columnIndex) => { | ||
// no headers or no meaningful stem value | ||
if (columnStem === undefined) { | ||
return false; | ||
} | ||
// always check against assigning twice | ||
if (assignedColumnIndexes[columnIndex]) { | ||
return false; | ||
} | ||
return columnStem === fieldLabelStem; | ||
}); | ||
// assign if found | ||
if (matchingColumnIndex !== -1) { | ||
assignedColumnIndexes[matchingColumnIndex] = true; | ||
result[field.name] = matchingColumnIndex; | ||
} | ||
}); | ||
return result; | ||
}, [fields, columns]); | ||
// track which fields need to show validation warning | ||
const [fieldTouched, setFieldTouched] = Object(external_react_["useState"])({}); | ||
const [validationError, setValidationError] = Object(external_react_["useState"])(null); | ||
const { fieldAssignments, dragState, dragEventBinder, dragHoverHandler, columnSelectHandler, assignHandler, unassignHandler } = useColumnDragState(fields, (fieldName) => { | ||
const { fieldAssignments, dragState, dragEventBinder, dragHoverHandler, columnSelectHandler, assignHandler, unassignHandler } = useColumnDragState(fields, initialAssignments, (fieldName) => { | ||
setFieldTouched((prev) => { | ||
@@ -1001,5 +1050,5 @@ if (prev[fieldName]) { | ||
} }, | ||
external_react_default.a.createElement(ColumnDragSourceArea_ColumnDragSourceArea, { hasHeaders: preview.hasHeaders, columns: columns, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: dragEventBinder, onSelect: columnSelectHandler, onUnassign: unassignHandler }), | ||
external_react_default.a.createElement(ColumnDragTargetArea_ColumnDragTargetArea, { fields: fields, columns: columns, hasHeaders: preview.hasHeaders, fieldTouched: fieldTouched, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: dragEventBinder, onHover: dragHoverHandler, onAssign: assignHandler, onUnassign: unassignHandler }), | ||
external_react_default.a.createElement(ColumnDragObject_ColumnDragObject, { hasHeaders: preview.hasHeaders, dragState: dragState }))); | ||
external_react_default.a.createElement(ColumnDragSourceArea_ColumnDragSourceArea, { columns: columns, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: dragEventBinder, onSelect: columnSelectHandler, onUnassign: unassignHandler }), | ||
external_react_default.a.createElement(ColumnDragTargetArea_ColumnDragTargetArea, { fields: fields, columns: columns, fieldTouched: fieldTouched, fieldAssignments: fieldAssignments, dragState: dragState, eventBinder: dragEventBinder, onHover: dragHoverHandler, onAssign: assignHandler, onUnassign: unassignHandler }), | ||
external_react_default.a.createElement(ColumnDragObject_ColumnDragObject, { dragState: dragState }))); | ||
}; | ||
@@ -1229,4 +1278,5 @@ | ||
/***/ }) | ||
/******/ ]); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "react-csv-importer", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "React CSV import widget with user-customizable mapping", | ||
@@ -30,2 +30,3 @@ "keywords": [ | ||
"stylelint-fix": "stylelint \"src/**/*.scss\" --fix", | ||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha --require ts-node/register --timeout 10000 test/**/*.test.ts", | ||
"storybook": "start-storybook -p 6006", | ||
@@ -39,3 +40,4 @@ "build-storybook": "build-storybook", | ||
"src/**/*.{ts,tsx}": "eslint --max-warnings=0", | ||
"src/**/*.scss": "stylelint" | ||
"src/**/*.scss": "stylelint", | ||
"test/**/*.{js,ts}": "eslint --max-warnings=0" | ||
}, | ||
@@ -49,9 +51,16 @@ "devDependencies": { | ||
"@storybook/react": "^6.0.21", | ||
"@types/chai": "^4.2.14", | ||
"@types/mocha": "^8.2.0", | ||
"@types/papaparse": "^5.2.2", | ||
"@types/react": "^16.9.49", | ||
"@types/react-dom": "^16.9.8", | ||
"@types/selenium-webdriver": "^4.0.11", | ||
"@types/webpack-dev-server": "^3.11.1", | ||
"@typescript-eslint/eslint-plugin": "^4.1.0", | ||
"@typescript-eslint/parser": "^4.1.0", | ||
"babel-loader": "^8.1.0", | ||
"chai": "^4.2.0", | ||
"chromedriver": "^87.0.7", | ||
"clean-webpack-plugin": "^3.0.0", | ||
"cross-env": "^7.0.3", | ||
"css-loader": "^4.3.0", | ||
@@ -65,6 +74,10 @@ "dotenv-webpack": "^2.0.0", | ||
"eslint-plugin-react-hooks": "^4.1.0", | ||
"expose-loader": "^1.0.3", | ||
"file-loader": "^6.1.0", | ||
"lint-staged": "^10.3.0", | ||
"mini-css-extract-plugin": "^0.11.1", | ||
"mocha": "^8.2.1", | ||
"prettier": "^2.1.1", | ||
"react": "^16.8.3", | ||
"react-dom": "^16.8.3", | ||
"react-is": "^16.13.1", | ||
@@ -74,2 +87,3 @@ "rimraf": "^3.0.2", | ||
"sass-loader": "^10.0.2", | ||
"selenium-webdriver": "^4.0.0-alpha.8", | ||
"style-loader": "^1.2.1", | ||
@@ -81,5 +95,7 @@ "stylelint": "^13.7.0", | ||
"ts-loader": "^8.0.3", | ||
"ts-node": "^9.1.1", | ||
"typescript": "^4.0.2", | ||
"webpack": "^4.44.1", | ||
"webpack-cli": "^3.3.12" | ||
"webpack-cli": "^3.3.12", | ||
"webpack-dev-server": "^3.7.0" | ||
}, | ||
@@ -86,0 +102,0 @@ "peerDependencies": { |
# React CSV Importer | ||
![https://www.npmjs.com/package/react-csv-importer](https://img.shields.io/npm/v/react-csv-importer) ![https://github.com/beamworks/react-csv-importer/actions](https://github.com/beamworks/react-csv-importer/actions/workflows/test.yml/badge.svg) | ||
This library combines an uploader + CSV parser + raw file preview + UI for custom user column | ||
@@ -21,2 +23,3 @@ mapping, all in one. Relies on the popular PapaParse CSV library to preview and process file contents directly in the browser. | ||
- raw file preview | ||
- auto-map fields to matching column names | ||
- user-selectable column mapping (drag-drop UI) | ||
@@ -82,2 +85,4 @@ - optional fields | ||
In the above example, if the user uploads a CSV file with column headers "Name", "Email" and so on, the columns will be automatically matched to fields with same labels. If any of the headers do not match, the user will have an opportunity to manually remap columns to the defined fields. | ||
## Dependencies | ||
@@ -89,4 +94,30 @@ | ||
## Local Development | ||
Perform local `git clone`, etc. Then ensure modules are installed: | ||
```sh | ||
yarn # root folder only needs this for Husky pre-commit triggers | ||
cd package-core | ||
yarn # main package dev dependencies | ||
``` | ||
Most of the interesting stuff is inside `package-core` folder. | ||
To start Storybook to have a hot-reloaded local sandbox: | ||
```sh | ||
yarn storybook | ||
``` | ||
To run the end-to-end test suite: | ||
```sh | ||
yarn test | ||
``` | ||
## Changes | ||
- 0.4.0 | ||
- auto-assign column headers | ||
- 0.3.0 | ||
@@ -93,0 +124,0 @@ - allow passing PapaParse config options |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
156111
1204
138
52