From 19d98671e9b8046297f50d91164a67cad2f89eb2 Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Tue, 25 Jun 2024 18:11:24 -0400 Subject: [PATCH 1/2] chore: divide data table to smaller chunks --- packages/ui/src/DataTable/ColumnFilterMenu.js | 60 ++ packages/ui/src/DataTable/DropdownCell.js | 61 ++ packages/ui/src/DataTable/EditabelCell.js | 48 ++ packages/ui/src/DataTable/index.js | 548 +++--------------- .../ui/src/DataTable/utils/formatPasteData.js | 16 + packages/ui/src/DataTable/utils/getAllRows.js | 11 + .../ui/src/DataTable/utils/getCellCopyText.js | 7 + .../ui/src/DataTable/utils/getCellInfo.js | 36 ++ .../DataTable/utils/getFieldPathToField.js | 7 + .../DataTable/utils/getLastSelectedEntity.js | 11 + .../src/DataTable/utils/getNewEntToSelect.js | 25 + .../ui/src/DataTable/utils/getRowCopyText.js | 28 + .../src/DataTable/utils/handleCopyColumn.js | 21 + .../src/DataTable/utils/handleCopyHelper.js | 15 + .../ui/src/DataTable/utils/handleCopyRows.js | 23 + packages/ui/src/DataTable/utils/index.js | 51 ++ .../utils/isBottomRightCornerOfRectangle.js | 20 + .../ui/src/DataTable/utils/isEntityClean.js | 13 + .../ui/src/DataTable/utils/removeCleanRows.js | 22 + packages/ui/src/DataTable/utils/rowClick.js | 9 +- packages/ui/src/DataTable/utils/utils.js | 37 ++ packages/ui/src/TgSelect/index.js | 1 + 22 files changed, 594 insertions(+), 476 deletions(-) create mode 100644 packages/ui/src/DataTable/ColumnFilterMenu.js create mode 100644 packages/ui/src/DataTable/DropdownCell.js create mode 100644 packages/ui/src/DataTable/EditabelCell.js create mode 100644 packages/ui/src/DataTable/utils/formatPasteData.js create mode 100644 packages/ui/src/DataTable/utils/getAllRows.js create mode 100644 packages/ui/src/DataTable/utils/getCellCopyText.js create mode 100644 packages/ui/src/DataTable/utils/getCellInfo.js create mode 100644 packages/ui/src/DataTable/utils/getFieldPathToField.js create mode 100644 packages/ui/src/DataTable/utils/getLastSelectedEntity.js create mode 100644 packages/ui/src/DataTable/utils/getNewEntToSelect.js create mode 100644 packages/ui/src/DataTable/utils/getRowCopyText.js create mode 100644 packages/ui/src/DataTable/utils/handleCopyColumn.js create mode 100644 packages/ui/src/DataTable/utils/handleCopyHelper.js create mode 100644 packages/ui/src/DataTable/utils/handleCopyRows.js create mode 100644 packages/ui/src/DataTable/utils/index.js create mode 100644 packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js create mode 100644 packages/ui/src/DataTable/utils/isEntityClean.js create mode 100644 packages/ui/src/DataTable/utils/removeCleanRows.js create mode 100644 packages/ui/src/DataTable/utils/utils.js diff --git a/packages/ui/src/DataTable/ColumnFilterMenu.js b/packages/ui/src/DataTable/ColumnFilterMenu.js new file mode 100644 index 00000000..1688fa32 --- /dev/null +++ b/packages/ui/src/DataTable/ColumnFilterMenu.js @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import classNames from "classnames"; +import { Icon, Popover } from "@blueprintjs/core"; + +export const ColumnFilterMenu = ({ + addFilters, + compact, + currentFilter, + currentParams, + dataType, + extraCompact, + filterActiveForColumn, + FilterMenu, + filterOn, + removeSingleFilter, + schemaForField, + setNewParams +}) => { + const [columnFilterMenuOpen, setColumnFilterMenuOpen] = useState(false); + return ( + { + setColumnFilterMenuOpen(false); + }} + isOpen={columnFilterMenuOpen} + modifiers={{ + preventOverflow: { enabled: true }, + hide: { enabled: false }, + flip: { enabled: false } + }} + > + { + setColumnFilterMenuOpen(!columnFilterMenuOpen); + }} + className={classNames("tg-filter-menu-button", { + "tg-active-filter": !!filterActiveForColumn + })} + /> + { + setColumnFilterMenuOpen(false); + }} + /> + + ); +}; diff --git a/packages/ui/src/DataTable/DropdownCell.js b/packages/ui/src/DataTable/DropdownCell.js new file mode 100644 index 00000000..23e9ccd2 --- /dev/null +++ b/packages/ui/src/DataTable/DropdownCell.js @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import classNames from "classnames"; +import TgSelect from "../TgSelect"; + +export const DropdownCell = ({ + options, + isMulti, + initialValue, + finishEdit, + cancelEdit, + dataTest +}) => { + const [v, setV] = useState( + isMulti + ? initialValue.split(",").map(v => ({ value: v, label: v })) + : initialValue + ); + return ( +
+ { + if (isMulti) { + setV(val); + return; + } + finishEdit(val ? val.value : null); + }} + {...dataTest} + popoverProps={{ + onClose: e => { + if (isMulti) { + if (e && e.key === "Escape") { + cancelEdit(); + } else { + finishEdit( + v && v.map + ? v + .map(v => v.value) + .filter(v => v) + .join(",") + : v + ); + } + } else { + cancelEdit(); + } + } + }} + options={options.map(value => ({ label: value, value }))} + /> +
+ ); +}; diff --git a/packages/ui/src/DataTable/EditabelCell.js b/packages/ui/src/DataTable/EditabelCell.js new file mode 100644 index 00000000..176bdb3a --- /dev/null +++ b/packages/ui/src/DataTable/EditabelCell.js @@ -0,0 +1,48 @@ +import React, { useState } from "react"; + +export const EditableCell = ({ + shouldSelectAll, + stopSelectAll, + initialValue, + finishEdit, + cancelEdit, + isNumeric, + dataTest +}) => { + const [v, setV] = useState(initialValue); + return ( + { + if (shouldSelectAll && r) { + r?.select(); + stopSelectAll(); + } + }} + {...dataTest} + type={isNumeric ? "number" : undefined} + value={v} + autoFocus + onKeyDown={e => { + if (e.key === "Enter") { + finishEdit(v); + e.stopPropagation(); + } else if (e.key === "Escape") { + e.stopPropagation(); + cancelEdit(); + } + }} + onBlur={() => { + finishEdit(v); + }} + onChange={e => { + setV(e.target.value); + }} + /> + ); +}; diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 82a9391e..c49f533c 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -1,15 +1,11 @@ -/* eslint react/jsx-no-bind: 0 */ -import React, { useState } from "react"; +import React from "react"; import ReactDOM from "react-dom"; -import copy from "copy-to-clipboard"; -import download from "downloadjs"; import { invert, toNumber, isEmpty, min, max, - flatMap, set, map, toString, @@ -40,7 +36,6 @@ import { ContextMenu, Checkbox, Icon, - Popover, Intent, Callout, Tooltip @@ -58,30 +53,50 @@ import immer, { produceWithPatches, enablePatches, applyPatches } from "immer"; import papaparse from "papaparse"; import remarkGfm from "remark-gfm"; -import TgSelect from "../TgSelect"; -import { withHotkeys } from "../utils/hotkeyUtils"; +import { + computePresets, + defaultParsePaste, + formatPasteData, + getAllRows, + getCellCopyText, + getCellInfo, + getEntityIdToEntity, + getFieldPathToIndex, + getFieldPathToField, + getIdOrCodeOrIndex, + getLastSelectedEntity, + getNewEntToSelect, + getNumberStrAtEnd, + getRecordsFromIdMap, + getRowCopyText, + getSelectedRowsFromEntities, + handleCopyColumn, + handleCopyHelper, + handleCopyRows, + isBottomRightCornerOfRectangle, + isEntityClean, + removeCleanRows, + stripNumberAtEnd +} from "./utils"; import InfoHelper from "../InfoHelper"; +import { withHotkeys } from "../utils/hotkeyUtils"; import getTextFromEl from "../utils/getTextFromEl"; -import { getSelectedRowsFromEntities } from "./utils/selection"; import rowClick, { changeSelectedEntities, finalizeSelection } from "./utils/rowClick"; import PagingTool from "./PagingTool"; import FilterAndSortMenu from "./FilterAndSortMenu"; -import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; import SearchBar from "./SearchBar"; import DisplayOptions from "./DisplayOptions"; import DisabledLoadingComponent from "./DisabledLoadingComponent"; import SortableColumns from "./SortableColumns"; -import computePresets from "./utils/computePresets"; import dataTableEnhancer from "./dataTableEnhancer"; import defaultProps from "./defaultProps"; import "../toastr"; import "@teselagen/react-table/react-table.css"; import "./style.css"; -import { getRecordsFromIdMap } from "./utils/withSelectedEntities"; import { CellDragHandle } from "./CellDragHandle"; import { nanoid } from "nanoid"; import { SwitchField } from "../FormComponents"; @@ -90,12 +105,22 @@ import { editCellHelper } from "./editCellHelper"; import { getCellVal } from "./getCellVal"; import { getVals } from "./getVals"; import { throwFormError } from "../throwFormError"; +import { DropdownCell } from "./DropdownCell"; +import { EditableCell } from "./EditabelCell"; +import { ColumnFilterMenu } from "./ColumnFilterMenu"; enablePatches(); const PRIMARY_SELECTED_VAL = "main_cell"; dayjs.extend(localizedFormat); const IS_LINUX = window.navigator.platform.toLowerCase().search("linux") > -1; + +const itemSizeEstimators = { + compact: () => 25.34, + normal: () => 33.34, + comfortable: () => 41.34 +}; + class DataTable extends React.Component { constructor(props) { super(props); @@ -179,6 +204,7 @@ class DataTable extends React.Component { } }); } + state = { columns: [], fullscreen: false @@ -186,15 +212,11 @@ class DataTable extends React.Component { static defaultProps = defaultProps; - toggleFullscreen = () => { - this.setState({ - fullscreen: !this.state.fullscreen - }); - }; handleEnterStartCellEdit = e => { e.stopPropagation(); this.startCellEdit(this.getPrimarySelectedCellId()); }; + flashTableBorder = () => { try { const table = ReactDOM.findDOMNode(this.table); @@ -206,6 +228,7 @@ class DataTable extends React.Component { console.error(`err when flashing table border:`, e); } }; + handleUndo = () => { const { change, @@ -231,6 +254,7 @@ class DataTable extends React.Component { }); } }; + handleRedo = () => { const { change, @@ -255,6 +279,7 @@ class DataTable extends React.Component { }); } }; + updateFromProps = (oldProps, newProps) => { const { selectedIds, @@ -397,6 +422,7 @@ class DataTable extends React.Component { }, 0); } }; + formatAndValidateEntities = ( entities, { useDefaultValues, indexToStartAt } = {} @@ -436,6 +462,7 @@ class DataTable extends React.Component { validationErrors }; }; + formatAndValidateTableInitial = () => { const { _origEntities, @@ -584,6 +611,7 @@ class DataTable extends React.Component { props }); }; + handleCopyHotkey = e => { const { isCellEditable, reduxFormSelectedEntityIdMap } = computePresets( this.props @@ -800,6 +828,7 @@ class DataTable extends React.Component { }); } }; + updateValidationHelper = () => { const { entities, reduxFormCellValidation } = computePresets(this.props); this.updateValidation(entities, reduxFormCellValidation); @@ -816,6 +845,7 @@ class DataTable extends React.Component { change("reduxFormCellValidation", tableWideErr); this.forceUpdate(); }; + handleDeleteCell = () => { const { reduxFormSelectedCells, @@ -856,54 +886,6 @@ class DataTable extends React.Component { this.handleCopyHotkey(e); }; - getCellCopyText = cellWrapper => { - const text = cellWrapper && cellWrapper.getAttribute("data-copy-text"); - const jsonText = cellWrapper && cellWrapper.getAttribute("data-copy-json"); - - const textContent = text || cellWrapper.textContent || ""; - return [textContent, jsonText]; - }; - - handleCopyColumn = (e, cellWrapper, selectedRecords) => { - const specificColumn = cellWrapper.getAttribute("data-test"); - let rowElsToCopy = getAllRows(e); - if (!rowElsToCopy) return; - if (selectedRecords) { - const ids = selectedRecords.map(e => getIdOrCodeOrIndex(e)?.toString()); - rowElsToCopy = Array.from(rowElsToCopy).filter(rowEl => { - const id = rowEl.closest(".rt-tr-group")?.getAttribute("data-test-id"); - return id !== undefined && ids.includes(id); - }); - } - if (!rowElsToCopy) return; - this.handleCopyRows(rowElsToCopy, { - specificColumn, - onFinishMsg: "Column Copied" - }); - }; - handleCopyRows = ( - rowElsToCopy, - { specificColumn, onFinishMsg, isDownload } = {} - ) => { - let textToCopy = []; - const jsonToCopy = []; - forEach(rowElsToCopy, rowEl => { - const [t, j] = this.getRowCopyText(rowEl, { specificColumn }); - textToCopy.push(t); - jsonToCopy.push(j); - }); - textToCopy = textToCopy.filter(text => text).join("\n"); - if (!textToCopy) return window.toastr.warning("No text to copy"); - if (isDownload) { - download(textToCopy.replaceAll("\t", ","), "tableData.csv", "text/csv"); - } else { - this.handleCopyHelper( - textToCopy, - jsonToCopy, - onFinishMsg || "Row Copied" - ); - } - }; updateEntitiesHelper = (ents, fn) => { const { change, reduxFormEntitiesUndoRedoStack = { currentVersion: 0 } } = this.props; @@ -924,51 +906,11 @@ class DataTable extends React.Component { }); }; - getRowCopyText = (rowEl, { specificColumn } = {}) => { - //takes in a row element - if (!rowEl) return []; - const textContent = []; - const jsonText = []; - - forEach(rowEl.children, cellEl => { - const cellChild = cellEl.querySelector(`[data-copy-text]`); - if (!cellChild) { - if (specificColumn) return []; //strip it - return; //just leave it blank - } - if ( - specificColumn && - cellChild.getAttribute("data-test") !== specificColumn - ) { - return []; - } - const [t, j] = this.getCellCopyText(cellChild); - textContent.push(t); - jsonText.push(j); - }); - - return [flatMap(textContent).join("\t"), jsonText]; - }; - - handleCopyHelper = (stringToCopy, jsonToCopy, message) => { - !window.Cypress && - copy(stringToCopy, { - onCopy: clipboardData => { - clipboardData.setData("application/json", JSON.stringify(jsonToCopy)); - }, - // keep this so that pasting into spreadsheets works. - format: "text/plain" - }); - if (message) { - window.toastr.success(message); - } - }; - handleCopyTable = (e, opts) => { try { const allRowEls = getAllRows(e); if (!allRowEls) return; - this.handleCopyRows(allRowEls, { + handleCopyRows(allRowEls, { ...opts, onFinishMsg: "Table Copied" }); @@ -977,6 +919,7 @@ class DataTable extends React.Component { window.toastr.error("Error copying rows."); } }; + handleCopySelectedCells = e => { const { entities = [], @@ -1022,7 +965,7 @@ class DataTable extends React.Component { } else { const jsonRow = []; // ignore header - let [rowCopyText, json] = this.getRowCopyText(allRows[i + 1]); + let [rowCopyText, json] = getRowCopyText(allRows[i + 1]); rowCopyText = rowCopyText.split("\t"); times(row.length, i => { const cell = row[i]; @@ -1037,7 +980,7 @@ class DataTable extends React.Component { }); if (!fullCellText) return window.toastr.warning("No text to copy"); - this.handleCopyHelper(fullCellText, fullJson, "Selected cells copied"); + handleCopyHelper(fullCellText, fullJson, "Selected cells copied"); }; handleCopySelectedRows = (selectedRecords, e) => { @@ -1060,7 +1003,7 @@ class DataTable extends React.Component { if (!allRowEls) return; const rowEls = rowNumbersToCopy.map(i => allRowEls[i]); - this.handleCopyRows(rowEls, { + handleCopyRows(rowEls, { onFinishMsg: "Selected rows copied" }); } catch (error) { @@ -1117,6 +1060,7 @@ class DataTable extends React.Component { /> ); }; + getThComponent = compose( withProps(props => { const { columnindex } = props; @@ -1335,7 +1279,11 @@ class DataTable extends React.Component { icon="fullscreen" active={fullscreen} minimal - onClick={this.toggleFullscreen} + onClick={() => { + this.setState({ + fullscreen: !this.state.fullscreen + }); + }} /> ); @@ -1836,7 +1784,7 @@ class DataTable extends React.Component { data-tip="Download Table as CSV" minimal icon="download" - > + /> )} {!noFooter && ( @@ -2194,6 +2142,7 @@ class DataTable extends React.Component { change("reduxFormSelectedCells", newSelectedCells); }; + renderCheckboxHeader = () => { const { reduxFormSelectedEntityIdMap, @@ -2316,6 +2265,7 @@ class DataTable extends React.Component { change("reduxFormEditingCell", null); this.refocusTable(); }; + refocusTable = () => { setTimeout(() => { const table = ReactDOM.findDOMNode(this.table)?.closest( @@ -2638,7 +2588,7 @@ class DataTable extends React.Component { }} dataTest={dataTest} cancelEdit={this.cancelCellEdit} - > + /> ); } else { return ( @@ -2654,7 +2604,7 @@ class DataTable extends React.Component { finishEdit={newVal => { this.finishCellEdit(cellId, newVal); }} - > + /> ); } } @@ -2726,7 +2676,7 @@ class DataTable extends React.Component { {isSelectedCell && (isRect - ? this.isBottomRightCornerOfRectangle({ + ? isBottomRightCornerOfRectangle({ cellId, selectionGrid, lastRowIndex, @@ -2741,7 +2691,7 @@ class DataTable extends React.Component { cellId={cellId} isSelectionARectangle={this.isSelectionARectangle} onDragEnd={this.onDragEnd} - > + /> )} ); @@ -2751,26 +2701,6 @@ class DataTable extends React.Component { }); return columnsToRender; }; - isBottomRightCornerOfRectangle = ({ - cellId, - selectionGrid, - lastRowIndex, - lastCellIndex, - entityMap, - pathToIndex - }) => { - selectionGrid.forEach(row => { - // remove undefineds from start of row - while (row[0] === undefined && row.length) row.shift(); - }); - const [rowId, cellPath] = cellId.split(":"); - const ent = entityMap[rowId]; - if (!ent) return; - const { i } = ent; - const cellIndex = pathToIndex[cellPath]; - const isBottomRight = i === lastRowIndex && cellIndex === lastCellIndex; - return isBottomRight; - }; onDragEnd = cellsToSelect => { const { @@ -2975,6 +2905,7 @@ class DataTable extends React.Component { change("reduxFormSelectedCells", newReduxFormSelectedCells); }); }; + getCopyTextForCell = (val, row = {}, column = {}) => { const { cellRenderer } = computePresets(this.props); // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this) @@ -3060,6 +2991,7 @@ class DataTable extends React.Component { }); }); }; + getEditableTableInfoAndThrowFormError = () => { const { schema, reduxFormEntities, reduxFormCellValidation } = computePresets(this.props); @@ -3169,12 +3101,12 @@ class DataTable extends React.Component { //TODOCOPY: we need to make sure that the cell copy is being used by the row copy.. right now we have 2 different things going on //do we need to be able to copy hidden cells? It seems like it should just copy what's on the page..? const specificColumn = cellWrapper.getAttribute("data-test"); - this.handleCopyRows([cellWrapper.closest(".rt-tr")], { + handleCopyRows([cellWrapper.closest(".rt-tr")], { specificColumn, onFinishMsg: "Cell copied" }); - const [text, jsonText] = this.getCellCopyText(cellWrapper); - this.handleCopyHelper(text, jsonText); + const [text, jsonText] = getCellCopyText(cellWrapper); + handleCopyHelper(text, jsonText); }} text="Cell" /> @@ -3184,7 +3116,7 @@ class DataTable extends React.Component { { - this.handleCopyColumn(e, cellWrapper); + handleCopyColumn(e, cellWrapper); }} text="Column" /> @@ -3194,7 +3126,7 @@ class DataTable extends React.Component { { - this.handleCopyColumn(e, cellWrapper, selectedRecords); + handleCopyColumn(e, cellWrapper, selectedRecords); }} text="Column (Selected)" /> @@ -3212,7 +3144,7 @@ class DataTable extends React.Component { { - this.handleCopyRows([row]); + handleCopyRows([row]); // loop through each cell in the row }} text="Row" @@ -3486,7 +3418,7 @@ class DataTable extends React.Component { })} > {columnTitleTextified && !noTitle && ( - + <> {maybeCheckbox} {renderTitleInner ? renderTitleInner : columnTitle}{" "} - + )}
25.34, - normal: () => 33.34, - comfortable: () => 41.34 -}; - -function getCellInfo({ - columnIndex, - columnPath, - rowId, - schema, - entities, - rowIndex, - isEntityDisabled, - entity -}) { - const leftpath = schema.fields[columnIndex - 1]?.path; - const rightpath = schema.fields[columnIndex + 1]?.path; - const cellIdToLeft = leftpath && `${rowId}:${leftpath}`; - const cellIdToRight = rightpath && `${rowId}:${rightpath}`; - const rowAboveId = - entities[rowIndex - 1] && - getIdOrCodeOrIndex(entities[rowIndex - 1], rowIndex - 1); - const rowBelowId = - entities[rowIndex + 1] && - getIdOrCodeOrIndex(entities[rowIndex + 1], rowIndex + 1); - const cellIdAbove = rowAboveId && `${rowAboveId}:${columnPath}`; - const cellIdBelow = rowBelowId && `${rowBelowId}:${columnPath}`; - - const cellId = `${rowId}:${columnPath}`; - const rowDisabled = isEntityDisabled(entity); - return { - cellId, - cellIdAbove, - cellIdToRight, - cellIdBelow, - cellIdToLeft, - rowDisabled - }; -} - -function ColumnFilterMenu({ - FilterMenu, - filterActiveForColumn, - compact, - extraCompact, - ...rest -}) { - const [columnFilterMenuOpen, setColumnFilterMenuOpen] = useState(false); - return ( - { - setColumnFilterMenuOpen(false); - }} - isOpen={columnFilterMenuOpen} - modifiers={{ - preventOverflow: { enabled: true }, - hide: { enabled: false }, - flip: { enabled: false } - }} - > - { - setColumnFilterMenuOpen(!columnFilterMenuOpen); - }} - className={classNames("tg-filter-menu-button", { - "tg-active-filter": !!filterActiveForColumn - })} - /> - { - setColumnFilterMenuOpen(false); - }} - {...rest} - /> - - ); -} - -function getLastSelectedEntity(idMap) { - let lastSelectedEnt; - let latestTime; - forEach(idMap, ({ time, entity }) => { - if (!latestTime || time > latestTime) { - lastSelectedEnt = entity; - latestTime = time; - } - }); - return lastSelectedEnt; -} - -function getNewEntToSelect({ - type, - lastSelectedIndex, - entities, - isEntityDisabled -}) { - let newIndexToSelect; - if (type === "up") { - newIndexToSelect = lastSelectedIndex - 1; - } else { - newIndexToSelect = lastSelectedIndex + 1; - } - const newEntToSelect = entities[newIndexToSelect]; - if (!newEntToSelect) return; - if (isEntityDisabled && isEntityDisabled(newEntToSelect)) { - return getNewEntToSelect({ - type, - lastSelectedIndex: newIndexToSelect, - entities, - isEntityDisabled - }); - } else { - return newEntToSelect; - } -} - -function getAllRows(e) { - const el = e.target.querySelector(".data-table-container") - ? e.target.querySelector(".data-table-container") - : e.target.closest(".data-table-container"); - - const allRowEls = el.querySelectorAll(".rt-tr"); - if (!allRowEls || !allRowEls.length) { - return; - } - return allRowEls; -} - -function EditableCell({ - shouldSelectAll, - stopSelectAll, - initialValue, - finishEdit, - cancelEdit, - isNumeric, - dataTest -}) { - const [v, setV] = useState(initialValue); - return ( - { - if (shouldSelectAll && r) { - r?.select(); - stopSelectAll(); - } - }} - {...dataTest} - type={isNumeric ? "number" : undefined} - value={v} - autoFocus - onKeyDown={e => { - if (e.key === "Enter") { - finishEdit(v); - e.stopPropagation(); - } else if (e.key === "Escape") { - e.stopPropagation(); - cancelEdit(); - } - }} - onBlur={() => { - finishEdit(v); - }} - onChange={e => { - setV(e.target.value); - }} - > - ); -} - -function DropdownCell({ - options, - isMulti, - initialValue, - finishEdit, - cancelEdit, - dataTest -}) { - const [v, setV] = useState( - isMulti - ? initialValue.split(",").map(v => ({ value: v, label: v })) - : initialValue - ); - return ( -
- { - if (isMulti) { - setV(val); - return; - } - finishEdit(val ? val.value : null); - }} - {...dataTest} - popoverProps={{ - onClose: e => { - if (isMulti) { - if (e && e.key === "Escape") { - cancelEdit(); - } else { - finishEdit( - v && v.map - ? v - .map(v => v.value) - .filter(v => v) - .join(",") - : v - ); - } - } else { - cancelEdit(); - } - } - }} - options={options.map(value => ({ label: value, value }))} - > -
- ); -} - -function getFieldPathToIndex(schema) { - const fieldToIndex = {}; - schema.fields.forEach((f, i) => { - fieldToIndex[f.path] = i; - }); - return fieldToIndex; -} - -function getFieldPathToField(schema) { - const fieldPathToField = {}; - schema.fields.forEach(f => { - fieldPathToField[f.path] = f; - }); - return fieldPathToField; -} - -const defaultParsePaste = str => { - return str.split(/\r\n|\n|\r/).map(row => row.split("\t")); -}; - -function getEntityIdToEntity(entities) { - const entityIdToEntity = {}; - entities.forEach((e, i) => { - entityIdToEntity[getIdOrCodeOrIndex(e, i)] = { e, i }; - }); - return entityIdToEntity; -} - -function endsWithNumber(str) { - return /[0-9]+$/.test(str); -} - -function getNumberStrAtEnd(str) { - if (endsWithNumber(str)) { - return str.match(/[0-9]+$/)[0]; - } - - return null; -} - -function stripNumberAtEnd(str) { - return str?.replace?.(getNumberStrAtEnd(str), ""); -} - -export function isEntityClean(e) { - let isClean = true; - some(e, (val, key) => { - if (key === "id") return; - if (key === "_isClean") return; - if (val) { - isClean = false; - return true; - } - }); - return isClean; -} - -const formatPasteData = ({ schema, newVal, path }) => { - const pathToField = getFieldPathToField(schema); - const column = pathToField[path]; - if (column.type === "genericSelect") { - if (newVal?.__genSelCol === path) { - newVal = newVal.__strVal; - } else { - newVal = undefined; - } - } else { - newVal = Object.hasOwn(newVal, "__strVal") ? newVal.__strVal : newVal; - } - return newVal; -}; - -export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { - const toFilterOut = {}; - const entsToUse = (reduxFormEntities || []).filter(e => { - if (!(e._isClean || isEntityClean(e))) return true; - else { - toFilterOut[getIdOrCodeOrIndex(e)] = true; - return false; - } - }); - - const validationToUse = {}; - forEach(reduxFormCellValidation, (v, k) => { - const [rowId] = k.split(":"); - if (!toFilterOut[rowId]) { - validationToUse[k] = v; - } - }); - return { entsToUse, validationToUse }; -} diff --git a/packages/ui/src/DataTable/utils/formatPasteData.js b/packages/ui/src/DataTable/utils/formatPasteData.js new file mode 100644 index 00000000..cde19517 --- /dev/null +++ b/packages/ui/src/DataTable/utils/formatPasteData.js @@ -0,0 +1,16 @@ +import { getFieldPathToField } from "./getFieldPathToField"; + +export const formatPasteData = ({ schema, newVal, path }) => { + const pathToField = getFieldPathToField(schema); + const column = pathToField[path]; + if (column.type === "genericSelect") { + if (newVal?.__genSelCol === path) { + newVal = newVal.__strVal; + } else { + newVal = undefined; + } + } else { + newVal = Object.hasOwn(newVal, "__strVal") ? newVal.__strVal : newVal; + } + return newVal; +}; diff --git a/packages/ui/src/DataTable/utils/getAllRows.js b/packages/ui/src/DataTable/utils/getAllRows.js new file mode 100644 index 00000000..64c200f1 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getAllRows.js @@ -0,0 +1,11 @@ +export const getAllRows = e => { + const el = e.target.querySelector(".data-table-container") + ? e.target.querySelector(".data-table-container") + : e.target.closest(".data-table-container"); + + const allRowEls = el.querySelectorAll(".rt-tr"); + if (!allRowEls || !allRowEls.length) { + return; + } + return allRowEls; +}; diff --git a/packages/ui/src/DataTable/utils/getCellCopyText.js b/packages/ui/src/DataTable/utils/getCellCopyText.js new file mode 100644 index 00000000..7fca4538 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getCellCopyText.js @@ -0,0 +1,7 @@ +export const getCellCopyText = cellWrapper => { + const text = cellWrapper && cellWrapper.getAttribute("data-copy-text"); + const jsonText = cellWrapper && cellWrapper.getAttribute("data-copy-json"); + + const textContent = text || cellWrapper.textContent || ""; + return [textContent, jsonText]; +}; diff --git a/packages/ui/src/DataTable/utils/getCellInfo.js b/packages/ui/src/DataTable/utils/getCellInfo.js new file mode 100644 index 00000000..d863c727 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getCellInfo.js @@ -0,0 +1,36 @@ +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; + +export const getCellInfo = ({ + columnIndex, + columnPath, + rowId, + schema, + entities, + rowIndex, + isEntityDisabled, + entity +}) => { + const leftpath = schema.fields[columnIndex - 1]?.path; + const rightpath = schema.fields[columnIndex + 1]?.path; + const cellIdToLeft = leftpath && `${rowId}:${leftpath}`; + const cellIdToRight = rightpath && `${rowId}:${rightpath}`; + const rowAboveId = + entities[rowIndex - 1] && + getIdOrCodeOrIndex(entities[rowIndex - 1], rowIndex - 1); + const rowBelowId = + entities[rowIndex + 1] && + getIdOrCodeOrIndex(entities[rowIndex + 1], rowIndex + 1); + const cellIdAbove = rowAboveId && `${rowAboveId}:${columnPath}`; + const cellIdBelow = rowBelowId && `${rowBelowId}:${columnPath}`; + + const cellId = `${rowId}:${columnPath}`; + const rowDisabled = isEntityDisabled(entity); + return { + cellId, + cellIdAbove, + cellIdToRight, + cellIdBelow, + cellIdToLeft, + rowDisabled + }; +}; diff --git a/packages/ui/src/DataTable/utils/getFieldPathToField.js b/packages/ui/src/DataTable/utils/getFieldPathToField.js new file mode 100644 index 00000000..52b4fe33 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getFieldPathToField.js @@ -0,0 +1,7 @@ +export const getFieldPathToField = schema => { + const fieldPathToField = {}; + schema.fields.forEach(f => { + fieldPathToField[f.path] = f; + }); + return fieldPathToField; +}; diff --git a/packages/ui/src/DataTable/utils/getLastSelectedEntity.js b/packages/ui/src/DataTable/utils/getLastSelectedEntity.js new file mode 100644 index 00000000..eb09a2b6 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getLastSelectedEntity.js @@ -0,0 +1,11 @@ +export const getLastSelectedEntity = idMap => { + let lastSelectedEnt; + let latestTime; + idMap.forEach(({ time, entity }) => { + if (!latestTime || time > latestTime) { + lastSelectedEnt = entity; + latestTime = time; + } + }); + return lastSelectedEnt; +}; diff --git a/packages/ui/src/DataTable/utils/getNewEntToSelect.js b/packages/ui/src/DataTable/utils/getNewEntToSelect.js new file mode 100644 index 00000000..a7d47155 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getNewEntToSelect.js @@ -0,0 +1,25 @@ +export const getNewEntToSelect = ({ + type, + lastSelectedIndex, + entities, + isEntityDisabled +}) => { + let newIndexToSelect; + if (type === "up") { + newIndexToSelect = lastSelectedIndex - 1; + } else { + newIndexToSelect = lastSelectedIndex + 1; + } + const newEntToSelect = entities[newIndexToSelect]; + if (!newEntToSelect) return; + if (isEntityDisabled && isEntityDisabled(newEntToSelect)) { + return getNewEntToSelect({ + type, + lastSelectedIndex: newIndexToSelect, + entities, + isEntityDisabled + }); + } else { + return newEntToSelect; + } +}; diff --git a/packages/ui/src/DataTable/utils/getRowCopyText.js b/packages/ui/src/DataTable/utils/getRowCopyText.js new file mode 100644 index 00000000..88a389a5 --- /dev/null +++ b/packages/ui/src/DataTable/utils/getRowCopyText.js @@ -0,0 +1,28 @@ +import { getCellCopyText } from "./getCellCopyText"; +import { flatMap } from "lodash-es"; + +export const getRowCopyText = (rowEl, { specificColumn } = {}) => { + //takes in a row element + if (!rowEl) return []; + const textContent = []; + const jsonText = []; + + rowEl.children.forEach(cellEl => { + const cellChild = cellEl.querySelector(`[data-copy-text]`); + if (!cellChild) { + if (specificColumn) return []; //strip it + return; //just leave it blank + } + if ( + specificColumn && + cellChild.getAttribute("data-test") !== specificColumn + ) { + return []; + } + const [t, j] = getCellCopyText(cellChild); + textContent.push(t); + jsonText.push(j); + }); + + return [flatMap(textContent).join("\t"), jsonText]; +}; diff --git a/packages/ui/src/DataTable/utils/handleCopyColumn.js b/packages/ui/src/DataTable/utils/handleCopyColumn.js new file mode 100644 index 00000000..d306053e --- /dev/null +++ b/packages/ui/src/DataTable/utils/handleCopyColumn.js @@ -0,0 +1,21 @@ +import { getAllRows } from "./getAllRows"; +import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { handleCopyRows } from "./handleCopyRows"; + +export const handleCopyColumn = (e, cellWrapper, selectedRecords) => { + const specificColumn = cellWrapper.getAttribute("data-test"); + let rowElsToCopy = getAllRows(e); + if (!rowElsToCopy) return; + if (selectedRecords) { + const ids = selectedRecords.map(e => getIdOrCodeOrIndex(e)?.toString()); + rowElsToCopy = Array.from(rowElsToCopy).filter(rowEl => { + const id = rowEl.closest(".rt-tr-group")?.getAttribute("data-test-id"); + return id !== undefined && ids.includes(id); + }); + } + if (!rowElsToCopy) return; + handleCopyRows(rowElsToCopy, { + specificColumn, + onFinishMsg: "Column Copied" + }); +}; diff --git a/packages/ui/src/DataTable/utils/handleCopyHelper.js b/packages/ui/src/DataTable/utils/handleCopyHelper.js new file mode 100644 index 00000000..9d603e99 --- /dev/null +++ b/packages/ui/src/DataTable/utils/handleCopyHelper.js @@ -0,0 +1,15 @@ +import copy from "copy-to-clipboard"; + +export const handleCopyHelper = (stringToCopy, jsonToCopy, message) => { + !window.Cypress && + copy(stringToCopy, { + onCopy: clipboardData => { + clipboardData.setData("application/json", JSON.stringify(jsonToCopy)); + }, + // keep this so that pasting into spreadsheets works. + format: "text/plain" + }); + if (message) { + window.toastr.success(message); + } +}; diff --git a/packages/ui/src/DataTable/utils/handleCopyRows.js b/packages/ui/src/DataTable/utils/handleCopyRows.js new file mode 100644 index 00000000..197cbabc --- /dev/null +++ b/packages/ui/src/DataTable/utils/handleCopyRows.js @@ -0,0 +1,23 @@ +import download from "downloadjs"; +import { getRowCopyText } from "./getRowCopyText"; +import { handleCopyHelper } from "./handleCopyHelper"; + +export const handleCopyRows = ( + rowElsToCopy, + { specificColumn, onFinishMsg, isDownload } = {} +) => { + let textToCopy = []; + const jsonToCopy = []; + rowElsToCopy.forEach(rowEl => { + const [t, j] = getRowCopyText(rowEl, { specificColumn }); + textToCopy.push(t); + jsonToCopy.push(j); + }); + textToCopy = textToCopy.filter(text => text).join("\n"); + if (!textToCopy) return window.toastr.warning("No text to copy"); + if (isDownload) { + download(textToCopy.replaceAll("\t", ","), "tableData.csv", "text/csv"); + } else { + handleCopyHelper(textToCopy, jsonToCopy, onFinishMsg || "Row Copied"); + } +}; diff --git a/packages/ui/src/DataTable/utils/index.js b/packages/ui/src/DataTable/utils/index.js new file mode 100644 index 00000000..48307406 --- /dev/null +++ b/packages/ui/src/DataTable/utils/index.js @@ -0,0 +1,51 @@ +import { isEntityClean } from "./isEntityClean"; +import { getSelectedRowsFromEntities } from "./selection"; +import { removeCleanRows } from "./removeCleanRows"; +import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import computePresets from "./computePresets"; +import { getRecordsFromIdMap } from "./withSelectedEntities"; +import { formatPasteData } from "./formatPasteData"; +import { getFieldPathToField } from "./getFieldPathToField"; +import { + defaultParsePaste, + getEntityIdToEntity, + getFieldPathToIndex, + getNumberStrAtEnd, + stripNumberAtEnd +} from "./utils"; +import { getAllRows } from "./getAllRows"; +import { getNewEntToSelect } from "./getNewEntToSelect"; +import { getLastSelectedEntity } from "./getLastSelectedEntity"; +import { getCellInfo } from "./getCellInfo"; +import { getCellCopyText } from "./getCellCopyText"; +import { getRowCopyText } from "./getRowCopyText"; +import { handleCopyHelper } from "./handleCopyHelper"; +import { handleCopyRows } from "./handleCopyRows"; +import { handleCopyColumn } from "./handleCopyColumn"; +import { isBottomRightCornerOfRectangle } from "./isBottomRightCornerOfRectangle"; + +export { + computePresets, + defaultParsePaste, + formatPasteData, + getAllRows, + getCellCopyText, + getCellInfo, + getEntityIdToEntity, + getFieldPathToIndex, + getFieldPathToField, + getIdOrCodeOrIndex, + getLastSelectedEntity, + getNewEntToSelect, + getNumberStrAtEnd, + getRecordsFromIdMap, + getRowCopyText, + getSelectedRowsFromEntities, + handleCopyColumn, + handleCopyHelper, + handleCopyRows, + isBottomRightCornerOfRectangle, + isEntityClean, + removeCleanRows, + stripNumberAtEnd +}; diff --git a/packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js b/packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js new file mode 100644 index 00000000..271e26ed --- /dev/null +++ b/packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js @@ -0,0 +1,20 @@ +export const isBottomRightCornerOfRectangle = ({ + cellId, + selectionGrid, + lastRowIndex, + lastCellIndex, + entityMap, + pathToIndex +}) => { + selectionGrid.forEach(row => { + // remove undefineds from start of row + while (row[0] === undefined && row.length) row.shift(); + }); + const [rowId, cellPath] = cellId.split(":"); + const ent = entityMap[rowId]; + if (!ent) return; + const { i } = ent; + const cellIndex = pathToIndex[cellPath]; + const isBottomRight = i === lastRowIndex && cellIndex === lastCellIndex; + return isBottomRight; +}; diff --git a/packages/ui/src/DataTable/utils/isEntityClean.js b/packages/ui/src/DataTable/utils/isEntityClean.js new file mode 100644 index 00000000..acf34e37 --- /dev/null +++ b/packages/ui/src/DataTable/utils/isEntityClean.js @@ -0,0 +1,13 @@ +export function isEntityClean(e) { + let isClean = true; + e.some((val, key) => { + if (key === "id") return false; + if (key === "_isClean") return false; + if (val) { + isClean = false; + return true; + } + return false; + }); + return isClean; +} diff --git a/packages/ui/src/DataTable/utils/removeCleanRows.js b/packages/ui/src/DataTable/utils/removeCleanRows.js new file mode 100644 index 00000000..b95f5552 --- /dev/null +++ b/packages/ui/src/DataTable/utils/removeCleanRows.js @@ -0,0 +1,22 @@ +import { isEntityClean } from "./isEntityClean"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; + +export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { + const toFilterOut = {}; + const entsToUse = (reduxFormEntities || []).filter(e => { + if (!(e._isClean || isEntityClean(e))) return true; + else { + toFilterOut[getIdOrCodeOrIndex(e)] = true; + return false; + } + }); + + const validationToUse = {}; + reduxFormCellValidation.forEach((v, k) => { + const [rowId] = k.split(":"); + if (!toFilterOut[rowId]) { + validationToUse[k] = v; + } + }); + return { entsToUse, validationToUse }; +} diff --git a/packages/ui/src/DataTable/utils/rowClick.js b/packages/ui/src/DataTable/utils/rowClick.js index 190d3cdb..8c4c5570 100644 --- a/packages/ui/src/DataTable/utils/rowClick.js +++ b/packages/ui/src/DataTable/utils/rowClick.js @@ -124,8 +124,10 @@ export function changeSelectedEntities({ idMap, entities = [], change }) { change("reduxFormSelectedEntityIdMap", newIdMap); } -export function finalizeSelection({ idMap, entities, props }) { - const { +export function finalizeSelection({ + idMap, + entities, + props: { onDeselect, onSingleRowSelect, onMultiRowSelect, @@ -133,7 +135,8 @@ export function finalizeSelection({ idMap, entities, props }) { onRowSelect, noSelect, change - } = props; + } +}) { if (noSelect) return; if ( noDeselectAll && diff --git a/packages/ui/src/DataTable/utils/utils.js b/packages/ui/src/DataTable/utils/utils.js new file mode 100644 index 00000000..b15021e3 --- /dev/null +++ b/packages/ui/src/DataTable/utils/utils.js @@ -0,0 +1,37 @@ +import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; + +export const getFieldPathToIndex = schema => { + const fieldToIndex = {}; + schema.fields.forEach((f, i) => { + fieldToIndex[f.path] = i; + }); + return fieldToIndex; +}; + +export const defaultParsePaste = str => { + return str.split(/\r\n|\n|\r/).map(row => row.split("\t")); +}; + +export const getEntityIdToEntity = entities => { + const entityIdToEntity = {}; + entities.forEach((e, i) => { + entityIdToEntity[getIdOrCodeOrIndex(e, i)] = { e, i }; + }); + return entityIdToEntity; +}; + +const endsWithNumber = str => { + return /[0-9]+$/.test(str); +}; + +export const getNumberStrAtEnd = str => { + if (endsWithNumber(str)) { + return str.match(/[0-9]+$/)[0]; + } + + return null; +}; + +export const stripNumberAtEnd = str => { + return str?.replace?.(getNumberStrAtEnd(str), ""); +}; diff --git a/packages/ui/src/TgSelect/index.js b/packages/ui/src/TgSelect/index.js index b6403e9d..34728a1e 100644 --- a/packages/ui/src/TgSelect/index.js +++ b/packages/ui/src/TgSelect/index.js @@ -512,6 +512,7 @@ export const itemListPredicate = (_queryString = "", items, isSimpleSearch) => { export function simplesearch(needle, haystack) { return (haystack || "").indexOf(needle) !== -1; } + function tagOptionRender(vals) { if (vals.noTagStyle) return vals.label; return ; From 5af38fa54bbbb6c251b6a05734db3f42a242152e Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Fri, 28 Jun 2024 11:04:32 -0400 Subject: [PATCH 2/2] fix: upgrade react-dom --- example-demos/oveViteDemo/src/index.jsx | 10 +- .../src/{index.js => index.jsx} | 10 +- helperUtils/renderDemo.js | 2 - helperUtils/renderDemo.jsx | 3 + .../ui/cypress/e2e/EditableCellTable.spec.js | 20 +- .../ui/cypress/e2e/UploadCsvWizard.spec.js | 70 +- packages/ui/cypress/e2e/dataTable.spec.js | 5 +- packages/ui/cypress/e2e/upload.spec.js | 2 +- packages/ui/cypress/support/index.js | 4 +- .../ui/demo/src/examples/UploadCsvWizard.js | 2 +- packages/ui/demo/src/examples/UploaderDemo.js | 4 - packages/ui/demo/src/index.js | 5 +- packages/ui/src/DataTable/CellDragHandle.js | 13 +- packages/ui/src/DataTable/EditabelCell.js | 51 +- packages/ui/src/DataTable/PagingTool.js | 2 +- packages/ui/src/DataTable/index.js | 554 ++++++------ .../src/DataTable/utils/getIdOrCodeOrIndex.js | 2 +- .../DataTable/utils/getLastSelectedEntity.js | 2 +- .../ui/src/DataTable/utils/getRowCopyText.js | 10 +- .../src/DataTable/utils/handleCopyColumn.js | 2 +- packages/ui/src/DataTable/utils/index.js | 2 +- .../ui/src/DataTable/utils/isEntityClean.js | 14 +- .../ui/src/DataTable/utils/removeCleanRows.js | 6 +- packages/ui/src/DataTable/utils/rowClick.js | 2 +- packages/ui/src/DataTable/utils/selection.js | 2 +- packages/ui/src/DataTable/utils/utils.js | 2 +- .../src/DataTable/validateTableWideErrors.js | 2 +- packages/ui/src/FillWindow.js | 5 +- packages/ui/src/FormComponents/Uploader.js | 800 +++++++++--------- .../src/FormComponents/tryToMatchSchemas.js | 6 - packages/ui/src/TgSelect/index.js | 1 - packages/ui/src/UploadCsvWizard.js | 683 +++++++-------- packages/ui/src/index.js | 6 +- packages/ui/src/showDialogOnDocBody.js | 14 +- packages/ui/src/useDialog.js | 11 +- packages/ui/src/utils/renderOnDoc.js | 13 +- 36 files changed, 1124 insertions(+), 1218 deletions(-) rename example-demos/oveWebpackDemo/src/{index.js => index.jsx} (57%) delete mode 100644 helperUtils/renderDemo.js create mode 100644 helperUtils/renderDemo.jsx diff --git a/example-demos/oveViteDemo/src/index.jsx b/example-demos/oveViteDemo/src/index.jsx index 122a5b87..8cf9dcc2 100644 --- a/example-demos/oveViteDemo/src/index.jsx +++ b/example-demos/oveViteDemo/src/index.jsx @@ -1,6 +1,6 @@ import "./shimGlobal"; import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import { Loading } from "@teselagen/ui"; @@ -10,14 +10,16 @@ import App from "./App"; import * as serviceWorker from "./serviceWorker"; -ReactDOM.render( +const domNode = document.getElementById("root"); +const root = createRoot(domNode); + +root.render( - , - document.getElementById("root") + ); // If you want your app to work offline and load faster, you can change diff --git a/example-demos/oveWebpackDemo/src/index.js b/example-demos/oveWebpackDemo/src/index.jsx similarity index 57% rename from example-demos/oveWebpackDemo/src/index.js rename to example-demos/oveWebpackDemo/src/index.jsx index d772e46f..5f969828 100644 --- a/example-demos/oveWebpackDemo/src/index.js +++ b/example-demos/oveWebpackDemo/src/index.jsx @@ -1,15 +1,17 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import store from "./store"; import "./index.css"; import App from "./App"; -ReactDOM.render( +const domNode = document.getElementById("root"); +const root = createRoot(domNode); + +root.render( - , - document.getElementById("root") + ); diff --git a/helperUtils/renderDemo.js b/helperUtils/renderDemo.js deleted file mode 100644 index e1439ab7..00000000 --- a/helperUtils/renderDemo.js +++ /dev/null @@ -1,2 +0,0 @@ -import { render } from "react-dom"; -export default Demo => render(, document.querySelector("#demo")); diff --git a/helperUtils/renderDemo.jsx b/helperUtils/renderDemo.jsx new file mode 100644 index 00000000..e47a951f --- /dev/null +++ b/helperUtils/renderDemo.jsx @@ -0,0 +1,3 @@ +import { createRoot } from "react-dom/client"; +const root = createRoot(document.querySelector("#demo")); +export default Demo => root.render(); diff --git a/packages/ui/cypress/e2e/EditableCellTable.spec.js b/packages/ui/cypress/e2e/EditableCellTable.spec.js index 5d1cdd5e..9fefb64e 100644 --- a/packages/ui/cypress/e2e/EditableCellTable.spec.js +++ b/packages/ui/cypress/e2e/EditableCellTable.spec.js @@ -29,8 +29,6 @@ describe("EditableCellTable.spec", () => { cy.get(".cellDragHandle"); cy.get(`[data-test="tgCell_name"]:first`).dblclick(); cy.get(".cellDragHandle").should("not.exist"); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(0); cy.focused().type("_zonk{enter}"); cy.get( `[data-tip="Must include the letter 'a'"] [data-test="tgCell_name"]:first` @@ -39,8 +37,6 @@ describe("EditableCellTable.spec", () => { }); it(`typing a letter should start edit`, () => { cy.visit("#/DataTable%20-%20EditableCellTable"); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(0); cy.get(`[data-test="tgCell_name"]:first`).type("zonk{enter}"); cy.get(`[data-test="tgCell_name"]:first`).should("contain", "zonk"); }); @@ -126,8 +122,6 @@ describe("EditableCellTable.spec", () => { `[data-tip="Must be a number"] [data-test="tgCell_howMany"]:first` ).should("contain", "NaN"); //should lowercase "Tom" cy.get(`[data-test="tgCell_howMany"]:first`).dblclick(); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(0); cy.focused().type("11{enter}"); cy.get(`[data-test="tgCell_howMany"]:first`).should("contain", "12"); //should have 12 post format cy.get( @@ -166,10 +160,10 @@ describe("EditableCellTable.spec", () => { // cy.get( // `.rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_name"]` // ).should("not.exist"); - // cy.get(`[data-test="tgCell_name"]`).eq(3).click({ force: true }); + // cy.get(`[data-test="tgCell_name"]`).eq(3).click(); // cy.get(`.rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_name"]`); - // cy.focused().type(`{enter}`) - // cy.focused().type(`haha{enter}`) + // cy.focused().type(`{enter}`); + // cy.focused().type(`haha{enter}`); // cy.get( // `.rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_name"]` // ).should("not.exist"); @@ -231,9 +225,7 @@ describe("EditableCellTable.spec", () => { cy.get( `[data-index="1"] .rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_type"]` ); - cy.get(`[data-index="49"] [data-test="tgCell_isProtein"]`).click({ - force: true - }); + cy.get(`[data-index="49"] [data-test="tgCell_isProtein"]`).click(); cy.get( `[data-index="49"] .rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_isProtein"]` ); @@ -253,12 +245,8 @@ describe("EditableCellTable.spec", () => { const redoCmd = IS_LINUX ? `{alt}{shift}z` : "{meta}{shift}z"; cy.visit("#/DataTable%20-%20EditableCellTable"); cy.get(`.rt-td:contains(tom88)`).dblclick(); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(0); cy.focused().type("{selectall}tasty55{enter}"); cy.get(`.rt-td:contains(tasty55)`).dblclick(); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(0); cy.focused().type("{selectall}delishhh{enter}"); cy.get(`.rt-td:contains(delishhh)`); cy.focused().type(undoCmd); diff --git a/packages/ui/cypress/e2e/UploadCsvWizard.spec.js b/packages/ui/cypress/e2e/UploadCsvWizard.spec.js index fcbfcc6d..874a46a9 100644 --- a/packages/ui/cypress/e2e/UploadCsvWizard.spec.js +++ b/packages/ui/cypress/e2e/UploadCsvWizard.spec.js @@ -147,17 +147,6 @@ describe("UploadCsvWizard.spec", () => { }); it(`messed up headers should trigger the wizard. editing the added file should work`, () => { cy.visit("#/UploadCsvWizard"); - // cy.contains("Build CSV File").click(); - // cy.get(`.rt-td [data-test="tgCell_description"]`) - // .eq(1) - // .click({ force: true }); - // cy.focused().type("description{enter}"); - - // cy.get( - // `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_name"]:first` - // ).dblclick({ force: true }); - // cy.focused().type("a{enter}"); - // cy.contains(`.bp3-dialog button`, "Cancel").click(); cy.uploadFile( ".tg-dropzone", "testUploadWizard_messedUpHeaders.csv", @@ -177,19 +166,21 @@ describe("UploadCsvWizard.spec", () => { ); cy.get(`.tg-test-is-regex`).click(); - cy.contains(".tg-select-option", "typo").click({ force: true }); + cy.contains(".tg-select-option", "typo").click(); cy.contains(".bp3-dialog", `zonk`).should("exist"); //the data from the file should be previewed cy.contains("Review and Edit Data").click(); cy.get( `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_name"]:first` - ).dblclick({ force: true }); - cy.focused().type("a{enter}"); + ) + .parent() + .type("a{enter}"); cy.get( `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_sequence"]:first` - ).dblclick({ force: true }); - cy.focused().type("g{enter}"); + ) + .parent() + .type("g{enter}"); cy.dragBetween(`.cellDragHandle`, `.rt-tr-last-row`); cy.contains("Add File").click(); cy.contains(`testUploadWizard_messedUpHeaders.csv`); @@ -197,7 +188,7 @@ describe("UploadCsvWizard.spec", () => { cy.get(`.tg-upload-file-list-item-edit`).click(); cy.contains(`Edit your data here.`); cy.get( - `[data-index="4"] [data-test="tgCell_sequence"]:contains(g)` + `[data-index="4"] [data-test="tgCell_sequence"]:contains(g):last` ).click(); cy.focused().type(`{backspace}`); cy.get(`.bp3-disabled:contains(Edit Data)`); @@ -646,13 +637,7 @@ a,,desc,,false,dna,misc_feature cy.contains(".bp3-dialog", `DEscription`); //the matched headers should show up cy.contains(".bp3-dialog", `Description`); //the expected headers should show up cy.contains("Review and Edit Data").click(); - cy.get(`[data-tip="Please enter a value here"]`); - cy.get(`.hasCellError:last [data-test="tgCell_name"]`); - cy.get(`button:contains(Next File).bp3-disabled`); - cy.get(`.hasCellError:last [data-test="tgCell_name"]`).click({ - force: true - }); - cy.focused().type("haha{enter}"); + cy.get(`.hasCellError`).type("haha{enter}"); // cy.get(`.hasCellError:last [data-test="tgCell_name"]`).type("haha{enter}", {force: true}); cy.get(`button:contains(Next File):first`).click(); cy.get( @@ -676,9 +661,7 @@ a,,desc,,false,dna,misc_feature cy.get( `.tg-upload-file-list-item:contains(testUploadWizard_invalidDataNonUnique.csv) .tg-upload-file-list-item-edit` ).click(); - cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ - force: true - }); + cy.get(`[data-index="0"] [data-test="tgCell_sequence"]:last`).click(); cy.focused().type(`tom{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -687,9 +670,7 @@ a,,desc,,false,dna,misc_feature cy.get( `.tg-upload-file-list-item:contains(testUploadWizard_invalidData.csv) .tg-upload-file-list-item-edit` ).click(); - cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ - force: true - }); + cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click(); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.focused().type(`robbin{enter}`, { delay: 100 }); @@ -1065,14 +1046,14 @@ thomas,,g,false,dna,misc_feature`, cy.visit("#/UploadCsvWizard"); cy.tgToggle("allowMultipleFiles"); cy.contains("Build CSV File").click(); - cy.get(`[data-test="tgCell_name"]:first`).click({ force: true }); + cy.get(`.rt-tbody [role="gridcell"]:first`).click(); cy.focused().paste(`Thomas Wee agagag False dna misc_feature`); cy.contains(".bp3-button", "Add File").click(); cy.contains("manual_data_entry.csv"); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.contains("Build CSV File").click(); - cy.get(`[data-test="tgCell_name"]:first`).click({ force: true }); + cy.get(`.rt-tbody [role="gridcell"]:first`).click(); cy.focused().paste(`Thomas Wee agagag False dna misc_feature`); cy.contains(".bp3-button", "Add File").click(); cy.contains("manual_data_entry(1).csv"); @@ -1081,13 +1062,11 @@ thomas,,g,false,dna,misc_feature`, ).click(); cy.contains(`Edit your data here.`); cy.contains(`Add 10 Rows`).click(); - cy.get(`[data-index="4"] [data-test="tgCell_sequence"]`).click({ - force: true - }); + cy.get(`[data-index="4"] [role="gridcell"]`).eq(2).click(); cy.focused().type(`{backspace}`); cy.get(`.bp3-disabled:contains(Edit Data)`).should("not.exist"); cy.focused().type(`tom{enter}`); - cy.get(`[data-index="4"] [data-test="tgCell_name"]`).click({ force: true }); + cy.get(`[data-index="4"] [role="gridcell"]:first`).click(); cy.focused().type(`taoh{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -1096,9 +1075,7 @@ thomas,,g,false,dna,misc_feature`, cy.get( `.tg-upload-file-list-item:contains(manual_data_entry(1).csv) .tg-upload-file-list-item-edit` ).click(); - cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ - force: true - }); + cy.get(`[data-index="0"] [role="gridcell"]`).eq(2).click(); cy.focused().type(`tom{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -1213,23 +1190,14 @@ thomas,,g,false,dna,misc_feature`, cy.contains( `Input your data here. Hover table headers for additional instructions` ); - cy.get(`.rt-td [data-test="tgCell_description"]`) - .eq(1) - .click({ force: true }); - cy.focused().type("description{enter}"); + cy.get(".rt-td").eq(1).type("description{enter}"); //there should be a checkbox in the isRegex boolean column cy.get(`[data-test="Is Regex"] .bp3-checkbox`); //should be able to edit and then drag to continue that edit further down - cy.get( - `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_name"]:first` - ).dblclick({ force: true }); - cy.focused().type("a{enter}"); - cy.get( - `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_sequence"]:first` - ).dblclick({ force: true }); - cy.focused().type("g{enter}"); + cy.get(".rt-td").eq(0).type("a{enter}"); + cy.get(".rt-td").eq(2).type("g{enter}"); cy.contains(".bp3-button", "Add File").click(); cy.contains("File Added"); diff --git a/packages/ui/cypress/e2e/dataTable.spec.js b/packages/ui/cypress/e2e/dataTable.spec.js index dc60bc2d..33f1eef8 100644 --- a/packages/ui/cypress/e2e/dataTable.spec.js +++ b/packages/ui/cypress/e2e/dataTable.spec.js @@ -9,8 +9,7 @@ describe("dataTable.spec", () => { cy.tgToggle("getRowClassName"); cy.get(".rt-tr-group.custom-getRowClassName").should("exist"); }); - //TODO THIS IS BREAKING! - it.skip(`it can select entities across pages`, () => { + it(`it can select entities across pages`, () => { cy.visit("#/DataTable"); cy.contains("0 Selected"); //select first entity @@ -162,7 +161,7 @@ describe("dataTable.spec", () => { checkIndices("greaterThan"); }); - it.skip("page size will persist on reload", () => { + it("page size will persist on reload", () => { cy.visit("#/DataTable"); cy.get(".data-table-container .paging-page-size").should("have.value", "5"); cy.get(".data-table-container .paging-page-size").select("50"); diff --git a/packages/ui/cypress/e2e/upload.spec.js b/packages/ui/cypress/e2e/upload.spec.js index b7174025..9e7ff732 100644 --- a/packages/ui/cypress/e2e/upload.spec.js +++ b/packages/ui/cypress/e2e/upload.spec.js @@ -59,7 +59,7 @@ describe("upload", () => { true ); - cy.contains("type must be .json"); + cy.contains("type must be .zip, .json"); cy.uploadBlobFiles( ".fileUploadLimitAndType.tg-dropzone", [ diff --git a/packages/ui/cypress/support/index.js b/packages/ui/cypress/support/index.js index f8c861a6..f09f7121 100644 --- a/packages/ui/cypress/support/index.js +++ b/packages/ui/cypress/support/index.js @@ -46,9 +46,7 @@ Cypress.Commands.add("dragBetween", (dragSelector, dropSelector) => { getOrWrap(dragSelector) .trigger("mousedown") .trigger("mousemove", 10, 10, { force: true }); - getOrWrap(dropSelector) - .trigger("mousemove", { force: true }) - .trigger("mouseup", { force: true }); + getOrWrap(dropSelector).trigger("mousemove").trigger("mouseup"); }); Cypress.Commands.add( diff --git a/packages/ui/demo/src/examples/UploadCsvWizard.js b/packages/ui/demo/src/examples/UploadCsvWizard.js index 3f68dd88..c76c8fca 100644 --- a/packages/ui/demo/src/examples/UploadCsvWizard.js +++ b/packages/ui/demo/src/examples/UploadCsvWizard.js @@ -6,7 +6,7 @@ import { FileUploadField } from "../../../src"; import DemoWrapper from "../DemoWrapper"; import { reduxForm } from "redux-form"; import { useToggle } from "../useToggle"; -import getIdOrCodeOrIndex from "../../../src/DataTable/utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "../../../src/DataTable/utils"; const simpleValidateAgainst = { fields: [{ path: "name" }, { path: "description" }, { path: "sequence" }] diff --git a/packages/ui/demo/src/examples/UploaderDemo.js b/packages/ui/demo/src/examples/UploaderDemo.js index 8a200429..1770aa6d 100644 --- a/packages/ui/demo/src/examples/UploaderDemo.js +++ b/packages/ui/demo/src/examples/UploaderDemo.js @@ -21,10 +21,6 @@ export default function UploaderDemo() { const [autoUnzip, autoUnzipToggleComp] = useToggle({ type: "autoUnzip" }); - // const [advancedAccept, advancedAcceptToggleComp] = useToggle({ - // type: "accept", - // label: "Toggle Advance Accept" - // }); return (
diff --git a/packages/ui/demo/src/index.js b/packages/ui/demo/src/index.js index adad2335..6e11ad22 100644 --- a/packages/ui/demo/src/index.js +++ b/packages/ui/demo/src/index.js @@ -25,7 +25,6 @@ import ScrollToTopDemo from "./examples/ScrollToTop"; import showAppSpinnerDemo from "./examples/showAppSpinnerDemo"; import EditableCellTable from "./examples/EditableCellTable"; import React from "react"; -import { render } from "react-dom"; import { Provider } from "react-redux"; import store from "./store"; import { FocusStyleManager } from "@blueprintjs/core"; @@ -33,6 +32,7 @@ import AdvancedOptionsDemo from "./examples/AdvancedOptionsDemo"; import FormComponents from "./examples/FormComponents"; import UploadCsvWizard from "./examples/UploadCsvWizard"; import TagSelectDemo from "./examples/TagSelectDemo"; +import { createRoot } from "react-dom/client"; FocusStyleManager.onlyShowFocusOnTabs(); @@ -261,4 +261,5 @@ const Demo = () => { ); }; -render(, document.querySelector("#demo")); +const root = createRoot(document.querySelector("#demo")); +root.render(); diff --git a/packages/ui/src/DataTable/CellDragHandle.js b/packages/ui/src/DataTable/CellDragHandle.js index 154918d7..855d93d2 100644 --- a/packages/ui/src/DataTable/CellDragHandle.js +++ b/packages/ui/src/DataTable/CellDragHandle.js @@ -1,21 +1,20 @@ import { flatMap } from "lodash-es"; import { forEach } from "lodash-es"; import React, { useRef } from "react"; -import ReactDOM from "react-dom"; -export function CellDragHandle({ +export const CellDragHandle = ({ thisTable, onDragEnd, cellId, isSelectionARectangle -}) { +}) => { const xStart = useRef(0); const timeoutkey = useRef(); const rowsToSelect = useRef(); const rectangleCellPaths = useRef(); const handleDrag = useRef(e => { - const table = ReactDOM.findDOMNode(thisTable).querySelector(".rt-table"); + const table = thisTable.querySelector(".rt-table"); const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [rowId, path] = cellId.split(":"); const selectedTr = table.querySelector( @@ -83,7 +82,7 @@ export function CellDragHandle({ const mouseup = useRef(() => { clearTimeout(timeoutkey.current); - const table = ReactDOM.findDOMNode(thisTable); + const table = thisTable; const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [, path] = cellId.split(":"); //remove the dashed borders @@ -126,6 +125,6 @@ export function CellDragHandle({ document.addEventListener("mouseup", mouseup.current, false); }} className="cellDragHandle" - >
+ /> ); -} +}; diff --git a/packages/ui/src/DataTable/EditabelCell.js b/packages/ui/src/DataTable/EditabelCell.js index 176bdb3a..c6453c92 100644 --- a/packages/ui/src/DataTable/EditabelCell.js +++ b/packages/ui/src/DataTable/EditabelCell.js @@ -1,15 +1,31 @@ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; export const EditableCell = ({ - shouldSelectAll, - stopSelectAll, - initialValue, - finishEdit, cancelEdit, + dataTest, + finishEdit, + initialValue, + isEditableCellInitialValue, isNumeric, - dataTest + shouldSelectAll, + stopSelectAll }) => { - const [v, setV] = useState(initialValue); + const [value, setValue] = useState(initialValue); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + if (isEditableCellInitialValue && !isNumeric) { + inputRef.current.selectionStart = inputRef.current.value.length; + inputRef.current.selectionEnd = inputRef.current.value.length; + } else if (shouldSelectAll) { + inputRef.current.select(); + stopSelectAll(); + } + } + }, [isEditableCellInitialValue, isNumeric, shouldSelectAll, stopSelectAll]); + return ( { - if (shouldSelectAll && r) { - r?.select(); - stopSelectAll(); - } - }} + ref={inputRef} {...dataTest} - type={isNumeric ? "number" : undefined} - value={v} autoFocus onKeyDown={e => { if (e.key === "Enter") { - finishEdit(v); + finishEdit(value); e.stopPropagation(); } else if (e.key === "Escape") { e.stopPropagation(); cancelEdit(); } }} - onBlur={() => { - finishEdit(v); - }} - onChange={e => { - setV(e.target.value); - }} + onBlur={() => finishEdit(value)} + onChange={e => setValue(e.target.value)} + type={isNumeric ? "number" : undefined} + value={value} /> ); }; diff --git a/packages/ui/src/DataTable/PagingTool.js b/packages/ui/src/DataTable/PagingTool.js index d13335ec..f422bef5 100644 --- a/packages/ui/src/DataTable/PagingTool.js +++ b/packages/ui/src/DataTable/PagingTool.js @@ -5,7 +5,7 @@ import { noop, get, toInteger } from "lodash-es"; import { Button, Classes } from "@blueprintjs/core"; import { onEnterOrBlurHelper } from "../utils/handlerHelpers"; import { defaultPageSizes } from "./utils/queryParams"; -import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./utils"; function PagingInput({ disabled, onBlur, defaultPage }) { const [page, setPage] = useState(defaultPage); diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index c49f533c..ce5eb73a 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -1,5 +1,4 @@ -import React from "react"; -import ReactDOM from "react-dom"; +import React, { createRef } from "react"; import { invert, toNumber, @@ -27,7 +26,6 @@ import { every } from "lodash-es"; import joinUrl from "url-join"; - import { Button, Menu, @@ -124,6 +122,8 @@ const itemSizeEstimators = { class DataTable extends React.Component { constructor(props) { super(props); + + this.tableRef = createRef(); if (this.props.helperProp) { this.props.helperProp.updateValidationHelper = this.updateValidationHelper; @@ -207,11 +207,41 @@ class DataTable extends React.Component { state = { columns: [], - fullscreen: false + fullscreen: false, + // This state prevents the first letter from not being written, + // when the user starts typing in a cell. It is quite hacky, we should + // refactor this in the future. + editableCellInitialValue: "" }; - static defaultProps = defaultProps; + getPrimarySelectedCellId = () => { + const { reduxFormSelectedCells = {} } = this.props; + for (const k of Object.keys(reduxFormSelectedCells)) { + if (reduxFormSelectedCells[k] === PRIMARY_SELECTED_VAL) { + return k; + } + } + }; + + startCellEdit = (cellId, { shouldSelectAll } = {}) => { + const { + change, + reduxFormSelectedCells = {}, + reduxFormEditingCell + } = computePresets(this.props); + const newSelectedCells = { ...reduxFormSelectedCells }; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + //check if the cell is already selected and editing and if so, don't change it + if (reduxFormEditingCell === cellId) return; + change("reduxFormSelectedCells", newSelectedCells); + change("reduxFormEditingCell", cellId); + if (shouldSelectAll) { + //we should select the text + change("reduxFormEditingCellSelectAll", true); + } + }; + handleEnterStartCellEdit = e => { e.stopPropagation(); this.startCellEdit(this.getPrimarySelectedCellId()); @@ -219,7 +249,7 @@ class DataTable extends React.Component { flashTableBorder = () => { try { - const table = ReactDOM.findDOMNode(this.table); + const table = this.tableRef.current.tableRef; table.classList.add("tgBorderBlue"); setTimeout(() => { table.classList.remove("tgBorderBlue"); @@ -229,6 +259,58 @@ class DataTable extends React.Component { } }; + formatAndValidateEntities = ( + entities, + { useDefaultValues, indexToStartAt } = {} + ) => { + const { schema } = this.props; + const editableFields = schema.fields.filter(f => !f.isNotEditable); + const validationErrors = {}; + + const newEnts = immer(entities, entities => { + entities.forEach((e, index) => { + editableFields.forEach(columnSchema => { + if (useDefaultValues) { + if (e[columnSchema.path] === undefined) { + if (isFunction(columnSchema.defaultValue)) { + e[columnSchema.path] = columnSchema.defaultValue( + index + indexToStartAt, + e + ); + } else e[columnSchema.path] = columnSchema.defaultValue; + } + } + //mutative + const { error } = editCellHelper({ + entity: e, + columnSchema, + newVal: e[columnSchema.path] + }); + if (error) { + const rowId = getIdOrCodeOrIndex(e, index); + validationErrors[`${rowId}:${columnSchema.path}`] = error; + } + }); + }); + }); + return { + newEnts, + validationErrors + }; + }; + + updateValidation = (entities, newCellValidate) => { + const { change, schema } = computePresets(this.props); + const tableWideErr = validateTableWideErrors({ + entities, + schema, + newCellValidate, + props: this.props + }); + change("reduxFormCellValidation", tableWideErr); + this.forceUpdate(); + }; + handleUndo = () => { const { change, @@ -291,7 +373,6 @@ class DataTable extends React.Component { reduxFormExpandedEntityIdMap, change } = newProps; - const table = ReactDOM.findDOMNode(this.table); const idMap = reduxFormSelectedEntityIdMap; @@ -357,7 +438,8 @@ class DataTable extends React.Component { // if not changing selectedIds then we just want to make sure selected entities // stored in redux are in proper format // if selected ids have changed then it will handle redux selection - const tableScrollElement = table.getElementsByClassName("rt-table")[0]; + const tableScrollElement = + this.tableRef.current.tableRef.getElementsByClassName("rt-table")[0]; const { entities: oldEntities = [], reduxFormSelectedEntityIdMap: oldIdMap @@ -406,8 +488,9 @@ class DataTable extends React.Component { const entityIndexToScrollTo = entities.findIndex( e => e.id === idToScrollTo || e.code === idToScrollTo ); - if (entityIndexToScrollTo === -1 || !table) return; - const tableBody = table.querySelector(".rt-tbody"); + if (entityIndexToScrollTo === -1 || !this.tableRef.current) return; + const tableBody = + this.tableRef.current.tableRef.querySelector(".rt-tbody"); if (!tableBody) return; const rowEl = tableBody.getElementsByClassName("rt-tr-group")[entityIndexToScrollTo]; @@ -423,46 +506,6 @@ class DataTable extends React.Component { } }; - formatAndValidateEntities = ( - entities, - { useDefaultValues, indexToStartAt } = {} - ) => { - const { schema } = this.props; - const editableFields = schema.fields.filter(f => !f.isNotEditable); - const validationErrors = {}; - - const newEnts = immer(entities, entities => { - entities.forEach((e, index) => { - editableFields.forEach(columnSchema => { - if (useDefaultValues) { - if (e[columnSchema.path] === undefined) { - if (isFunction(columnSchema.defaultValue)) { - e[columnSchema.path] = columnSchema.defaultValue( - index + indexToStartAt, - e - ); - } else e[columnSchema.path] = columnSchema.defaultValue; - } - } - //mutative - const { error } = editCellHelper({ - entity: e, - columnSchema, - newVal: e[columnSchema.path] - }); - if (error) { - const rowId = getIdOrCodeOrIndex(e, index); - validationErrors[`${rowId}:${columnSchema.path}`] = error; - } - }); - }); - }); - return { - newEnts, - validationErrors - }; - }; - formatAndValidateTableInitial = () => { const { _origEntities, @@ -489,153 +532,26 @@ class DataTable extends React.Component { }); }; - componentDidMount() { - const { - isCellEditable, - entities = [], - isLoading, - showForcedHiddenColumns, - setShowForcedHidden - } = this.props; - isCellEditable && this.formatAndValidateTableInitial(); - this.updateFromProps({}, computePresets(this.props)); - document.addEventListener("paste", this.handlePaste); - - if (!entities.length && !isLoading && !showForcedHiddenColumns) { - setShowForcedHidden(true); - } - // const table = ReactDOM.findDOMNode(this.table); - // let theads = table.getElementsByClassName("rt-thead"); - // let tbody = table.getElementsByClassName("rt-tbody")[0]; - - // tbody.addEventListener("scroll", () => { - // for (let i = 0; i < theads.length; i++) { - // theads.item(i).scrollLeft = tbody.scrollLeft; - // } - // }); - } - - componentDidUpdate(oldProps) { - // const tableBody = table.querySelector(".rt-tbody"); - // const headerNode = table.querySelector(".rt-thead.-header"); - // if (headerNode) headerNode.style.overflowY = "inherit"; - // if (tableBody && tableBody.scrollHeight > tableBody.clientHeight) { - // if (headerNode) { - // headerNode.style.overflowY = "scroll"; - // headerNode.style.overflowX = "hidden"; - // } - // } - - this.updateFromProps(computePresets(oldProps), computePresets(this.props)); - - // comment in to test what is causing re-render - // Object.entries(this.props).forEach( - // ([key, val]) => - // oldProps[key] !== val && console.info(`Prop '${key}' changed`) - // ); - } - - componentWillUnmount() { - document.removeEventListener("paste", this.handlePaste); - } - - handleRowMove = (type, shiftHeld) => e => { - e.preventDefault(); - e.stopPropagation(); - const props = computePresets(this.props); - const { - noSelect, - entities, - reduxFormSelectedEntityIdMap: idMap, - isEntityDisabled, - isSingleSelect - } = props; - let newIdMap = {}; - const lastSelectedEnt = getLastSelectedEntity(idMap); - - if (noSelect) return; - if (lastSelectedEnt) { - let lastSelectedIndex = entities.findIndex( - ent => ent === lastSelectedEnt - ); - if (lastSelectedIndex === -1) { - if (lastSelectedEnt.id !== undefined) { - lastSelectedIndex = entities.findIndex( - ent => ent.id === lastSelectedEnt.id - ); - } else if (lastSelectedEnt.code !== undefined) { - lastSelectedIndex = entities.findIndex( - ent => ent.code === lastSelectedEnt.code - ); - } - } - if (lastSelectedIndex === -1) { - return; - } - const newEntToSelect = getNewEntToSelect({ - type, - lastSelectedIndex, - entities, - isEntityDisabled - }); - - if (!newEntToSelect) return; - if (shiftHeld && !isSingleSelect) { - if (idMap[newEntToSelect.id || newEntToSelect.code]) { - //the entity being moved to has already been selected - newIdMap = omit(idMap, [lastSelectedEnt.id || lastSelectedEnt.code]); - newIdMap[newEntToSelect.id || newEntToSelect.code].time = - Date.now() + 1; - } else { - //the entity being moved to has NOT been selected yet - newIdMap = { - ...idMap, - [newEntToSelect.id || newEntToSelect.code]: { - entity: newEntToSelect, - time: Date.now() - } - }; - } - } else { - //no shiftHeld - newIdMap[newEntToSelect.id || newEntToSelect.code] = { - entity: newEntToSelect, - time: Date.now() - }; + updateEntitiesHelper = (ents, fn) => { + const { change, reduxFormEntitiesUndoRedoStack = { currentVersion: 0 } } = + this.props; + const [nextState, patches, inversePatches] = produceWithPatches(ents, fn); + if (!inversePatches.length) return; + const thatNewNew = [...nextState]; + thatNewNew.isDirty = true; + change("reduxFormEntities", thatNewNew); + change("reduxFormEntitiesUndoRedoStack", { + ...omitBy(reduxFormEntitiesUndoRedoStack, (v, k) => { + return toNumber(k) > reduxFormEntitiesUndoRedoStack.currentVersion + 1; + }), + currentVersion: reduxFormEntitiesUndoRedoStack.currentVersion + 1, + [reduxFormEntitiesUndoRedoStack.currentVersion + 1]: { + inversePatches, + patches } - } - - finalizeSelection({ - idMap: newIdMap, - entities, - props }); }; - handleCopyHotkey = e => { - const { isCellEditable, reduxFormSelectedEntityIdMap } = computePresets( - this.props - ); - - if (isCellEditable) { - this.handleCopySelectedCells(e); - } else { - this.handleCopySelectedRows( - getRecordsFromIdMap(reduxFormSelectedEntityIdMap), - e - ); - } - }; - - getPrimarySelectedCellId = () => { - const { reduxFormSelectedCells = {} } = this.props; - for (const k of Object.keys(reduxFormSelectedCells)) { - if (reduxFormSelectedCells[k] === PRIMARY_SELECTED_VAL) { - return k; - } - } - }; - handlePaste = e => { const { isCellEditable, @@ -790,6 +706,145 @@ class DataTable extends React.Component { } } }; + + componentDidMount() { + const { + isCellEditable, + entities = [], + isLoading, + showForcedHiddenColumns, + setShowForcedHidden + } = this.props; + isCellEditable && this.formatAndValidateTableInitial(); + this.updateFromProps({}, computePresets(this.props)); + document.addEventListener("paste", this.handlePaste); + + if (!entities.length && !isLoading && !showForcedHiddenColumns) { + setShowForcedHidden(true); + } + // const table = this.tableRef.current.tableRef; + // let theads = table.getElementsByClassName("rt-thead"); + // let tbody = table.getElementsByClassName("rt-tbody")[0]; + + // tbody.addEventListener("scroll", () => { + // for (let i = 0; i < theads.length; i++) { + // theads.item(i).scrollLeft = tbody.scrollLeft; + // } + // }); + } + + componentDidUpdate(oldProps) { + // const tableBody = table.querySelector(".rt-tbody"); + // const headerNode = table.querySelector(".rt-thead.-header"); + // if (headerNode) headerNode.style.overflowY = "inherit"; + // if (tableBody && tableBody.scrollHeight > tableBody.clientHeight) { + // if (headerNode) { + // headerNode.style.overflowY = "scroll"; + // headerNode.style.overflowX = "hidden"; + // } + // } + + this.updateFromProps(computePresets(oldProps), computePresets(this.props)); + + // comment in to test what is causing re-render + // Object.entries(this.props).forEach( + // ([key, val]) => + // oldProps[key] !== val && console.info(`Prop '${key}' changed`) + // ); + } + + componentWillUnmount() { + document.removeEventListener("paste", this.handlePaste); + } + + handleRowMove = (type, shiftHeld) => e => { + e.preventDefault(); + e.stopPropagation(); + const props = computePresets(this.props); + const { + noSelect, + entities, + reduxFormSelectedEntityIdMap: idMap, + isEntityDisabled, + isSingleSelect + } = props; + let newIdMap = {}; + const lastSelectedEnt = getLastSelectedEntity(idMap); + + if (noSelect) return; + if (lastSelectedEnt) { + let lastSelectedIndex = entities.findIndex( + ent => ent === lastSelectedEnt + ); + if (lastSelectedIndex === -1) { + if (lastSelectedEnt.id !== undefined) { + lastSelectedIndex = entities.findIndex( + ent => ent.id === lastSelectedEnt.id + ); + } else if (lastSelectedEnt.code !== undefined) { + lastSelectedIndex = entities.findIndex( + ent => ent.code === lastSelectedEnt.code + ); + } + } + if (lastSelectedIndex === -1) { + return; + } + const newEntToSelect = getNewEntToSelect({ + type, + lastSelectedIndex, + entities, + isEntityDisabled + }); + + if (!newEntToSelect) return; + if (shiftHeld && !isSingleSelect) { + if (idMap[newEntToSelect.id || newEntToSelect.code]) { + //the entity being moved to has already been selected + newIdMap = omit(idMap, [lastSelectedEnt.id || lastSelectedEnt.code]); + newIdMap[newEntToSelect.id || newEntToSelect.code].time = + Date.now() + 1; + } else { + //the entity being moved to has NOT been selected yet + newIdMap = { + ...idMap, + [newEntToSelect.id || newEntToSelect.code]: { + entity: newEntToSelect, + time: Date.now() + } + }; + } + } else { + //no shiftHeld + newIdMap[newEntToSelect.id || newEntToSelect.code] = { + entity: newEntToSelect, + time: Date.now() + }; + } + } + + finalizeSelection({ + idMap: newIdMap, + entities, + props + }); + }; + + handleCopyHotkey = e => { + const { isCellEditable, reduxFormSelectedEntityIdMap } = computePresets( + this.props + ); + + if (isCellEditable) { + this.handleCopySelectedCells(e); + } else { + this.handleCopySelectedRows( + getRecordsFromIdMap(reduxFormSelectedEntityIdMap), + e + ); + } + }; + handleSelectAllRows = e => { const { change, @@ -834,18 +889,6 @@ class DataTable extends React.Component { this.updateValidation(entities, reduxFormCellValidation); }; - updateValidation = (entities, newCellValidate) => { - const { change, schema } = computePresets(this.props); - const tableWideErr = validateTableWideErrors({ - entities, - schema, - newCellValidate, - props: this.props - }); - change("reduxFormCellValidation", tableWideErr); - this.forceUpdate(); - }; - handleDeleteCell = () => { const { reduxFormSelectedCells, @@ -886,26 +929,6 @@ class DataTable extends React.Component { this.handleCopyHotkey(e); }; - updateEntitiesHelper = (ents, fn) => { - const { change, reduxFormEntitiesUndoRedoStack = { currentVersion: 0 } } = - this.props; - const [nextState, patches, inversePatches] = produceWithPatches(ents, fn); - if (!inversePatches.length) return; - const thatNewNew = [...nextState]; - thatNewNew.isDirty = true; - change("reduxFormEntities", thatNewNew); - change("reduxFormEntitiesUndoRedoStack", { - ...omitBy(reduxFormEntitiesUndoRedoStack, (v, k) => { - return toNumber(k) > reduxFormEntitiesUndoRedoStack.currentVersion + 1; - }), - currentVersion: reduxFormEntitiesUndoRedoStack.currentVersion + 1, - [reduxFormEntitiesUndoRedoStack.currentVersion + 1]: { - inversePatches, - patches - } - }); - }; - handleCopyTable = (e, opts) => { try { const allRowEls = getAllRows(e); @@ -1438,24 +1461,17 @@ class DataTable extends React.Component { {...(isCellEditable && { tabIndex: -1, onKeyDown: e => { - // const isArrowKey = - // (e.keyCode >= 37 && e.keyCode <= 40) || e.keyCode === 9; - // if (isArrowKey && e.target?.tagName !== "INPUT") { - const isTabKey = e.keyCode === 9; - // const isEnter = e.keyCode === 13; - // console.log(`onKeydown datatable inner`); - // console.log(`isEnter:`, isEnter) - const isArrowKey = e.keyCode >= 37 && e.keyCode <= 40; - // console.log(`e.target?.tagName:`,e.target?.tagName) + const isTabKey = e.key === "Tab"; + const isArrowKey = e.key.startsWith("Arrow"); if ( (isArrowKey && e.target?.tagName !== "INPUT") || isTabKey // || (isEnter && e.target?.tagName === "INPUT") ) { const { schema, entities } = computePresets(this.props); - const left = e.keyCode === 37; - const up = e.keyCode === 38; - const down = e.keyCode === 40 || e.keyCode === 13; + const left = e.key === "ArrowLeft"; + const up = e.key === "ArrowUp"; + const down = e.key === "ArrowDown" || e.key === "Enter"; let cellIdToUse = this.getPrimarySelectedCellId(); const pathToIndex = getFieldPathToIndex(schema); const entityMap = getEntityIdToEntity(entities); @@ -1548,13 +1564,23 @@ class DataTable extends React.Component { const entity = entityIdToEntity[rowId].e; if (!entity) return; const rowDisabled = isEntityDisabled(entity); - const isNum = e.keyCode >= 48 && e.keyCode <= 57; - const isLetter = e.keyCode >= 65 && e.keyCode <= 90; - if (!isNum && !isLetter) return; + const isNum = e.code?.startsWith("Digit"); + const isLetter = e.code?.startsWith("Key"); + if (!isNum && !isLetter) { + this.setState(prev => ({ + ...prev, + editableCellInitialValue: "" + })); + return; + } else { + this.setState(prev => ({ + ...prev, + editableCellInitialValue: e.key + })); + } if (rowDisabled) return; this.startCellEdit(cellId, { shouldSelectAll: true }); e.stopPropagation(); - // e.preventDefault(); } })} > @@ -1694,10 +1720,7 @@ class DataTable extends React.Component { )} { - if (n) this.table = n; - }} - // additionalBodyEl={} + ref={this.tableRef} className={classNames({ isCellEditable, "tg-table-loading": isLoading, @@ -1929,24 +1952,6 @@ class DataTable extends React.Component { }; }; - startCellEdit = (cellId, { shouldSelectAll } = {}) => { - const { - change, - reduxFormSelectedCells = {}, - reduxFormEditingCell - } = computePresets(this.props); - const newSelectedCells = { ...reduxFormSelectedCells }; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - //check if the cell is already selected and editing and if so, don't change it - if (reduxFormEditingCell === cellId) return; - change("reduxFormSelectedCells", newSelectedCells); - change("reduxFormEditingCell", cellId); - if (shouldSelectAll) { - //we should select the text - change("reduxFormEditingCellSelectAll", true); - } - }; - getTableCellProps = (state, rowInfo, column) => { const { entities, @@ -2268,7 +2273,7 @@ class DataTable extends React.Component { refocusTable = () => { setTimeout(() => { - const table = ReactDOM.findDOMNode(this.table)?.closest( + const table = this.tableRef.current?.tableRef?.closest( ".data-table-container>div" ); table?.focus(); @@ -2545,7 +2550,6 @@ class DataTable extends React.Component { { const checked = e.target.checked; @@ -2555,9 +2559,6 @@ class DataTable extends React.Component { ); noEllipsis = true; } else { - // if (column.type === "genericSelect") { - // val = - // } if (reduxFormEditingCell === cellId) { if (column.type === "genericSelect") { const GenericSelectComp = column.GenericSelectComp; @@ -2600,7 +2601,14 @@ class DataTable extends React.Component { shouldSelectAll={reduxFormEditingCellSelectAll} cancelEdit={this.cancelCellEdit} isNumeric={column.type === "number"} - initialValue={text} + initialValue={ + this.state.editableCellInitialValue.length + ? this.state.editableCellInitialValue + : text + } + isEditableCellInitialValue={ + !!this.state.editableCellInitialValue.length + } finishEdit={newVal => { this.finishCellEdit(cellId, newVal); }} @@ -2687,7 +2695,7 @@ class DataTable extends React.Component { : isSelectedCell === PRIMARY_SELECTED_VAL) && ( { this.insertRows({ above: true }); }} - > + /> { this.insertRows({}); }} - > + /> 1 ? "s" : ""}`} @@ -3393,7 +3401,7 @@ class DataTable extends React.Component { }} indeterminate={isIndeterminate} checked={isChecked} - > + /> ); } diff --git a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js index bcbf17c1..2a210198 100644 --- a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js +++ b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js @@ -1,4 +1,4 @@ -export default (record, rowIndex) => { +export const getIdOrCodeOrIndex = (record, rowIndex) => { if (record.id || record.id === 0) { return record.id; } else if (record.code) { diff --git a/packages/ui/src/DataTable/utils/getLastSelectedEntity.js b/packages/ui/src/DataTable/utils/getLastSelectedEntity.js index eb09a2b6..ecfbae5b 100644 --- a/packages/ui/src/DataTable/utils/getLastSelectedEntity.js +++ b/packages/ui/src/DataTable/utils/getLastSelectedEntity.js @@ -1,7 +1,7 @@ export const getLastSelectedEntity = idMap => { let lastSelectedEnt; let latestTime; - idMap.forEach(({ time, entity }) => { + Object.values(idMap).forEach(({ time, entity }) => { if (!latestTime || time > latestTime) { lastSelectedEnt = entity; latestTime = time; diff --git a/packages/ui/src/DataTable/utils/getRowCopyText.js b/packages/ui/src/DataTable/utils/getRowCopyText.js index 88a389a5..c706bd63 100644 --- a/packages/ui/src/DataTable/utils/getRowCopyText.js +++ b/packages/ui/src/DataTable/utils/getRowCopyText.js @@ -7,22 +7,22 @@ export const getRowCopyText = (rowEl, { specificColumn } = {}) => { const textContent = []; const jsonText = []; - rowEl.children.forEach(cellEl => { + for (const cellEl of rowEl.children) { const cellChild = cellEl.querySelector(`[data-copy-text]`); if (!cellChild) { - if (specificColumn) return []; //strip it - return; //just leave it blank + if (specificColumn) continue; //strip it + continue; //just leave it blank } if ( specificColumn && cellChild.getAttribute("data-test") !== specificColumn ) { - return []; + continue; } const [t, j] = getCellCopyText(cellChild); textContent.push(t); jsonText.push(j); - }); + } return [flatMap(textContent).join("\t"), jsonText]; }; diff --git a/packages/ui/src/DataTable/utils/handleCopyColumn.js b/packages/ui/src/DataTable/utils/handleCopyColumn.js index d306053e..4aa04870 100644 --- a/packages/ui/src/DataTable/utils/handleCopyColumn.js +++ b/packages/ui/src/DataTable/utils/handleCopyColumn.js @@ -1,5 +1,5 @@ import { getAllRows } from "./getAllRows"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import { handleCopyRows } from "./handleCopyRows"; export const handleCopyColumn = (e, cellWrapper, selectedRecords) => { diff --git a/packages/ui/src/DataTable/utils/index.js b/packages/ui/src/DataTable/utils/index.js index 48307406..2d85f6cb 100644 --- a/packages/ui/src/DataTable/utils/index.js +++ b/packages/ui/src/DataTable/utils/index.js @@ -1,7 +1,7 @@ import { isEntityClean } from "./isEntityClean"; import { getSelectedRowsFromEntities } from "./selection"; import { removeCleanRows } from "./removeCleanRows"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import computePresets from "./computePresets"; import { getRecordsFromIdMap } from "./withSelectedEntities"; import { formatPasteData } from "./formatPasteData"; diff --git a/packages/ui/src/DataTable/utils/isEntityClean.js b/packages/ui/src/DataTable/utils/isEntityClean.js index acf34e37..f9d507d4 100644 --- a/packages/ui/src/DataTable/utils/isEntityClean.js +++ b/packages/ui/src/DataTable/utils/isEntityClean.js @@ -1,13 +1,15 @@ export function isEntityClean(e) { + if (typeof e !== "object" || e === null) { + return true; // or return false depending on what you want for non-object inputs + } let isClean = true; - e.some((val, key) => { - if (key === "id") return false; - if (key === "_isClean") return false; + for (const [key, val] of Object.entries(e)) { + if (key === "id") continue; + if (key === "_isClean") continue; if (val) { isClean = false; - return true; + break; } - return false; - }); + } return isClean; } diff --git a/packages/ui/src/DataTable/utils/removeCleanRows.js b/packages/ui/src/DataTable/utils/removeCleanRows.js index b95f5552..5958166a 100644 --- a/packages/ui/src/DataTable/utils/removeCleanRows.js +++ b/packages/ui/src/DataTable/utils/removeCleanRows.js @@ -1,7 +1,7 @@ import { isEntityClean } from "./isEntityClean"; import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; -export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { +export const removeCleanRows = (reduxFormEntities, reduxFormCellValidation) => { const toFilterOut = {}; const entsToUse = (reduxFormEntities || []).filter(e => { if (!(e._isClean || isEntityClean(e))) return true; @@ -12,11 +12,11 @@ export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { }); const validationToUse = {}; - reduxFormCellValidation.forEach((v, k) => { + Object.entries(reduxFormCellValidation || {}).forEach(([k, v]) => { const [rowId] = k.split(":"); if (!toFilterOut[rowId]) { validationToUse[k] = v; } }); return { entsToUse, validationToUse }; -} +}; diff --git a/packages/ui/src/DataTable/utils/rowClick.js b/packages/ui/src/DataTable/utils/rowClick.js index 8c4c5570..e4ee123a 100644 --- a/packages/ui/src/DataTable/utils/rowClick.js +++ b/packages/ui/src/DataTable/utils/rowClick.js @@ -1,6 +1,6 @@ import { isEmpty, forEach, range } from "lodash-es"; import { getSelectedRowsFromEntities } from "./selection"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import { getRecordsFromIdMap } from "./withSelectedEntities"; export default function rowClick(e, rowInfo, entities, props) { diff --git a/packages/ui/src/DataTable/utils/selection.js b/packages/ui/src/DataTable/utils/selection.js index ea4a08d8..39757606 100644 --- a/packages/ui/src/DataTable/utils/selection.js +++ b/packages/ui/src/DataTable/utils/selection.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; export const getSelectedRowsFromEntities = (entities, idMap) => { if (!idMap) return []; diff --git a/packages/ui/src/DataTable/utils/utils.js b/packages/ui/src/DataTable/utils/utils.js index b15021e3..fe7b4d1c 100644 --- a/packages/ui/src/DataTable/utils/utils.js +++ b/packages/ui/src/DataTable/utils/utils.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; export const getFieldPathToIndex = schema => { const fieldToIndex = {}; diff --git a/packages/ui/src/DataTable/validateTableWideErrors.js b/packages/ui/src/DataTable/validateTableWideErrors.js index 11c2eb93..c29a36b2 100644 --- a/packages/ui/src/DataTable/validateTableWideErrors.js +++ b/packages/ui/src/DataTable/validateTableWideErrors.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./utils"; import { getCellVal } from "./getCellVal"; import { forEach, isArray } from "lodash-es"; import { startCase } from "lodash-es"; diff --git a/packages/ui/src/FillWindow.js b/packages/ui/src/FillWindow.js index ab25adc5..c333c342 100644 --- a/packages/ui/src/FillWindow.js +++ b/packages/ui/src/FillWindow.js @@ -1,6 +1,5 @@ -import React from "react"; +import React, { createPortal } from "react"; import { isFunction } from "lodash-es"; -import reactDom from "react-dom"; import rerenderOnWindowResize from "./rerenderOnWindowResize"; import "./FillWindow.css"; @@ -63,7 +62,7 @@ export default class FillWindow extends React.Component { : this.props.children}
); - if (asPortal) return reactDom.createPortal(inner, window.document.body); + if (asPortal) return createPortal(inner, window.document.body); return inner; } } diff --git a/packages/ui/src/FormComponents/Uploader.js b/packages/ui/src/FormComponents/Uploader.js index e6c08cfa..6b0a9284 100644 --- a/packages/ui/src/FormComponents/Uploader.js +++ b/packages/ui/src/FormComponents/Uploader.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button, Callout, @@ -16,7 +16,6 @@ import classnames from "classnames"; import { nanoid } from "nanoid"; import papaparse, { unparse } from "papaparse"; import downloadjs from "downloadjs"; -import { configure, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import UploadCsvWizardDialog, { SimpleInsertDataDialog @@ -30,7 +29,7 @@ import { removeExt } from "@teselagen/file-utils"; import tryToMatchSchemas from "./tryToMatchSchemas"; -import { forEach, isArray, isFunction, isPlainObject, noop } from "lodash-es"; +import { isArray, isFunction, isPlainObject, noop } from "lodash-es"; import { flatMap } from "lodash-es"; import urljoin from "url-join"; import popoverOverflowModifiers from "../utils/popoverOverflowModifiers"; @@ -38,14 +37,12 @@ import writeXlsxFile from "write-excel-file"; import { startCase } from "lodash-es"; import { getNewName } from "./getNewName"; import { isObject } from "lodash-es"; -import { connect } from "react-redux"; +import { useDispatch } from "react-redux"; import { initialize } from "redux-form"; import classNames from "classnames"; -import { compose } from "recompose"; import convertSchema from "../DataTable/utils/convertSchema"; import { LoadingDots } from "./LoadingDots"; -configure({ isolateGlobalState: true }); const helperText = [ `How to Use This Template to Upload New Data`, `1. Go to the first tab and delete the example data.`, @@ -64,58 +61,106 @@ const helperSchema = [ } ]; -class ValidateAgainstSchema { - fields = []; - - constructor() { - makeObservable(this, { - fields: observable.shallow - }); - } - - setValidateAgainstSchema(newValidateAgainstSchema) { - if (!newValidateAgainstSchema) { - this.fields = []; - return; - } - const schema = convertSchema(newValidateAgainstSchema); - if ( - schema.fields.some(f => { - if (f.path === "id") { - return true; - } - return false; - }) - ) { - throw new Error( - `Uploader was passed a validateAgainstSchema with a fields array that contains a field with a path of "id". This is not allowed.` - ); - } - forEach(schema, (v, k) => { - this[k] = v; - }); +const setValidateAgainstSchema = newValidateAgainstSchema => { + if (!newValidateAgainstSchema) return { fields: [] }; + const schema = convertSchema(newValidateAgainstSchema); + if ( + schema.fields.some(f => { + if (f.path === "id") { + return true; + } + return false; + }) + ) { + throw new Error( + `Uploader was passed a validateAgainstSchema with a fields array that contains a field with a path of "id". This is not allowed.` + ); } -} - -// autorun(() => { -// console.log( -// `validateAgainstSchemaStore?.fields:`, -// JSON.stringify(validateAgainstSchemaStore?.fields, null, 4) -// ); -// }); -// validateAgainstSchemaStore.fields = ["hahah"]; -// validateAgainstSchemaStore.fields.push("yaa"); + return schema; +}; -// const validateAgainstSchema = observable.shallow({ -// fields: [] -// }) - -// validateAgainstSchema.fields = ["hahah"]; +const InnerDropZone = ({ + getRootProps, + getInputProps, + isDragAccept, + isDragReject, + isDragActive, + className, + minimal, + dropzoneDisabled, + contentOverride, + simpleAccept, + innerIcon, + innerText, + validateAgainstSchema, + handleManuallyEnterData, + noBuildCsvOption, + showFilesCount, + fileList + // isDragActive + // isDragReject + // isDragAccept +}) => ( +
+
+ + {contentOverride || ( +
+ {innerIcon || } + {innerText || (minimal ? "Upload" : "Click or drag to upload")} + {validateAgainstSchema && !noBuildCsvOption && ( +
+ ...or {manualEnterMessage} + {/*
+ {manualEnterSubMessage} +
*/} +
+ )} +
+ )} +
-// wink wink -const emptyPromise = Promise.resolve.bind(Promise); + {showFilesCount ? ( +
+ Files: {fileList ? fileList.length : 0} +
+ ) : null} +
+); -function UploaderInner({ +const UploaderInner = ({ accept: __accept, contentOverride: maybeContentOverride, innerIcon, @@ -130,7 +175,9 @@ function UploaderInner({ showUploadList = true, beforeUpload, fileList, //list of files with options: {name, loading, error, url, originalName, downloadName} - onFileSuccess = emptyPromise, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! + onFileSuccess = async () => { + return; + }, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! onFieldSubmit = noop, //called when all files have successfully uploaded // fileFinished = noop, onRemove = noop, //called when a file has been selected to be removed @@ -141,22 +188,27 @@ function UploaderInner({ autoUnzip, disabled: _disabled, noBuildCsvOption, - initializeForm, showFilesCount, threeDotMenuItems, onPreviewClick -}) { +}) => { + const dispatch = useDispatch(); let dropzoneDisabled = _disabled; let _accept = __accept; - const validateAgainstSchemaStore = useRef(new ValidateAgainstSchema()); const [acceptLoading, setAcceptLoading] = useState(); const [resolvedAccept, setResolvedAccept] = useState(); + if (resolvedAccept) { _accept = resolvedAccept; } - const isAcceptPromise = - __accept?.then || - (Array.isArray(__accept) ? __accept.some(a => a?.then) : false); + + const isAcceptPromise = useMemo( + () => + __accept?.then || + (Array.isArray(__accept) ? __accept.some(a => a?.then) : false), + [__accept] + ); + useEffect(() => { if (isAcceptPromise) { setAcceptLoading(true); @@ -169,35 +221,36 @@ function UploaderInner({ ); } }, [__accept, isAcceptPromise]); + if (isAcceptPromise && !resolvedAccept) { _accept = []; } + if (acceptLoading) dropzoneDisabled = true; - const accept = !_accept - ? undefined - : isAcceptPromise && !resolvedAccept - ? [] - : isPlainObject(_accept) - ? [_accept] - : isArray(_accept) - ? _accept - : _accept.split(",").map(a => ({ type: a })); - const callout = _callout || accept?.find?.(a => a?.callout)?.callout; + const accept = useMemo( + () => + !_accept + ? undefined + : isAcceptPromise && !resolvedAccept + ? [] + : isPlainObject(_accept) + ? [_accept] + : isArray(_accept) + ? _accept + : _accept.split(",").map(a => ({ type: a })), + [_accept, isAcceptPromise, resolvedAccept] + ); - const validateAgainstSchemaToUse = - _validateAgainstSchema || - accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema; + const callout = _callout || accept?.find?.(a => a?.callout)?.callout; - useEffect(() => { - // validateAgainstSchema - validateAgainstSchemaStore.current.setValidateAgainstSchema( - validateAgainstSchemaToUse - ); - }, [validateAgainstSchemaToUse]); - let validateAgainstSchema; - if (validateAgainstSchemaToUse) { - validateAgainstSchema = validateAgainstSchemaStore.current; - } + const validateAgainstSchema = useMemo( + () => + setValidateAgainstSchema( + _validateAgainstSchema || + accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema + ), + [_validateAgainstSchema, accept] + ); if ( (validateAgainstSchema || autoUnzip) && @@ -215,6 +268,7 @@ function UploaderInner({ const { showDialogPromise: showUploadCsvWizardDialog, comp } = useDialog({ ModalComponent: UploadCsvWizardDialog }); + const { showDialogPromise: showSimpleInsertDataDialog, comp: comp2 } = useDialog({ ModalComponent: SimpleInsertDataDialog @@ -553,7 +607,7 @@ function UploaderInner({ } {...getFileDownloadAttr(exampleFile)} key={i} - >
+ /> ); } )} @@ -619,7 +673,7 @@ function UploaderInner({ }} size={10} icon="download" - > + /> )} @@ -631,7 +685,8 @@ function UploaderInner({ // make the dots below "load" <> - Accept Loading + Accept Loading + ) : ( <>Accepts {simpleAccept} @@ -650,135 +705,135 @@ function UploaderInner({ .join(", ") : undefined } - {...{ - onDrop: async (_acceptedFiles, rejectedFiles) => { - let acceptedFiles = []; - for (const file of _acceptedFiles) { - if ((validateAgainstSchema || autoUnzip) && isZipFile(file)) { - const files = await filterFilesInZip( - file, - simpleAccept - ?.split(", ") - ?.map(a => (a.startsWith(".") ? a : "." + a)) || [] - ); - acceptedFiles.push(...files.map(f => f.originFileObj)); - } else { - acceptedFiles.push(file); - } - } - cleanupFiles(); - if (rejectedFiles.length) { - let msg = ""; - rejectedFiles.forEach(file => { - if (msg) msg += "\n"; - msg += - `${file.file.name}: ` + - file.errors.map(err => err.message).join(", "); - }); - window.toastr && - window.toastr.warning( -
{msg}
- ); + onDrop={async (_acceptedFiles, rejectedFiles) => { + let acceptedFiles = []; + for (const file of _acceptedFiles) { + if ((validateAgainstSchema || autoUnzip) && isZipFile(file)) { + const files = await filterFilesInZip( + file, + simpleAccept + ?.split(", ") + ?.map(a => (a.startsWith(".") ? a : "." + a)) || [] + ); + acceptedFiles.push(...files.map(f => f.originFileObj)); + } else { + acceptedFiles.push(file); } - if (!acceptedFiles.length) return; - setLoading(true); - acceptedFiles = trimFiles(acceptedFiles, fileLimit); - - acceptedFiles.forEach(file => { - file.preview = URL.createObjectURL(file); - file.loading = true; - if (!file.id) { - file.id = nanoid(); - } - filesToClean.current.push(file); + } + cleanupFiles(); + if (rejectedFiles.length) { + let msg = ""; + rejectedFiles.forEach(file => { + if (msg) msg += "\n"; + msg += + `${file.file.name}: ` + + file.errors.map(err => err.message).join(", "); }); - - if (readBeforeUpload) { - acceptedFiles = await Promise.all( - acceptedFiles.map(file => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsText(file, "UTF-8"); - reader.onload = evt => { - file.parsedString = evt.target.result; - resolve(file); - }; - reader.onerror = err => { - console.error("err:", err); - reject(err); - }; - }); - }) + window.toastr && + window.toastr.warning( +
{msg}
); + } + if (!acceptedFiles.length) return; + setLoading(true); + acceptedFiles = trimFiles(acceptedFiles, fileLimit); + + acceptedFiles.forEach(file => { + file.preview = URL.createObjectURL(file); + file.loading = true; + if (!file.id) { + file.id = nanoid(); } - const cleanedAccepted = acceptedFiles.map(file => { - return { - originFileObj: file, - originalFileObj: file, - id: file.id, - lastModified: file.lastModified, - lastModifiedDate: file.lastModifiedDate, - loading: file.loading, - name: file.name, - preview: file.preview, - size: file.size, - type: file.type, - ...(file.parsedString - ? { parsedString: file.parsedString } - : {}) - }; - }); + filesToClean.current.push(file); + }); - const toKeep = []; - if (validateAgainstSchema) { - const filesWIssues = []; - const filesWOIssues = []; - for (const [i, file] of cleanedAccepted.entries()) { - if (isCsvOrExcelFile(file)) { - let parsedF; - try { - parsedF = await parseCsvOrExcelFile(file, { - csvParserOptions: isFunction( - validateAgainstSchema.csvParserOptions - ) - ? validateAgainstSchema.csvParserOptions({ - validateAgainstSchema - }) - : validateAgainstSchema.csvParserOptions - }); - } catch (error) { - console.error("error:", error); - window.toastr && - window.toastr.error( - `There was an error parsing your file. Please try again. ${ - error.message || error - }` - ); - return; - } + if (readBeforeUpload) { + acceptedFiles = await Promise.all( + acceptedFiles.map(file => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = evt => { + file.parsedString = evt.target.result; + resolve(file); + }; + reader.onerror = err => { + console.error("err:", err); + reject(err); + }; + }); + }) + ); + } + const cleanedAccepted = acceptedFiles.map(file => { + return { + originFileObj: file, + originalFileObj: file, + id: file.id, + lastModified: file.lastModified, + lastModifiedDate: file.lastModifiedDate, + loading: file.loading, + name: file.name, + preview: file.preview, + size: file.size, + type: file.type, + ...(file.parsedString + ? { parsedString: file.parsedString } + : {}) + }; + }); - const { - csvValidationIssue: _csvValidationIssue, - matchedHeaders, - userSchema, - searchResults, - ignoredHeadersMsg - } = await tryToMatchSchemas({ - incomingData: parsedF.data, - validateAgainstSchema + const toKeep = []; + if (validateAgainstSchema) { + const filesWIssues = []; + const filesWOIssues = []; + for (const [i, file] of cleanedAccepted.entries()) { + if (isCsvOrExcelFile(file)) { + let parsedF; + try { + parsedF = await parseCsvOrExcelFile(file, { + csvParserOptions: isFunction( + validateAgainstSchema.csvParserOptions + ) + ? validateAgainstSchema.csvParserOptions({ + validateAgainstSchema + }) + : validateAgainstSchema.csvParserOptions }); - if (userSchema?.userData?.length === 0) { - console.error( - `userSchema, parsedF.data:`, - userSchema, - parsedF.data + } catch (error) { + console.error("error:", error); + window.toastr && + window.toastr.error( + `There was an error parsing your file. Please try again. ${ + error.message || error + }` ); - } else { - toKeep.push(file); - let csvValidationIssue = _csvValidationIssue; - if (csvValidationIssue) { - if (isObject(csvValidationIssue)) { - initializeForm( + return; + } + + const { + csvValidationIssue: _csvValidationIssue, + matchedHeaders, + userSchema, + searchResults, + ignoredHeadersMsg + } = await tryToMatchSchemas({ + incomingData: parsedF.data, + validateAgainstSchema + }); + if (userSchema?.userData?.length === 0) { + console.error( + `userSchema, parsedF.data:`, + userSchema, + parsedF.data + ); + } else { + toKeep.push(file); + let csvValidationIssue = _csvValidationIssue; + if (csvValidationIssue) { + if (isObject(csvValidationIssue)) { + dispatch( + initialize( `editableCellTable${ cleanedAccepted.length > 1 ? `-${i}` : "" }`, @@ -790,149 +845,142 @@ function UploaderInner({ keepValues: true, updateUnregisteredFields: true } + ) + ); + const err = Object.values(csvValidationIssue)[0]; + // csvValidationIssue = `It looks like there was an error with your data - \n\n${ + // err && err.message ? err.message : err + // }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string + const errMsg = err && err.message ? err.message : err; + if (isPlainObject(errMsg)) { + throw new Error( + `errMsg is an object ${JSON.stringify( + errMsg, + null, + 4 + )}` ); - const err = Object.values(csvValidationIssue)[0]; - // csvValidationIssue = `It looks like there was an error with your data - \n\n${ - // err && err.message ? err.message : err - // }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string - const errMsg = - err && err.message ? err.message : err; - if (isPlainObject(errMsg)) { - throw new Error( - `errMsg is an object ${JSON.stringify( - errMsg, - null, - 4 - )}` - ); - } - csvValidationIssue = ( + } + csvValidationIssue = ( +
-
- It looks like there was an error with your - data (Correct on the Review Data page): -
-
{errMsg}
-
- Please review your headers and then correct - any errors on the next page. -
+ It looks like there was an error with your data + (Correct on the Review Data page):
- ); - } - filesWIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults - }); - } else { - filesWOIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults - }); - const newFileName = removeExt(file.name) + `.csv`; - - const { newFile, cleanedEntities } = getNewCsvFile( - userSchema.userData, - newFileName +
{errMsg}
+
+ Please review your headers and then correct any + errors on the next page. +
+
); - - file.meta = parsedF.meta; - file.hasEditClick = true; - file.parsedData = cleanedEntities; - file.name = newFileName; - file.originFileObj = newFile; - file.originalFileObj = newFile; } - } - } else { - toKeep.push(file); - } - } - if (filesWIssues.length) { - const { file } = filesWIssues[0]; - const allFiles = [...filesWIssues, ...filesWOIssues]; - const doAllFilesHaveSameHeaders = allFiles.every(f => { - if (f.userSchema.fields && f.userSchema.fields.length) { - return f.userSchema.fields.every((h, i) => { - return ( - h.path === allFiles[0].userSchema.fields[i].path - ); + filesWIssues.push({ + file, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + userSchema, + searchResults }); - } - return false; - }); - const multipleFiles = allFiles.length > 1; - const { res } = await showUploadCsvWizardDialog( - "onUploadWizardFinish", - { - dialogProps: { - title: `Fix Up File${multipleFiles ? "s" : ""} ${ - multipleFiles - ? "" - : file.name - ? `"${file.name}"` - : "" - }` - }, - doAllFilesHaveSameHeaders, - filesWIssues: allFiles, - validateAgainstSchema - } - ); + } else { + filesWOIssues.push({ + file, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + userSchema, + searchResults + }); + const newFileName = removeExt(file.name) + `.csv`; - if (!res) { - window.toastr.warning(`File Upload Aborted`); - return; - } else { - allFiles.forEach(({ file }, i) => { - const newEntities = res[i]; - // const newFileName = removeExt(file.name) + `_updated.csv`; - //swap out file with a new csv file const { newFile, cleanedEntities } = getNewCsvFile( - newEntities, - file.name + userSchema.userData, + newFileName ); + file.meta = parsedF.meta; file.hasEditClick = true; file.parsedData = cleanedEntities; - // file.name = newFileName; + file.name = newFileName; file.originFileObj = newFile; file.originalFileObj = newFile; - }); - setTimeout(() => { - //inside a timeout for cypress purposes - window.toastr.success( - `Added Fixed Up File${ - allFiles.length > 1 ? "s" : "" - } ${allFiles.map(({ file }) => file.name).join(", ")}` - ); - }, 200); + } } + } else { + toKeep.push(file); } - } else { - toKeep.push(...cleanedAccepted); } + if (filesWIssues.length) { + const { file } = filesWIssues[0]; + const allFiles = [...filesWIssues, ...filesWOIssues]; + const doAllFilesHaveSameHeaders = allFiles.every(f => { + if (f.userSchema.fields && f.userSchema.fields.length) { + return f.userSchema.fields.every((h, i) => { + return h.path === allFiles[0].userSchema.fields[i].path; + }); + } + return false; + }); + const multipleFiles = allFiles.length > 1; + const { res } = await showUploadCsvWizardDialog( + "onUploadWizardFinish", + { + dialogProps: { + title: `Fix Up File${multipleFiles ? "s" : ""} ${ + multipleFiles ? "" : file.name ? `"${file.name}"` : "" + }` + }, + doAllFilesHaveSameHeaders, + filesWIssues: allFiles, + validateAgainstSchema + } + ); + + if (!res) { + window.toastr.warning(`File Upload Aborted`); + return; + } else { + allFiles.forEach(({ file }, i) => { + const newEntities = res[i]; + // const newFileName = removeExt(file.name) + `_updated.csv`; + //swap out file with a new csv file + const { newFile, cleanedEntities } = getNewCsvFile( + newEntities, + file.name + ); - if (toKeep.length === 0) { - window.toastr && - window.toastr.error( - `It looks like there wasn't any data in your file. Please add some data and try again` - ); + file.hasEditClick = true; + file.parsedData = cleanedEntities; + // file.name = newFileName; + file.originFileObj = newFile; + file.originalFileObj = newFile; + }); + setTimeout(() => { + //inside a timeout for cypress purposes + window.toastr.success( + `Added Fixed Up File${ + allFiles.length > 1 ? "s" : "" + } ${allFiles.map(({ file }) => file.name).join(", ")}` + ); + }, 200); + } } - const cleanedFileList = trimFiles( - [...toKeep, ...fileListToUse], - fileLimit - ); - handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList }); + } else { + toKeep.push(...cleanedAccepted); } + + if (toKeep.length === 0) { + window.toastr && + window.toastr.error( + `It looks like there wasn't any data in your file. Please add some data and try again` + ); + } + const cleanedFileList = trimFiles( + [...toKeep, ...fileListToUse], + fileLimit + ); + handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList }); }} {...dropzoneProps} > @@ -942,71 +990,26 @@ function UploaderInner({ isDragAccept, isDragReject, isDragActive - // isDragActive - // isDragReject - // isDragAccept }) => ( -
-
- - {contentOverride || ( -
- {innerIcon || ( - - )} - {innerText || - (minimal ? "Upload" : "Click or drag to upload")} - {validateAgainstSchema && !noBuildCsvOption && ( -
- ...or {manualEnterMessage} - {/*
- {manualEnterSubMessage} -
*/} -
- )} -
- )} -
- - {showFilesCount ? ( -
- Files: {fileList ? fileList.length : 0} -
- ) : null} -
+ )} {/* {validateAgainstSchema && } */} @@ -1188,12 +1191,9 @@ function UploaderInner({ ); -} +}; -const Uploader = compose( - connect(undefined, { initializeForm: initialize }), - observer -)(UploaderInner); +const Uploader = observer(UploaderInner); export default Uploader; diff --git a/packages/ui/src/FormComponents/tryToMatchSchemas.js b/packages/ui/src/FormComponents/tryToMatchSchemas.js index 46887e7f..0d167c0a 100644 --- a/packages/ui/src/FormComponents/tryToMatchSchemas.js +++ b/packages/ui/src/FormComponents/tryToMatchSchemas.js @@ -14,12 +14,6 @@ const getSchema = data => ({ return { path, type: "string" }; }), userData: data - // userData: data.map((d) => { - // if (!d.id) { - // d.id = nanoid(); - // } - // return d - // }) }); export default async function tryToMatchSchemas({ incomingData, diff --git a/packages/ui/src/TgSelect/index.js b/packages/ui/src/TgSelect/index.js index 34728a1e..b6403e9d 100644 --- a/packages/ui/src/TgSelect/index.js +++ b/packages/ui/src/TgSelect/index.js @@ -512,7 +512,6 @@ export const itemListPredicate = (_queryString = "", items, isSimpleSearch) => { export function simplesearch(needle, haystack) { return (haystack || "").indexOf(needle) !== -1; } - function tagOptionRender(vals) { if (vals.noTagStyle) return vals.label; return ; diff --git a/packages/ui/src/UploadCsvWizard.js b/packages/ui/src/UploadCsvWizard.js index 92595324..bf9a7918 100644 --- a/packages/ui/src/UploadCsvWizard.js +++ b/packages/ui/src/UploadCsvWizard.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { reduxForm, change, formValueSelector, destroy } from "redux-form"; import { Callout, Icon, Intent, Tab, Tabs } from "@blueprintjs/core"; import immer from "immer"; @@ -12,10 +12,11 @@ import { tgFormValueSelector } from "./utils/tgFormValues"; import { some } from "lodash-es"; import { times } from "lodash-es"; import DialogFooter from "./DialogFooter"; -import DataTable, { removeCleanRows } from "./DataTable"; +import DataTable from "./DataTable"; +import { removeCleanRows } from "./DataTable/utils"; import wrapDialog from "./wrapDialog"; import { omit } from "lodash-es"; -import { connect } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { MatchHeaders } from "./MatchHeaders"; import { isEmpty } from "lodash-es"; import { addSpecialPropToAsyncErrs } from "./FormComponents/tryToMatchSchemas"; @@ -35,64 +36,62 @@ const UploadCsvWizardDialog = compose( reduxForm({ form: "UploadCsvWizardDialog" }), - connect( - (state, props) => { - if (props.filesWIssues.length > 0) { - const reduxFormEntitiesArray = []; - const finishedFiles = props.filesWIssues.map((f, i) => { - const { reduxFormEntities, reduxFormCellValidation } = - formValueSelector(`editableCellTable-${i}`)( - state, - "reduxFormEntities", - "reduxFormCellValidation" - ); - reduxFormEntitiesArray.push(reduxFormEntities); - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation - ); - return ( - entsToUse && - entsToUse.length && - !some(validationToUse, v => v) && - entsToUse - ); - }); - return { - reduxFormEntitiesArray, - finishedFiles - }; - } - }, - { changeForm: change, destroyForms: destroy } - ), observer )(function UploadCsvWizardDialogOuter({ - validateAgainstSchema, - reduxFormEntitiesArray, - filesWIssues: _filesWIssues, - finishedFiles, - onUploadWizardFinish, - doAllFilesHaveSameHeaders, - destroyForms, csvValidationIssue, + doAllFilesHaveSameHeaders, + filesWIssues: _filesWIssues, + flippedMatchedHeaders, ignoredHeadersMsg, - searchResults, matchedHeaders, + onUploadWizardFinish, + searchResults, userSchema, - flippedMatchedHeaders, - changeForm + validateAgainstSchema }) { + const dispatch = useDispatch(); // will unmount state hook - React.useEffect(() => { + useEffect(() => { return () => { - destroyForms( - "editableCellTable", - ...times(_filesWIssues.length, i => `editableCellTable-${i}`) + dispatch( + destroy( + "editableCellTable", + ...times(_filesWIssues.length, i => `editableCellTable-${i}`) + ) ); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [_filesWIssues.length, dispatch]); + + const changeForm = (...args) => dispatch(change(...args)); + const { reduxFormEntitiesArray, finishedFiles } = useSelector(state => { + if (_filesWIssues.length > 0) { + const reduxFormEntitiesArray = []; + const finishedFiles = _filesWIssues.map((f, i) => { + const { reduxFormEntities, reduxFormCellValidation } = + formValueSelector(`editableCellTable-${i}`)( + state, + "reduxFormEntities", + "reduxFormCellValidation" + ); + reduxFormEntitiesArray.push(reduxFormEntities); + const { entsToUse, validationToUse } = removeCleanRows( + reduxFormEntities, + reduxFormCellValidation + ); + return ( + entsToUse && + entsToUse.length && + !some(validationToUse, v => v) && + entsToUse + ); + }); + return { + reduxFormEntitiesArray, + finishedFiles + }; + } + }); + const [hasSubmittedOuter, setSubmittedOuter] = useState(); const [steps, setSteps] = useState(getInitialSteps(true)); @@ -118,7 +117,6 @@ const UploadCsvWizardDialog = compose( > {filesWIssues.map((f, i) => { const isGood = finishedFiles[i]; - const isThisTheLastBadFile = finishedFiles.every((ff, j) => { if (i === j) { return true; @@ -135,108 +133,98 @@ const UploadCsvWizardDialog = compose( {" "} + />{" "} {f.file.name} } panel={ { - setSubmittedOuter(false); - setSteps(getInitialSteps(true)); - }), - onMultiFileUploadSubmit: async () => { - let nextUnfinishedFile; - //find the next unfinished file - for ( - let j = (i + 1) % finishedFiles.length; - j < finishedFiles.length; - j++ - ) { - if (j === i) { - break; - } else if (!finishedFiles[j]) { - nextUnfinishedFile = j; - break; - } else if (j === finishedFiles.length - 1) { - j = -1; - } + isThisTheLastBadFile={isThisTheLastBadFile} + onBackClick={ + doAllFilesHaveSameHeaders && + (() => { + setSubmittedOuter(false); + setSteps(getInitialSteps(true)); + }) + } + onMultiFileUploadSubmit={async () => { + let nextUnfinishedFile; + //find the next unfinished file + for ( + let j = (i + 1) % finishedFiles.length; + j < finishedFiles.length; + j++ + ) { + if (j === i) { + break; + } else if (!finishedFiles[j]) { + nextUnfinishedFile = j; + break; + } else if (j === finishedFiles.length - 1) { + j = -1; } - - if (nextUnfinishedFile !== undefined) { - //do async validation here if needed - - const currentEnts = - reduxFormEntitiesArray[focusedTab]; - + } + if (nextUnfinishedFile !== undefined) { + //do async validation here if needed + const currentEnts = reduxFormEntitiesArray[focusedTab]; + if ( + await asyncValidateHelper( + validateAgainstSchema, + currentEnts, + changeForm, + `editableCellTable-${focusedTab}` + ) + ) + return; + setFocusedTab(nextUnfinishedFile); + } else { + //do async validation here if needed + for (const [i, ents] of finishedFiles.entries()) { if ( await asyncValidateHelper( validateAgainstSchema, - currentEnts, + ents, changeForm, - `editableCellTable-${focusedTab}` + `editableCellTable-${i}` ) ) return; - - setFocusedTab(nextUnfinishedFile); - } else { - //do async validation here if needed - - for (const [i, ents] of finishedFiles.entries()) { - if ( - await asyncValidateHelper( - validateAgainstSchema, - ents, - changeForm, - `editableCellTable-${i}` - ) - ) - return; - } - - //we are done - onUploadWizardFinish({ - res: finishedFiles.map(ents => { - return maybeStripIdFromEntities( - ents, - f.validateAgainstSchema - ); - }) - }); } - }, - validateAgainstSchema, - reduxFormEntitiesArray, - filesWIssues, - finishedFiles, - onUploadWizardFinish, - doAllFilesHaveSameHeaders, - destroyForms, - setFilesWIssues, - csvValidationIssue, - ignoredHeadersMsg, - searchResults, - matchedHeaders, - userSchema, - flippedMatchedHeaders, - // reduxFormEntities, - changeForm, - fileIndex: i, - form: `correctCSVHeadersForm-${i}`, - datatableFormName: `editableCellTable-${i}`, - ...f, - ...(doAllFilesHaveSameHeaders && { - csvValidationIssue: false - }) + //we are done + onUploadWizardFinish({ + res: finishedFiles.map(ents => { + return maybeStripIdFromEntities( + ents, + f.validateAgainstSchema + ); + }) + }); + } }} + validateAgainstSchema={validateAgainstSchema} + reduxFormEntitiesArray={reduxFormEntitiesArray} + filesWIssues={filesWIssues} + finishedFiles={finishedFiles} + onUploadWizardFinish={onUploadWizardFinish} + doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} + setFilesWIssues={setFilesWIssues} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeader={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + changeForm={changeForm} + fileIndex={i} + form={`correctCSVHeadersForm-${i}`} + datatableFormName={`editableCellTable-${i}`} + {...f} + {...(doAllFilesHaveSameHeaders && { + csvValidationIssue: false + })} /> } - > + /> ); })} @@ -248,34 +236,27 @@ const UploadCsvWizardDialog = compose( comp = ( <> {doAllFilesHaveSameHeaders && ( - + )} {!hasSubmittedOuter && ( { - return `editableCellTable-${i}`; - }), - reduxFormEntitiesArray, - // onMultiFileUploadSubmit, - csvValidationIssue, - ignoredHeadersMsg, - searchResults, - matchedHeaders, - userSchema, - flippedMatchedHeaders, - // reduxFormEntities, - changeForm, - setFilesWIssues, - filesWIssues, - fileIndex: 0, - ...filesWIssues[0] - }} + doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} + datatableFormNames={filesWIssues.map((f, i) => { + return `editableCellTable-${i}`; + })} + reduxFormEntitiesArray={reduxFormEntitiesArray} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeaders={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + changeForm={changeForm} + setFilesWIssues={setFilesWIssues} + filesWIssues={filesWIssues} + fileIndex={0} + {...filesWIssues[0]} /> )} {hasSubmittedOuter && tabs} @@ -287,230 +268,200 @@ const UploadCsvWizardDialog = compose( setSteps(getInitialSteps(false)); }} text="Review and Edit Data" - > + /> )} ); } - return ( -
- {comp} -
- ); + return
{comp}
; } else { return ( ); } }); -const UploadCsvWizardDialogInner = compose( - reduxForm(), - connect((state, props) => { - return formValueSelector(props.datatableFormName || "editableCellTable")( - state, - "reduxFormEntities", - "reduxFormCellValidation" - ); - }) -)(function UploadCsvWizardDialogInner({ - validateAgainstSchema, - userSchema, - searchResults, - onUploadWizardFinish, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - //fromRedux: - handleSubmit, - fileIndex, - reduxFormEntities, - onBackClick, - reduxFormCellValidation, - changeForm, - setFilesWIssues, - doAllFilesHaveSameHeaders, - filesWIssues, - datatableFormName = "editableCellTable", - onMultiFileUploadSubmit, - isThisTheLastBadFile, - submitting -}) { - const [hasSubmitted, setSubmitted] = useState(!csvValidationIssue); - const [steps, setSteps] = useState(getInitialSteps(csvValidationIssue)); +const UploadCsvWizardDialogInner = reduxForm()( + function UploadCsvWizardDialogInner({ + validateAgainstSchema, + userSchema, + searchResults, + onUploadWizardFinish, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + handleSubmit, + fileIndex, + onBackClick, + changeForm, + setFilesWIssues, + doAllFilesHaveSameHeaders, + filesWIssues, + datatableFormName = "editableCellTable", + onMultiFileUploadSubmit, + isThisTheLastBadFile, + submitting + }) { + const [hasSubmitted, setSubmitted] = useState(!csvValidationIssue); + const [steps, setSteps] = useState(getInitialSteps(csvValidationIssue)); - let inner; - if (hasSubmitted) { - inner = ( - + const { reduxFormEntities, reduxFormCellValidation } = useSelector(state => + formValueSelector(datatableFormName)( + state, + "reduxFormEntities", + "reduxFormCellValidation" + ) ); - } else { - inner = ( - + + let inner; + if (hasSubmitted) { + inner = ( + + ); + } else { + inner = ( + + ); + } + const { entsToUse, validationToUse } = removeCleanRows( + reduxFormEntities, + reduxFormCellValidation ); - } - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation - ); - return ( -
- {!doAllFilesHaveSameHeaders && ( - - )} -
{inner}
- v)) - } - intent={ - hasSubmitted && onMultiFileUploadSubmit && isThisTheLastBadFile - ? Intent.SUCCESS - : Intent.PRIMARY - } - noCancel={onMultiFileUploadSubmit} - {...(hasSubmitted && { - onBackClick: - onBackClick || - (() => { + return ( +
+ {!doAllFilesHaveSameHeaders && ( + + )} +
{inner}
+ v)) + } + intent={ + hasSubmitted && onMultiFileUploadSubmit && isThisTheLastBadFile + ? Intent.SUCCESS + : Intent.PRIMARY + } + noCancel={onMultiFileUploadSubmit} + {...(hasSubmitted && { + onBackClick: + onBackClick || + (() => { + setSteps( + immer(steps, draft => { + draft[0].active = true; + draft[0].completed = false; + draft[1].active = false; + }) + ); + setSubmitted(false); + }) + })} + onClick={handleSubmit(async function () { + if (!hasSubmitted) { + //step 1 submit setSteps( immer(steps, draft => { - draft[0].active = true; - draft[0].completed = false; - draft[1].active = false; + draft[0].active = false; + draft[0].completed = true; + draft[1].active = true; }) ); - setSubmitted(false); - }) - })} - onClick={handleSubmit(async function () { - if (!hasSubmitted) { - //step 1 submit - setSteps( - immer(steps, draft => { - draft[0].active = false; - draft[0].completed = true; - draft[1].active = true; - }) - ); - setSubmitted(true); - } else { - if (!onMultiFileUploadSubmit) { - //do async validation here if needed - if ( - await asyncValidateHelper( - validateAgainstSchema, - entsToUse, - changeForm, - `editableCellTable` + setSubmitted(true); + } else { + if (!onMultiFileUploadSubmit) { + //do async validation here if needed + if ( + await asyncValidateHelper( + validateAgainstSchema, + entsToUse, + changeForm, + `editableCellTable` + ) ) - ) - return; + return; + } + //step 2 submit + const payload = maybeStripIdFromEntities( + entsToUse, + validateAgainstSchema + ); + return onMultiFileUploadSubmit + ? await onMultiFileUploadSubmit() + : onUploadWizardFinish({ res: [payload] }); } - //step 2 submit - const payload = maybeStripIdFromEntities( - entsToUse, - validateAgainstSchema - ); - return onMultiFileUploadSubmit - ? await onMultiFileUploadSubmit() - : onUploadWizardFinish({ res: [payload] }); - } - })} - style={{ alignSelf: "end" }} - > -
- ); -}); + })} + style={{ alignSelf: "end" }} + /> +
+ ); + } +); export default UploadCsvWizardDialog; const exampleData = { userData: times(5).map(() => ({ _isClean: true })) }; -export const PreviewCsvData = observer(function (props) { + +export const PreviewCsvData = observer(props => { const { matchedHeaders, isEditingExistingFile, showDoesDataLookCorrectMsg, headerMessage, datatableFormName, - // onlyShowRowsWErrors, validateAgainstSchema, userSchema = exampleData, initialEntities } = props; const rerenderKey = useRef(0); rerenderKey.current = rerenderKey.current + 1; - // const useExampleData = userSchema === exampleData; - // const [loading, setLoading] = useState(true); - // useEffect(() => { - // // simulate layout change outside of React lifecycle - // setTimeout(() => { - // setLoading(false); - // }, 400); - // }, []); - - // const [val, forceUpdate] = useForceUpdate(); - const data = userSchema.userData && userSchema.userData.length && @@ -575,7 +526,7 @@ export const PreviewCsvData = observer(function (props) { + /> )} + /> ); }); @@ -608,14 +559,12 @@ export const SimpleInsertDataDialog = compose( "reduxFormEntities", "reduxFormCellValidation" ), - connect(undefined, { changeForm: change }), observer )(function SimpleInsertDataDialog({ onSimpleInsertDialogFinish, reduxFormEntities, reduxFormCellValidation, validateAgainstSchema, - changeForm, submitting, isEditingExistingFile, matchedHeaders, @@ -625,11 +574,14 @@ export const SimpleInsertDataDialog = compose( userSchema, initialEntities }) { + const dispatch = useDispatch(); const { entsToUse, validationToUse } = removeCleanRows( reduxFormEntities, reduxFormCellValidation ); + const changeForm = (...args) => dispatch(change(...args)); + return ( <>
@@ -642,20 +594,17 @@ export const SimpleInsertDataDialog = compose( label="File Name:" defaultValue={"manual_data_entry"} name="fileName" - > + /> + matchedHeaders={matchedHeaders} + isEditingExistingFile={isEditingExistingFile} + showDoesDataLookCorrectMsg={showDoesDataLookCorrectMsg} + headerMessage={headerMessage} + validateAgainstSchema={validateAgainstSchema} + userSchema={userSchema} + initialEntities={initialEntities} + datatableFormName={"simpleInsertEditableTable"} + />
e)} text={isEditingExistingFile ? "Edit Data" : "Add File"} - > + /> ); }); @@ -715,11 +664,3 @@ function maybeStripIdFromEntities(ents, validateAgainstSchema) { } return toRet?.map(e => omit(e, ["_isClean"])); } - -//create your forceUpdate hook -// function useForceUpdate() { -// const [val, setValue] = useState(0); // integer state -// return [val, () => setValue(value => value + 1)]; // update state to force render -// // A function that increment 👆🏻 the previous state like here -// // is better than directly setting `setValue(value + 1)` -// } diff --git a/packages/ui/src/index.js b/packages/ui/src/index.js index 72816cc1..ca5addc5 100644 --- a/packages/ui/src/index.js +++ b/packages/ui/src/index.js @@ -18,11 +18,11 @@ export { } from "./DataTable/utils/withSelectedEntities"; export { default as DataTable, - ConnectedPagingTool as PagingTool, - removeCleanRows + ConnectedPagingTool as PagingTool } from "./DataTable"; +export { removeCleanRows } from "./DataTable/utils"; -export { default as getIdOrCodeOrIndex } from "./DataTable/utils/getIdOrCodeOrIndex"; +export { getIdOrCodeOrIndex } from "./DataTable/utils"; export { default as convertSchema } from "./DataTable/utils/convertSchema"; export { default as Loading } from "./Loading"; export { throwFormError } from "./throwFormError"; diff --git a/packages/ui/src/showDialogOnDocBody.js b/packages/ui/src/showDialogOnDocBody.js index 4d4f8bd9..2f1452d4 100644 --- a/packages/ui/src/showDialogOnDocBody.js +++ b/packages/ui/src/showDialogOnDocBody.js @@ -1,4 +1,4 @@ -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import React from "react"; // import withDialog from "./enhancers/withDialog"; import { Dialog } from "@blueprintjs/core"; @@ -19,19 +19,15 @@ export default function showDialogOnDocBody(DialogComp, options = {}) { DialogCompToUse = props => { return ( - + ); }; } else { DialogCompToUse = DialogComp; } - ReactDOM.render( - , - dialogHolder + const root = createRoot(dialogHolder); + root.render( + ); } diff --git a/packages/ui/src/useDialog.js b/packages/ui/src/useDialog.js index 38b1f65e..a9f559c6 100644 --- a/packages/ui/src/useDialog.js +++ b/packages/ui/src/useDialog.js @@ -1,6 +1,6 @@ import React, { useState } from "react"; -/* +/* const {toggleDialog, comp} = useDialog({ ModalComponent: SimpleInsertData, @@ -31,12 +31,14 @@ export const useDialog = ({ ModalComponent, ...rest }) => { ...rest?.dialogProps, ...additionalProps?.dialogProps }} - > + /> ); + const toggleDialog = () => { setOpen(!isOpen); }; - async function showDialogPromise(handlerName, moreProps = {}) { + + const showDialogPromise = async (handlerName, moreProps = {}) => { return new Promise(resolve => { //return a promise that can be awaited setAdditionalProps({ @@ -59,6 +61,7 @@ export const useDialog = ({ ModalComponent, ...rest }) => { }); setOpen(true); //open the dialog }); - } + }; + return { comp, showDialogPromise, toggleDialog, setAdditionalProps }; }; diff --git a/packages/ui/src/utils/renderOnDoc.js b/packages/ui/src/utils/renderOnDoc.js index 2ce4b142..e36f1e14 100644 --- a/packages/ui/src/utils/renderOnDoc.js +++ b/packages/ui/src/utils/renderOnDoc.js @@ -1,29 +1,32 @@ -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; export function renderOnDoc(fn) { const elemDiv = document.createElement("div"); elemDiv.style.cssText = "position:absolute;width:100%;height:100%;top:0px;opacity:0.3;z-index:0;"; document.body.appendChild(elemDiv); + const root = createRoot(elemDiv); const handleClose = () => { setTimeout(() => { - ReactDOM.unmountComponentAtNode(elemDiv); + root.unmount(elemDiv); document.body.removeChild(elemDiv); }); }; - return ReactDOM.render(fn(handleClose), elemDiv); + root.render(fn(handleClose)); } + export function renderOnDocSimple(el) { const elemDiv = document.createElement("div"); elemDiv.style.cssText = "position:absolute;width:100%;height:100%;top:0px;opacity:1;z-index:10000;"; document.body.appendChild(elemDiv); + const root = createRoot(elemDiv); + root.render(el); const handleClose = () => { setTimeout(() => { - ReactDOM.unmountComponentAtNode(elemDiv); + root.unmount(); document.body.removeChild(elemDiv); }); }; - ReactDOM.render(el, elemDiv); return handleClose; }