From 6b22e5b9a536e5e49150fc1932fffb37d9c3fea5 Mon Sep 17 00:00:00 2001 From: Juan Espinosa <30691527+jgespinosa10@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:27:46 -0400 Subject: [PATCH] Revert "Divide DataTable & upgrade react-dom" --- example-demos/oveViteDemo/src/index.jsx | 10 +- .../src/{index.jsx => index.js} | 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/ColumnFilterMenu.js | 60 - packages/ui/src/DataTable/DropdownCell.js | 61 - packages/ui/src/DataTable/EditabelCell.js | 55 - packages/ui/src/DataTable/PagingTool.js | 2 +- packages/ui/src/DataTable/index.js | 1168 +++++++++++------ .../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 - .../src/DataTable/utils/getIdOrCodeOrIndex.js | 2 +- .../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 | 15 - .../ui/src/DataTable/utils/removeCleanRows.js | 22 - packages/ui/src/DataTable/utils/rowClick.js | 11 +- packages/ui/src/DataTable/utils/selection.js | 2 +- packages/ui/src/DataTable/utils/utils.js | 37 - .../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/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 +- 46 files changed, 1686 insertions(+), 1710 deletions(-) rename example-demos/oveWebpackDemo/src/{index.jsx => index.js} (57%) create mode 100644 helperUtils/renderDemo.js delete mode 100644 helperUtils/renderDemo.jsx delete mode 100644 packages/ui/src/DataTable/ColumnFilterMenu.js delete mode 100644 packages/ui/src/DataTable/DropdownCell.js delete mode 100644 packages/ui/src/DataTable/EditabelCell.js delete mode 100644 packages/ui/src/DataTable/utils/formatPasteData.js delete mode 100644 packages/ui/src/DataTable/utils/getAllRows.js delete mode 100644 packages/ui/src/DataTable/utils/getCellCopyText.js delete mode 100644 packages/ui/src/DataTable/utils/getCellInfo.js delete mode 100644 packages/ui/src/DataTable/utils/getFieldPathToField.js delete mode 100644 packages/ui/src/DataTable/utils/getLastSelectedEntity.js delete mode 100644 packages/ui/src/DataTable/utils/getNewEntToSelect.js delete mode 100644 packages/ui/src/DataTable/utils/getRowCopyText.js delete mode 100644 packages/ui/src/DataTable/utils/handleCopyColumn.js delete mode 100644 packages/ui/src/DataTable/utils/handleCopyHelper.js delete mode 100644 packages/ui/src/DataTable/utils/handleCopyRows.js delete mode 100644 packages/ui/src/DataTable/utils/index.js delete mode 100644 packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js delete mode 100644 packages/ui/src/DataTable/utils/isEntityClean.js delete mode 100644 packages/ui/src/DataTable/utils/removeCleanRows.js delete mode 100644 packages/ui/src/DataTable/utils/utils.js diff --git a/example-demos/oveViteDemo/src/index.jsx b/example-demos/oveViteDemo/src/index.jsx index 8cf9dcc2..122a5b87 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 { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import { Loading } from "@teselagen/ui"; @@ -10,16 +10,14 @@ import App from "./App"; import * as serviceWorker from "./serviceWorker"; -const domNode = document.getElementById("root"); -const root = createRoot(domNode); - -root.render( +ReactDOM.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.jsx b/example-demos/oveWebpackDemo/src/index.js similarity index 57% rename from example-demos/oveWebpackDemo/src/index.jsx rename to example-demos/oveWebpackDemo/src/index.js index 5f969828..d772e46f 100644 --- a/example-demos/oveWebpackDemo/src/index.jsx +++ b/example-demos/oveWebpackDemo/src/index.js @@ -1,17 +1,15 @@ import React from "react"; -import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import store from "./store"; import "./index.css"; import App from "./App"; -const domNode = document.getElementById("root"); -const root = createRoot(domNode); - -root.render( +ReactDOM.render( - + , + document.getElementById("root") ); diff --git a/helperUtils/renderDemo.js b/helperUtils/renderDemo.js new file mode 100644 index 00000000..e1439ab7 --- /dev/null +++ b/helperUtils/renderDemo.js @@ -0,0 +1,2 @@ +import { render } from "react-dom"; +export default Demo => render(, document.querySelector("#demo")); diff --git a/helperUtils/renderDemo.jsx b/helperUtils/renderDemo.jsx deleted file mode 100644 index e47a951f..00000000 --- a/helperUtils/renderDemo.jsx +++ /dev/null @@ -1,3 +0,0 @@ -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 9fefb64e..5d1cdd5e 100644 --- a/packages/ui/cypress/e2e/EditableCellTable.spec.js +++ b/packages/ui/cypress/e2e/EditableCellTable.spec.js @@ -29,6 +29,8 @@ 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` @@ -37,6 +39,8 @@ 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"); }); @@ -122,6 +126,8 @@ 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( @@ -160,10 +166,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(); + // cy.get(`[data-test="tgCell_name"]`).eq(3).click({ force: true }); // 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"); @@ -225,7 +231,9 @@ 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(); + cy.get(`[data-index="49"] [data-test="tgCell_isProtein"]`).click({ + force: true + }); cy.get( `[data-index="49"] .rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_isProtein"]` ); @@ -245,8 +253,12 @@ 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 874a46a9..fcbfcc6d 100644 --- a/packages/ui/cypress/e2e/UploadCsvWizard.spec.js +++ b/packages/ui/cypress/e2e/UploadCsvWizard.spec.js @@ -147,6 +147,17 @@ 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", @@ -166,21 +177,19 @@ describe("UploadCsvWizard.spec", () => { ); cy.get(`.tg-test-is-regex`).click(); - cy.contains(".tg-select-option", "typo").click(); + cy.contains(".tg-select-option", "typo").click({ force: true }); 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` - ) - .parent() - .type("a{enter}"); + ).dblclick({ force: true }); + cy.focused().type("a{enter}"); cy.get( `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_sequence"]:first` - ) - .parent() - .type("g{enter}"); + ).dblclick({ force: true }); + cy.focused().type("g{enter}"); cy.dragBetween(`.cellDragHandle`, `.rt-tr-last-row`); cy.contains("Add File").click(); cy.contains(`testUploadWizard_messedUpHeaders.csv`); @@ -188,7 +197,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):last` + `[data-index="4"] [data-test="tgCell_sequence"]:contains(g)` ).click(); cy.focused().type(`{backspace}`); cy.get(`.bp3-disabled:contains(Edit Data)`); @@ -637,7 +646,13 @@ 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(`.hasCellError`).type("haha{enter}"); + 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:last [data-test="tgCell_name"]`).type("haha{enter}", {force: true}); cy.get(`button:contains(Next File):first`).click(); cy.get( @@ -661,7 +676,9 @@ 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"]:last`).click(); + cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ + force: true + }); cy.focused().type(`tom{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -670,7 +687,9 @@ 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(); + cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ + force: true + }); // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.focused().type(`robbin{enter}`, { delay: 100 }); @@ -1046,14 +1065,14 @@ thomas,,g,false,dna,misc_feature`, cy.visit("#/UploadCsvWizard"); cy.tgToggle("allowMultipleFiles"); cy.contains("Build CSV File").click(); - cy.get(`.rt-tbody [role="gridcell"]:first`).click(); + cy.get(`[data-test="tgCell_name"]:first`).click({ force: true }); 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(`.rt-tbody [role="gridcell"]:first`).click(); + cy.get(`[data-test="tgCell_name"]:first`).click({ force: true }); cy.focused().paste(`Thomas Wee agagag False dna misc_feature`); cy.contains(".bp3-button", "Add File").click(); cy.contains("manual_data_entry(1).csv"); @@ -1062,11 +1081,13 @@ thomas,,g,false,dna,misc_feature`, ).click(); cy.contains(`Edit your data here.`); cy.contains(`Add 10 Rows`).click(); - cy.get(`[data-index="4"] [role="gridcell"]`).eq(2).click(); + cy.get(`[data-index="4"] [data-test="tgCell_sequence"]`).click({ + force: true + }); cy.focused().type(`{backspace}`); cy.get(`.bp3-disabled:contains(Edit Data)`).should("not.exist"); cy.focused().type(`tom{enter}`); - cy.get(`[data-index="4"] [role="gridcell"]:first`).click(); + cy.get(`[data-index="4"] [data-test="tgCell_name"]`).click({ force: true }); cy.focused().type(`taoh{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -1075,7 +1096,9 @@ 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"] [role="gridcell"]`).eq(2).click(); + cy.get(`[data-index="0"] [data-test="tgCell_sequence"]`).click({ + force: true + }); cy.focused().type(`tom{enter}`); cy.get(`.bp3-button:contains(Edit Data)`).click(); cy.contains(`File Updated`); @@ -1190,14 +1213,23 @@ thomas,,g,false,dna,misc_feature`, cy.contains( `Input your data here. Hover table headers for additional instructions` ); - cy.get(".rt-td").eq(1).type("description{enter}"); + cy.get(`.rt-td [data-test="tgCell_description"]`) + .eq(1) + .click({ force: true }); + cy.focused().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(".rt-td").eq(0).type("a{enter}"); - cy.get(".rt-td").eq(2).type("g{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.get( + `.hasCellError[data-tip="Please enter a value here"] [data-test="tgCell_sequence"]:first` + ).dblclick({ force: true }); + cy.focused().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 33f1eef8..dc60bc2d 100644 --- a/packages/ui/cypress/e2e/dataTable.spec.js +++ b/packages/ui/cypress/e2e/dataTable.spec.js @@ -9,7 +9,8 @@ describe("dataTable.spec", () => { cy.tgToggle("getRowClassName"); cy.get(".rt-tr-group.custom-getRowClassName").should("exist"); }); - it(`it can select entities across pages`, () => { + //TODO THIS IS BREAKING! + it.skip(`it can select entities across pages`, () => { cy.visit("#/DataTable"); cy.contains("0 Selected"); //select first entity @@ -161,7 +162,7 @@ describe("dataTable.spec", () => { checkIndices("greaterThan"); }); - it("page size will persist on reload", () => { + it.skip("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 9e7ff732..b7174025 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 .zip, .json"); + cy.contains("type must be .json"); cy.uploadBlobFiles( ".fileUploadLimitAndType.tg-dropzone", [ diff --git a/packages/ui/cypress/support/index.js b/packages/ui/cypress/support/index.js index f09f7121..f8c861a6 100644 --- a/packages/ui/cypress/support/index.js +++ b/packages/ui/cypress/support/index.js @@ -46,7 +46,9 @@ Cypress.Commands.add("dragBetween", (dragSelector, dropSelector) => { getOrWrap(dragSelector) .trigger("mousedown") .trigger("mousemove", 10, 10, { force: true }); - getOrWrap(dropSelector).trigger("mousemove").trigger("mouseup"); + getOrWrap(dropSelector) + .trigger("mousemove", { force: true }) + .trigger("mouseup", { force: true }); }); Cypress.Commands.add( diff --git a/packages/ui/demo/src/examples/UploadCsvWizard.js b/packages/ui/demo/src/examples/UploadCsvWizard.js index c76c8fca..3f68dd88 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"; +import getIdOrCodeOrIndex from "../../../src/DataTable/utils/getIdOrCodeOrIndex"; 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 1770aa6d..8a200429 100644 --- a/packages/ui/demo/src/examples/UploaderDemo.js +++ b/packages/ui/demo/src/examples/UploaderDemo.js @@ -21,6 +21,10 @@ 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 6e11ad22..adad2335 100644 --- a/packages/ui/demo/src/index.js +++ b/packages/ui/demo/src/index.js @@ -25,6 +25,7 @@ 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"; @@ -32,7 +33,6 @@ 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,5 +261,4 @@ const Demo = () => { ); }; -const root = createRoot(document.querySelector("#demo")); -root.render(); +render(, document.querySelector("#demo")); diff --git a/packages/ui/src/DataTable/CellDragHandle.js b/packages/ui/src/DataTable/CellDragHandle.js index 855d93d2..154918d7 100644 --- a/packages/ui/src/DataTable/CellDragHandle.js +++ b/packages/ui/src/DataTable/CellDragHandle.js @@ -1,20 +1,21 @@ import { flatMap } from "lodash-es"; import { forEach } from "lodash-es"; import React, { useRef } from "react"; +import ReactDOM from "react-dom"; -export const CellDragHandle = ({ +export function CellDragHandle({ thisTable, onDragEnd, cellId, isSelectionARectangle -}) => { +}) { const xStart = useRef(0); const timeoutkey = useRef(); const rowsToSelect = useRef(); const rectangleCellPaths = useRef(); const handleDrag = useRef(e => { - const table = thisTable.querySelector(".rt-table"); + const table = ReactDOM.findDOMNode(thisTable).querySelector(".rt-table"); const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [rowId, path] = cellId.split(":"); const selectedTr = table.querySelector( @@ -82,7 +83,7 @@ export const CellDragHandle = ({ const mouseup = useRef(() => { clearTimeout(timeoutkey.current); - const table = thisTable; + const table = ReactDOM.findDOMNode(thisTable); const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [, path] = cellId.split(":"); //remove the dashed borders @@ -125,6 +126,6 @@ export const CellDragHandle = ({ document.addEventListener("mouseup", mouseup.current, false); }} className="cellDragHandle" - /> + >
); -}; +} diff --git a/packages/ui/src/DataTable/ColumnFilterMenu.js b/packages/ui/src/DataTable/ColumnFilterMenu.js deleted file mode 100644 index 1688fa32..00000000 --- a/packages/ui/src/DataTable/ColumnFilterMenu.js +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 23e9ccd2..00000000 --- a/packages/ui/src/DataTable/DropdownCell.js +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index c6453c92..00000000 --- a/packages/ui/src/DataTable/EditabelCell.js +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; - -export const EditableCell = ({ - cancelEdit, - dataTest, - finishEdit, - initialValue, - isEditableCellInitialValue, - isNumeric, - shouldSelectAll, - stopSelectAll -}) => { - 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 (e.key === "Enter") { - finishEdit(value); - e.stopPropagation(); - } else if (e.key === "Escape") { - e.stopPropagation(); - cancelEdit(); - } - }} - 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 f422bef5..d13335ec 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"; +import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; 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 ce5eb73a..82a9391e 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -1,10 +1,15 @@ -import React, { createRef } from "react"; +/* eslint react/jsx-no-bind: 0 */ +import React, { useState } 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, @@ -26,6 +31,7 @@ import { every } from "lodash-es"; import joinUrl from "url-join"; + import { Button, Menu, @@ -34,6 +40,7 @@ import { ContextMenu, Checkbox, Icon, + Popover, Intent, Callout, Tooltip @@ -51,50 +58,30 @@ import immer, { produceWithPatches, enablePatches, applyPatches } from "immer"; import papaparse from "papaparse"; import remarkGfm from "remark-gfm"; -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 TgSelect from "../TgSelect"; import { withHotkeys } from "../utils/hotkeyUtils"; +import InfoHelper from "../InfoHelper"; 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"; @@ -103,27 +90,15 @@ 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); - - this.tableRef = createRef(); if (this.props.helperProp) { this.props.helperProp.updateValidationHelper = this.updateValidationHelper; @@ -204,52 +179,25 @@ class DataTable extends React.Component { } }); } - state = { columns: [], - 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: "" + fullscreen: false }; - static defaultProps = defaultProps; - getPrimarySelectedCellId = () => { - const { reduxFormSelectedCells = {} } = this.props; - for (const k of Object.keys(reduxFormSelectedCells)) { - if (reduxFormSelectedCells[k] === PRIMARY_SELECTED_VAL) { - return k; - } - } - }; + static defaultProps = defaultProps; - 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); - } + toggleFullscreen = () => { + this.setState({ + fullscreen: !this.state.fullscreen + }); }; - handleEnterStartCellEdit = e => { e.stopPropagation(); this.startCellEdit(this.getPrimarySelectedCellId()); }; - flashTableBorder = () => { try { - const table = this.tableRef.current.tableRef; + const table = ReactDOM.findDOMNode(this.table); table.classList.add("tgBorderBlue"); setTimeout(() => { table.classList.remove("tgBorderBlue"); @@ -258,59 +206,6 @@ class DataTable extends React.Component { console.error(`err when flashing table border:`, e); } }; - - 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, @@ -336,7 +231,6 @@ class DataTable extends React.Component { }); } }; - handleRedo = () => { const { change, @@ -361,7 +255,6 @@ class DataTable extends React.Component { }); } }; - updateFromProps = (oldProps, newProps) => { const { selectedIds, @@ -373,6 +266,7 @@ class DataTable extends React.Component { reduxFormExpandedEntityIdMap, change } = newProps; + const table = ReactDOM.findDOMNode(this.table); const idMap = reduxFormSelectedEntityIdMap; @@ -438,8 +332,7 @@ 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 = - this.tableRef.current.tableRef.getElementsByClassName("rt-table")[0]; + const tableScrollElement = table.getElementsByClassName("rt-table")[0]; const { entities: oldEntities = [], reduxFormSelectedEntityIdMap: oldIdMap @@ -488,9 +381,8 @@ class DataTable extends React.Component { const entityIndexToScrollTo = entities.findIndex( e => e.id === idToScrollTo || e.code === idToScrollTo ); - if (entityIndexToScrollTo === -1 || !this.tableRef.current) return; - const tableBody = - this.tableRef.current.tableRef.querySelector(".rt-tbody"); + if (entityIndexToScrollTo === -1 || !table) return; + const tableBody = table.querySelector(".rt-tbody"); if (!tableBody) return; const rowEl = tableBody.getElementsByClassName("rt-tr-group")[entityIndexToScrollTo]; @@ -505,7 +397,45 @@ class DataTable extends React.Component { }, 0); } }; + 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, @@ -532,25 +462,151 @@ class DataTable extends React.Component { }); }; - 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 + 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() + }; } + } + + 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 { @@ -663,188 +719,49 @@ class DataTable extends React.Component { const pathToIndex = getFieldPathToIndex(schema); const indexToPath = invert(pathToIndex); const startCellIndex = pathToIndex[primaryCellPath]; - pasteData.forEach((row, i) => { - row.forEach((newVal, j) => { - if (newVal) { - const cellIndexToChange = startCellIndex + j; - const entity = entitiesToManipulate[i]; - if (entity) { - delete entity._isClean; - const path = indexToPath[cellIndexToChange]; - if (path) { - const { error } = editCellHelper({ - entity, - path, - schema, - newVal: formatPasteData({ - newVal, - path, - schema - }) - }); - const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`; - if (!newSelectedCells[cellId]) { - newSelectedCells[cellId] = true; - } - if (error) { - newCellValidate[cellId] = error; - } else { - delete newCellValidate[cellId]; - } - } - } - } - }); - }); - this.updateValidation(entities, newCellValidate); - }); - change("reduxFormSelectedCells", newSelectedCells); - } - } - } catch (error) { - console.error(`error:`, error); - } - } - }; - - 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() - } - }; + pasteData.forEach((row, i) => { + row.forEach((newVal, j) => { + if (newVal) { + const cellIndexToChange = startCellIndex + j; + const entity = entitiesToManipulate[i]; + if (entity) { + delete entity._isClean; + const path = indexToPath[cellIndexToChange]; + if (path) { + const { error } = editCellHelper({ + entity, + path, + schema, + newVal: formatPasteData({ + newVal, + path, + schema + }) + }); + const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`; + if (!newSelectedCells[cellId]) { + newSelectedCells[cellId] = true; + } + if (error) { + newCellValidate[cellId] = error; + } else { + delete newCellValidate[cellId]; + } + } + } + } + }); + }); + this.updateValidation(entities, newCellValidate); + }); + change("reduxFormSelectedCells", newSelectedCells); + } } - } else { - //no shiftHeld - newIdMap[newEntToSelect.id || newEntToSelect.code] = { - entity: newEntToSelect, - time: Date.now() - }; + } catch (error) { + console.error(`error:`, error); } } - - 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, @@ -883,12 +800,22 @@ class DataTable extends React.Component { }); } }; - updateValidationHelper = () => { const { entities, reduxFormCellValidation } = computePresets(this.props); 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, @@ -929,11 +856,119 @@ 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; + 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 + } + }); + }; + + 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; - handleCopyRows(allRowEls, { + this.handleCopyRows(allRowEls, { ...opts, onFinishMsg: "Table Copied" }); @@ -942,7 +977,6 @@ class DataTable extends React.Component { window.toastr.error("Error copying rows."); } }; - handleCopySelectedCells = e => { const { entities = [], @@ -988,7 +1022,7 @@ class DataTable extends React.Component { } else { const jsonRow = []; // ignore header - let [rowCopyText, json] = getRowCopyText(allRows[i + 1]); + let [rowCopyText, json] = this.getRowCopyText(allRows[i + 1]); rowCopyText = rowCopyText.split("\t"); times(row.length, i => { const cell = row[i]; @@ -1003,7 +1037,7 @@ class DataTable extends React.Component { }); if (!fullCellText) return window.toastr.warning("No text to copy"); - handleCopyHelper(fullCellText, fullJson, "Selected cells copied"); + this.handleCopyHelper(fullCellText, fullJson, "Selected cells copied"); }; handleCopySelectedRows = (selectedRecords, e) => { @@ -1026,7 +1060,7 @@ class DataTable extends React.Component { if (!allRowEls) return; const rowEls = rowNumbersToCopy.map(i => allRowEls[i]); - handleCopyRows(rowEls, { + this.handleCopyRows(rowEls, { onFinishMsg: "Selected rows copied" }); } catch (error) { @@ -1083,7 +1117,6 @@ class DataTable extends React.Component { /> ); }; - getThComponent = compose( withProps(props => { const { columnindex } = props; @@ -1302,11 +1335,7 @@ class DataTable extends React.Component { icon="fullscreen" active={fullscreen} minimal - onClick={() => { - this.setState({ - fullscreen: !this.state.fullscreen - }); - }} + onClick={this.toggleFullscreen} /> ); @@ -1461,17 +1490,24 @@ class DataTable extends React.Component { {...(isCellEditable && { tabIndex: -1, onKeyDown: e => { - const isTabKey = e.key === "Tab"; - const isArrowKey = e.key.startsWith("Arrow"); + // 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) if ( (isArrowKey && e.target?.tagName !== "INPUT") || isTabKey // || (isEnter && e.target?.tagName === "INPUT") ) { const { schema, entities } = computePresets(this.props); - const left = e.key === "ArrowLeft"; - const up = e.key === "ArrowUp"; - const down = e.key === "ArrowDown" || e.key === "Enter"; + const left = e.keyCode === 37; + const up = e.keyCode === 38; + const down = e.keyCode === 40 || e.keyCode === 13; let cellIdToUse = this.getPrimarySelectedCellId(); const pathToIndex = getFieldPathToIndex(schema); const entityMap = getEntityIdToEntity(entities); @@ -1564,23 +1600,13 @@ class DataTable extends React.Component { const entity = entityIdToEntity[rowId].e; if (!entity) return; const rowDisabled = isEntityDisabled(entity); - 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 - })); - } + const isNum = e.keyCode >= 48 && e.keyCode <= 57; + const isLetter = e.keyCode >= 65 && e.keyCode <= 90; + if (!isNum && !isLetter) return; if (rowDisabled) return; this.startCellEdit(cellId, { shouldSelectAll: true }); e.stopPropagation(); + // e.preventDefault(); } })} > @@ -1720,7 +1746,10 @@ class DataTable extends React.Component { )} { + if (n) this.table = n; + }} + // additionalBodyEl={} className={classNames({ isCellEditable, "tg-table-loading": isLoading, @@ -1807,7 +1836,7 @@ class DataTable extends React.Component { data-tip="Download Table as CSV" minimal icon="download" - /> + > )} {!noFooter && ( @@ -1952,6 +1981,24 @@ 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, @@ -2147,7 +2194,6 @@ class DataTable extends React.Component { change("reduxFormSelectedCells", newSelectedCells); }; - renderCheckboxHeader = () => { const { reduxFormSelectedEntityIdMap, @@ -2270,10 +2316,9 @@ class DataTable extends React.Component { change("reduxFormEditingCell", null); this.refocusTable(); }; - refocusTable = () => { setTimeout(() => { - const table = this.tableRef.current?.tableRef?.closest( + const table = ReactDOM.findDOMNode(this.table)?.closest( ".data-table-container>div" ); table?.focus(); @@ -2550,6 +2595,7 @@ class DataTable extends React.Component { { const checked = e.target.checked; @@ -2559,6 +2605,9 @@ class DataTable extends React.Component { ); noEllipsis = true; } else { + // if (column.type === "genericSelect") { + // val = + // } if (reduxFormEditingCell === cellId) { if (column.type === "genericSelect") { const GenericSelectComp = column.GenericSelectComp; @@ -2589,7 +2638,7 @@ class DataTable extends React.Component { }} dataTest={dataTest} cancelEdit={this.cancelCellEdit} - /> + > ); } else { return ( @@ -2601,18 +2650,11 @@ class DataTable extends React.Component { shouldSelectAll={reduxFormEditingCellSelectAll} cancelEdit={this.cancelCellEdit} isNumeric={column.type === "number"} - initialValue={ - this.state.editableCellInitialValue.length - ? this.state.editableCellInitialValue - : text - } - isEditableCellInitialValue={ - !!this.state.editableCellInitialValue.length - } + initialValue={text} finishEdit={newVal => { this.finishCellEdit(cellId, newVal); }} - /> + > ); } } @@ -2684,7 +2726,7 @@ class DataTable extends React.Component { {isSelectedCell && (isRect - ? isBottomRightCornerOfRectangle({ + ? this.isBottomRightCornerOfRectangle({ cellId, selectionGrid, lastRowIndex, @@ -2695,11 +2737,11 @@ class DataTable extends React.Component { : isSelectedCell === PRIMARY_SELECTED_VAL) && ( + > )} ); @@ -2709,6 +2751,26 @@ 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 { @@ -2913,7 +2975,6 @@ 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) @@ -2999,7 +3060,6 @@ class DataTable extends React.Component { }); }); }; - getEditableTableInfoAndThrowFormError = () => { const { schema, reduxFormEntities, reduxFormCellValidation } = computePresets(this.props); @@ -3109,12 +3169,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"); - handleCopyRows([cellWrapper.closest(".rt-tr")], { + this.handleCopyRows([cellWrapper.closest(".rt-tr")], { specificColumn, onFinishMsg: "Cell copied" }); - const [text, jsonText] = getCellCopyText(cellWrapper); - handleCopyHelper(text, jsonText); + const [text, jsonText] = this.getCellCopyText(cellWrapper); + this.handleCopyHelper(text, jsonText); }} text="Cell" /> @@ -3124,7 +3184,7 @@ class DataTable extends React.Component { { - handleCopyColumn(e, cellWrapper); + this.handleCopyColumn(e, cellWrapper); }} text="Column" /> @@ -3134,7 +3194,7 @@ class DataTable extends React.Component { { - handleCopyColumn(e, cellWrapper, selectedRecords); + this.handleCopyColumn(e, cellWrapper, selectedRecords); }} text="Column (Selected)" /> @@ -3152,7 +3212,7 @@ class DataTable extends React.Component { { - handleCopyRows([row]); + this.handleCopyRows([row]); // loop through each cell in the row }} text="Row" @@ -3203,7 +3263,7 @@ class DataTable extends React.Component { onClick={() => { this.insertRows({ above: true }); }} - /> + > { this.insertRows({}); }} - /> + > 1 ? "s" : ""}`} @@ -3401,7 +3461,7 @@ class DataTable extends React.Component { }} indeterminate={isIndeterminate} checked={isChecked} - /> + > ); } @@ -3426,7 +3486,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 deleted file mode 100644 index cde19517..00000000 --- a/packages/ui/src/DataTable/utils/formatPasteData.js +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 64c200f1..00000000 --- a/packages/ui/src/DataTable/utils/getAllRows.js +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 7fca4538..00000000 --- a/packages/ui/src/DataTable/utils/getCellCopyText.js +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index d863c727..00000000 --- a/packages/ui/src/DataTable/utils/getCellInfo.js +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 52b4fe33..00000000 --- a/packages/ui/src/DataTable/utils/getFieldPathToField.js +++ /dev/null @@ -1,7 +0,0 @@ -export const getFieldPathToField = schema => { - const fieldPathToField = {}; - schema.fields.forEach(f => { - fieldPathToField[f.path] = f; - }); - return fieldPathToField; -}; diff --git a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js index 2a210198..bcbf17c1 100644 --- a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js +++ b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js @@ -1,4 +1,4 @@ -export const getIdOrCodeOrIndex = (record, rowIndex) => { +export default (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 deleted file mode 100644 index ecfbae5b..00000000 --- a/packages/ui/src/DataTable/utils/getLastSelectedEntity.js +++ /dev/null @@ -1,11 +0,0 @@ -export const getLastSelectedEntity = idMap => { - let lastSelectedEnt; - let latestTime; - Object.values(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 deleted file mode 100644 index a7d47155..00000000 --- a/packages/ui/src/DataTable/utils/getNewEntToSelect.js +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index c706bd63..00000000 --- a/packages/ui/src/DataTable/utils/getRowCopyText.js +++ /dev/null @@ -1,28 +0,0 @@ -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 = []; - - for (const cellEl of rowEl.children) { - const cellChild = cellEl.querySelector(`[data-copy-text]`); - if (!cellChild) { - if (specificColumn) continue; //strip it - continue; //just leave it blank - } - if ( - specificColumn && - cellChild.getAttribute("data-test") !== specificColumn - ) { - 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 deleted file mode 100644 index 4aa04870..00000000 --- a/packages/ui/src/DataTable/utils/handleCopyColumn.js +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 9d603e99..00000000 --- a/packages/ui/src/DataTable/utils/handleCopyHelper.js +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 197cbabc..00000000 --- a/packages/ui/src/DataTable/utils/handleCopyRows.js +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 2d85f6cb..00000000 --- a/packages/ui/src/DataTable/utils/index.js +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index 271e26ed..00000000 --- a/packages/ui/src/DataTable/utils/isBottomRightCornerOfRectangle.js +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index f9d507d4..00000000 --- a/packages/ui/src/DataTable/utils/isEntityClean.js +++ /dev/null @@ -1,15 +0,0 @@ -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; - for (const [key, val] of Object.entries(e)) { - if (key === "id") continue; - if (key === "_isClean") continue; - if (val) { - isClean = false; - break; - } - } - return isClean; -} diff --git a/packages/ui/src/DataTable/utils/removeCleanRows.js b/packages/ui/src/DataTable/utils/removeCleanRows.js deleted file mode 100644 index 5958166a..00000000 --- a/packages/ui/src/DataTable/utils/removeCleanRows.js +++ /dev/null @@ -1,22 +0,0 @@ -import { isEntityClean } from "./isEntityClean"; -import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; - -export const 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 = {}; - 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 e4ee123a..190d3cdb 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) { @@ -124,10 +124,8 @@ export function changeSelectedEntities({ idMap, entities = [], change }) { change("reduxFormSelectedEntityIdMap", newIdMap); } -export function finalizeSelection({ - idMap, - entities, - props: { +export function finalizeSelection({ idMap, entities, props }) { + const { onDeselect, onSingleRowSelect, onMultiRowSelect, @@ -135,8 +133,7 @@ export function finalizeSelection({ onRowSelect, noSelect, change - } -}) { + } = props; if (noSelect) return; if ( noDeselectAll && diff --git a/packages/ui/src/DataTable/utils/selection.js b/packages/ui/src/DataTable/utils/selection.js index 39757606..ea4a08d8 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 deleted file mode 100644 index fe7b4d1c..00000000 --- a/packages/ui/src/DataTable/utils/utils.js +++ /dev/null @@ -1,37 +0,0 @@ -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/DataTable/validateTableWideErrors.js b/packages/ui/src/DataTable/validateTableWideErrors.js index c29a36b2..11c2eb93 100644 --- a/packages/ui/src/DataTable/validateTableWideErrors.js +++ b/packages/ui/src/DataTable/validateTableWideErrors.js @@ -1,4 +1,4 @@ -import { getIdOrCodeOrIndex } from "./utils"; +import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; 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 c333c342..ab25adc5 100644 --- a/packages/ui/src/FillWindow.js +++ b/packages/ui/src/FillWindow.js @@ -1,5 +1,6 @@ -import React, { createPortal } from "react"; +import React from "react"; import { isFunction } from "lodash-es"; +import reactDom from "react-dom"; import rerenderOnWindowResize from "./rerenderOnWindowResize"; import "./FillWindow.css"; @@ -62,7 +63,7 @@ export default class FillWindow extends React.Component { : this.props.children}
); - if (asPortal) return createPortal(inner, window.document.body); + if (asPortal) return reactDom.createPortal(inner, window.document.body); return inner; } } diff --git a/packages/ui/src/FormComponents/Uploader.js b/packages/ui/src/FormComponents/Uploader.js index e668b731..f1c0b007 100644 --- a/packages/ui/src/FormComponents/Uploader.js +++ b/packages/ui/src/FormComponents/Uploader.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Button, Callout, @@ -16,6 +16,7 @@ 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 @@ -29,7 +30,7 @@ import { removeExt } from "@teselagen/file-utils"; import tryToMatchSchemas from "./tryToMatchSchemas"; -import { isArray, isFunction, isPlainObject, noop } from "lodash-es"; +import { forEach, isArray, isFunction, isPlainObject, noop } from "lodash-es"; import { flatMap } from "lodash-es"; import urljoin from "url-join"; import popoverOverflowModifiers from "../utils/popoverOverflowModifiers"; @@ -37,12 +38,14 @@ import writeXlsxFile from "write-excel-file"; import { startCase } from "lodash-es"; import { getNewName } from "./getNewName"; import { isObject } from "lodash-es"; -import { useDispatch } from "react-redux"; +import { connect } 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.`, @@ -61,106 +64,58 @@ const helperSchema = [ } ]; -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.` - ); +class ValidateAgainstSchema { + fields = []; + + constructor() { + makeObservable(this, { + fields: observable.shallow + }); } - return schema; -}; -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} -
*/} -
- )} -
- )} -
+ 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; + }); + } +} - {showFilesCount ? ( -
- Files: {fileList ? fileList.length : 0} -
- ) : null} -
-); +// autorun(() => { +// console.log( +// `validateAgainstSchemaStore?.fields:`, +// JSON.stringify(validateAgainstSchemaStore?.fields, null, 4) +// ); +// }); +// validateAgainstSchemaStore.fields = ["hahah"]; +// validateAgainstSchemaStore.fields.push("yaa"); -const UploaderInner = ({ +// const validateAgainstSchema = observable.shallow({ +// fields: [] +// }) + +// validateAgainstSchema.fields = ["hahah"]; + +// wink wink +const emptyPromise = Promise.resolve.bind(Promise); + +function UploaderInner({ accept: __accept, contentOverride: maybeContentOverride, innerIcon, @@ -175,9 +130,7 @@ const UploaderInner = ({ showUploadList = true, beforeUpload, fileList, //list of files with options: {name, loading, error, url, originalName, downloadName} - onFileSuccess = async () => { - return; - }, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! + onFileSuccess = emptyPromise, //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 @@ -188,27 +141,22 @@ const 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 = useMemo( - () => - __accept?.then || - (Array.isArray(__accept) ? __accept.some(a => a?.then) : false), - [__accept] - ); - + const isAcceptPromise = + __accept?.then || + (Array.isArray(__accept) ? __accept.some(a => a?.then) : false); useEffect(() => { if (isAcceptPromise) { setAcceptLoading(true); @@ -221,36 +169,35 @@ const UploaderInner = ({ ); } }, [__accept, isAcceptPromise]); - if (isAcceptPromise && !resolvedAccept) { _accept = []; } - if (acceptLoading) dropzoneDisabled = true; - const accept = useMemo( - () => - !_accept - ? undefined - : isAcceptPromise && !resolvedAccept - ? [] - : isPlainObject(_accept) - ? [_accept] - : isArray(_accept) - ? _accept - : _accept.split(",").map(a => ({ type: a })), - [_accept, isAcceptPromise, resolvedAccept] - ); - + 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 validateAgainstSchema = useMemo( - () => - setValidateAgainstSchema( - _validateAgainstSchema || - accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema - ), - [_validateAgainstSchema, accept] - ); + const validateAgainstSchemaToUse = + _validateAgainstSchema || + accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema; + + useEffect(() => { + // validateAgainstSchema + validateAgainstSchemaStore.current.setValidateAgainstSchema( + validateAgainstSchemaToUse + ); + }, [validateAgainstSchemaToUse]); + let validateAgainstSchema; + if (validateAgainstSchemaToUse) { + validateAgainstSchema = validateAgainstSchemaStore.current; + } if ( (validateAgainstSchema || autoUnzip) && @@ -268,7 +215,6 @@ const UploaderInner = ({ const { showDialogPromise: showUploadCsvWizardDialog, comp } = useDialog({ ModalComponent: UploadCsvWizardDialog }); - const { showDialogPromise: showSimpleInsertDataDialog, comp: comp2 } = useDialog({ ModalComponent: SimpleInsertDataDialog @@ -607,7 +553,7 @@ const UploaderInner = ({ } {...getFileDownloadAttr(exampleFile)} key={i} - /> + > ); } )} @@ -673,7 +619,7 @@ const UploaderInner = ({ }} size={10} icon="download" - /> + > )} @@ -685,8 +631,7 @@ const UploaderInner = ({ // make the dots below "load" <> - Accept Loading - + Accept Loading ) : ( <>Accepts {simpleAccept} @@ -705,135 +650,135 @@ const 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); + {...{ + 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(", "); + 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}
+ ); + } + 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); }); - 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(); + 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); + }; + }); + }) + ); } - filesToClean.current.push(file); - }); + 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 } + : {}) + }; + }); - 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 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; + } - 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 + const { + csvValidationIssue: _csvValidationIssue, + matchedHeaders, + userSchema, + searchResults, + ignoredHeadersMsg + } = await tryToMatchSchemas({ + incomingData: parsedF.data, + validateAgainstSchema }); - } catch (error) { - console.error("error:", error); - window.toastr && - window.toastr.error( - `There was an error parsing your file. Please try again. ${ - error.message || error - }` + if (userSchema?.userData?.length === 0) { + console.error( + `userSchema, parsedF.data:`, + userSchema, + parsedF.data ); - 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( + } else { + toKeep.push(file); + let csvValidationIssue = _csvValidationIssue; + if (csvValidationIssue) { + if (isObject(csvValidationIssue)) { + initializeForm( `editableCellTable${ cleanedAccepted.length > 1 ? `-${i}` : "" }`, @@ -845,142 +790,149 @@ const 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 - )}` ); - } - csvValidationIssue = ( -
-
- It looks like there was an error with your data - (Correct on the Review Data page): -
-
{errMsg}
+ 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 = (
- 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): +
+
{errMsg}
+
+ Please review your headers and then correct + any errors on the next 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 ); + + file.meta = parsedF.meta; + file.hasEditClick = true; + file.parsedData = cleanedEntities; + file.name = newFileName; + file.originFileObj = newFile; + file.originalFileObj = newFile; } - filesWIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults - }); - } else { - filesWOIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults + } + } 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 + ); }); - const newFileName = removeExt(file.name) + `.csv`; + } + 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( - userSchema.userData, - newFileName + newEntities, + file.name ); - file.meta = parsedF.meta; file.hasEditClick = true; file.parsedData = cleanedEntities; - file.name = newFileName; + // 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; }); + 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); } - 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 - ); - - 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); } + } else { + toKeep.push(...cleanedAccepted); } - } 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` - ); + 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 }); } - const cleanedFileList = trimFiles( - [...toKeep, ...fileListToUse], - fileLimit - ); - handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList }); }} {...dropzoneProps} > @@ -990,26 +942,71 @@ const 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 && } */} @@ -1191,9 +1188,12 @@ const UploaderInner = ({ ); -}; +} -const Uploader = observer(UploaderInner); +const Uploader = compose( + connect(undefined, { initializeForm: initialize }), + observer +)(UploaderInner); export default Uploader; diff --git a/packages/ui/src/FormComponents/tryToMatchSchemas.js b/packages/ui/src/FormComponents/tryToMatchSchemas.js index 0d167c0a..46887e7f 100644 --- a/packages/ui/src/FormComponents/tryToMatchSchemas.js +++ b/packages/ui/src/FormComponents/tryToMatchSchemas.js @@ -14,6 +14,12 @@ 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/UploadCsvWizard.js b/packages/ui/src/UploadCsvWizard.js index bf9a7918..92595324 100644 --- a/packages/ui/src/UploadCsvWizard.js +++ b/packages/ui/src/UploadCsvWizard.js @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useRef, useState } from "react"; import { reduxForm, change, formValueSelector, destroy } from "redux-form"; import { Callout, Icon, Intent, Tab, Tabs } from "@blueprintjs/core"; import immer from "immer"; @@ -12,11 +12,10 @@ import { tgFormValueSelector } from "./utils/tgFormValues"; import { some } from "lodash-es"; import { times } from "lodash-es"; import DialogFooter from "./DialogFooter"; -import DataTable from "./DataTable"; -import { removeCleanRows } from "./DataTable/utils"; +import DataTable, { removeCleanRows } from "./DataTable"; import wrapDialog from "./wrapDialog"; import { omit } from "lodash-es"; -import { useDispatch, useSelector } from "react-redux"; +import { connect } from "react-redux"; import { MatchHeaders } from "./MatchHeaders"; import { isEmpty } from "lodash-es"; import { addSpecialPropToAsyncErrs } from "./FormComponents/tryToMatchSchemas"; @@ -36,62 +35,64 @@ 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({ - csvValidationIssue, - doAllFilesHaveSameHeaders, + validateAgainstSchema, + reduxFormEntitiesArray, filesWIssues: _filesWIssues, - flippedMatchedHeaders, - ignoredHeadersMsg, - matchedHeaders, + finishedFiles, onUploadWizardFinish, + doAllFilesHaveSameHeaders, + destroyForms, + csvValidationIssue, + ignoredHeadersMsg, searchResults, + matchedHeaders, userSchema, - validateAgainstSchema + flippedMatchedHeaders, + changeForm }) { - const dispatch = useDispatch(); // will unmount state hook - useEffect(() => { + React.useEffect(() => { return () => { - dispatch( - destroy( - "editableCellTable", - ...times(_filesWIssues.length, i => `editableCellTable-${i}`) - ) + destroyForms( + "editableCellTable", + ...times(_filesWIssues.length, i => `editableCellTable-${i}`) ); }; - }, [_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 - }; - } - }); - + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const [hasSubmittedOuter, setSubmittedOuter] = useState(); const [steps, setSteps] = useState(getInitialSteps(true)); @@ -117,6 +118,7 @@ const UploadCsvWizardDialog = compose( > {filesWIssues.map((f, i) => { const isGood = finishedFiles[i]; + const isThisTheLastBadFile = finishedFiles.every((ff, j) => { if (i === j) { return true; @@ -133,98 +135,108 @@ 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, + 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 ( - 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 (nextUnfinishedFile !== undefined) { + //do async validation here if needed + + const currentEnts = + reduxFormEntitiesArray[focusedTab]; + if ( await asyncValidateHelper( validateAgainstSchema, - ents, + currentEnts, changeForm, - `editableCellTable-${i}` + `editableCellTable-${focusedTab}` ) ) 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 + ); + }) + }); } - //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 + }) }} - 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 - })} /> } - /> + > ); })} @@ -236,27 +248,34 @@ const UploadCsvWizardDialog = compose( comp = ( <> {doAllFilesHaveSameHeaders && ( - + )} {!hasSubmittedOuter && ( { - 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]} + {...{ + doAllFilesHaveSameHeaders, + datatableFormNames: filesWIssues.map((f, i) => { + return `editableCellTable-${i}`; + }), + reduxFormEntitiesArray, + // onMultiFileUploadSubmit, + csvValidationIssue, + ignoredHeadersMsg, + searchResults, + matchedHeaders, + userSchema, + flippedMatchedHeaders, + // reduxFormEntities, + changeForm, + setFilesWIssues, + filesWIssues, + fileIndex: 0, + ...filesWIssues[0] + }} /> )} {hasSubmittedOuter && tabs} @@ -268,200 +287,230 @@ const UploadCsvWizardDialog = compose( setSteps(getInitialSteps(false)); }} text="Review and Edit Data" - /> + > )} ); } - return
{comp}
; + return ( +
+ {comp} +
+ ); } else { return ( ); } }); -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)); - - const { reduxFormEntities, reduxFormCellValidation } = useSelector(state => - formValueSelector(datatableFormName)( - state, - "reduxFormEntities", - "reduxFormCellValidation" - ) +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)); - let inner; - if (hasSubmitted) { - inner = ( - - ); - } else { - inner = ( - - ); - } - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation + let inner; + if (hasSubmitted) { + inner = ( + ); + } else { + inner = ( + + ); + } + const { entsToUse, validationToUse } = removeCleanRows( + reduxFormEntities, + reduxFormCellValidation + ); - 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 + return ( +
+ {!doAllFilesHaveSameHeaders && ( + + )} +
{inner}
+ v)) + } + intent={ + hasSubmitted && onMultiFileUploadSubmit && isThisTheLastBadFile + ? Intent.SUCCESS + : Intent.PRIMARY + } + noCancel={onMultiFileUploadSubmit} + {...(hasSubmitted && { + onBackClick: + onBackClick || + (() => { setSteps( immer(steps, draft => { - draft[0].active = false; - draft[0].completed = true; - draft[1].active = true; + draft[0].active = true; + draft[0].completed = false; + draft[1].active = false; }) ); - setSubmitted(true); - } else { - if (!onMultiFileUploadSubmit) { - //do async validation here if needed - if ( - await asyncValidateHelper( - validateAgainstSchema, - entsToUse, - changeForm, - `editableCellTable` - ) + 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` ) - return; - } - //step 2 submit - const payload = maybeStripIdFromEntities( - entsToUse, - validateAgainstSchema - ); - return onMultiFileUploadSubmit - ? await onMultiFileUploadSubmit() - : onUploadWizardFinish({ res: [payload] }); + ) + return; } - })} - style={{ alignSelf: "end" }} - /> -
- ); - } -); + //step 2 submit + const payload = maybeStripIdFromEntities( + entsToUse, + validateAgainstSchema + ); + return onMultiFileUploadSubmit + ? await onMultiFileUploadSubmit() + : onUploadWizardFinish({ res: [payload] }); + } + })} + style={{ alignSelf: "end" }} + >
+
+ ); +}); export default UploadCsvWizardDialog; const exampleData = { userData: times(5).map(() => ({ _isClean: true })) }; - -export const PreviewCsvData = observer(props => { +export const PreviewCsvData = observer(function (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 && @@ -526,7 +575,7 @@ export const PreviewCsvData = observer(props => { + > )} { initialEntities={(initialEntities ? initialEntities : data) || []} entities={(initialEntities ? initialEntities : data) || []} schema={validateAgainstSchema} - /> + > ); }); @@ -559,12 +608,14 @@ export const SimpleInsertDataDialog = compose( "reduxFormEntities", "reduxFormCellValidation" ), + connect(undefined, { changeForm: change }), observer )(function SimpleInsertDataDialog({ onSimpleInsertDialogFinish, reduxFormEntities, reduxFormCellValidation, validateAgainstSchema, + changeForm, submitting, isEditingExistingFile, matchedHeaders, @@ -574,14 +625,11 @@ export const SimpleInsertDataDialog = compose( userSchema, initialEntities }) { - const dispatch = useDispatch(); const { entsToUse, validationToUse } = removeCleanRows( reduxFormEntities, reduxFormCellValidation ); - const changeForm = (...args) => dispatch(change(...args)); - return ( <>
@@ -594,17 +642,20 @@ export const SimpleInsertDataDialog = compose( label="File Name:" defaultValue={"manual_data_entry"} name="fileName" - /> + > + {...{ + matchedHeaders, + isEditingExistingFile, + showDoesDataLookCorrectMsg, + headerMessage, + // onlyShowRowsWErrors, + validateAgainstSchema, + userSchema, + initialEntities, + datatableFormName: "simpleInsertEditableTable" + }} + >
e)} text={isEditingExistingFile ? "Edit Data" : "Add File"} - /> + > ); }); @@ -664,3 +715,11 @@ 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 ca5addc5..72816cc1 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 + ConnectedPagingTool as PagingTool, + removeCleanRows } from "./DataTable"; -export { removeCleanRows } from "./DataTable/utils"; -export { getIdOrCodeOrIndex } from "./DataTable/utils"; +export { default as getIdOrCodeOrIndex } from "./DataTable/utils/getIdOrCodeOrIndex"; 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 2f1452d4..4d4f8bd9 100644 --- a/packages/ui/src/showDialogOnDocBody.js +++ b/packages/ui/src/showDialogOnDocBody.js @@ -1,4 +1,4 @@ -import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; import React from "react"; // import withDialog from "./enhancers/withDialog"; import { Dialog } from "@blueprintjs/core"; @@ -19,15 +19,19 @@ export default function showDialogOnDocBody(DialogComp, options = {}) { DialogCompToUse = props => { return ( - + ); }; } else { DialogCompToUse = DialogComp; } - const root = createRoot(dialogHolder); - root.render( - + ReactDOM.render( + , + dialogHolder ); } diff --git a/packages/ui/src/useDialog.js b/packages/ui/src/useDialog.js index a9f559c6..38b1f65e 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,14 +31,12 @@ export const useDialog = ({ ModalComponent, ...rest }) => { ...rest?.dialogProps, ...additionalProps?.dialogProps }} - /> + > ); - const toggleDialog = () => { setOpen(!isOpen); }; - - const showDialogPromise = async (handlerName, moreProps = {}) => { + async function showDialogPromise(handlerName, moreProps = {}) { return new Promise(resolve => { //return a promise that can be awaited setAdditionalProps({ @@ -61,7 +59,6 @@ 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 e36f1e14..2ce4b142 100644 --- a/packages/ui/src/utils/renderOnDoc.js +++ b/packages/ui/src/utils/renderOnDoc.js @@ -1,32 +1,29 @@ -import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; 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(() => { - root.unmount(elemDiv); + ReactDOM.unmountComponentAtNode(elemDiv); document.body.removeChild(elemDiv); }); }; - root.render(fn(handleClose)); + return ReactDOM.render(fn(handleClose), elemDiv); } - 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(() => { - root.unmount(); + ReactDOM.unmountComponentAtNode(elemDiv); document.body.removeChild(elemDiv); }); }; + ReactDOM.render(el, elemDiv); return handleClose; }