diff --git a/example-demos/oveViteDemo/src/index.jsx b/example-demos/oveViteDemo/src/index.jsx index 122a5b87..8cf9dcc2 100644 --- a/example-demos/oveViteDemo/src/index.jsx +++ b/example-demos/oveViteDemo/src/index.jsx @@ -1,6 +1,6 @@ import "./shimGlobal"; import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import { Loading } from "@teselagen/ui"; @@ -10,14 +10,16 @@ import App from "./App"; import * as serviceWorker from "./serviceWorker"; -ReactDOM.render( +const domNode = document.getElementById("root"); +const root = createRoot(domNode); + +root.render( - , - document.getElementById("root") + ); // If you want your app to work offline and load faster, you can change diff --git a/example-demos/oveWebpackDemo/src/index.js b/example-demos/oveWebpackDemo/src/index.jsx similarity index 57% rename from example-demos/oveWebpackDemo/src/index.js rename to example-demos/oveWebpackDemo/src/index.jsx index d772e46f..5f969828 100644 --- a/example-demos/oveWebpackDemo/src/index.js +++ b/example-demos/oveWebpackDemo/src/index.jsx @@ -1,15 +1,17 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; import store from "./store"; import "./index.css"; import App from "./App"; -ReactDOM.render( +const domNode = document.getElementById("root"); +const root = createRoot(domNode); + +root.render( - , - document.getElementById("root") + ); diff --git a/helperUtils/renderDemo.js b/helperUtils/renderDemo.js deleted file mode 100644 index e1439ab7..00000000 --- a/helperUtils/renderDemo.js +++ /dev/null @@ -1,2 +0,0 @@ -import { render } from "react-dom"; -export default Demo => render(, document.querySelector("#demo")); diff --git a/helperUtils/renderDemo.jsx b/helperUtils/renderDemo.jsx new file mode 100644 index 00000000..e47a951f --- /dev/null +++ b/helperUtils/renderDemo.jsx @@ -0,0 +1,3 @@ +import { createRoot } from "react-dom/client"; +const root = createRoot(document.querySelector("#demo")); +export default Demo => root.render(); diff --git a/packages/ove/demo/src/index.js b/packages/ove/demo/src/index.jsx similarity index 93% rename from packages/ove/demo/src/index.js rename to packages/ove/demo/src/index.jsx index 86c90e23..c016bbbd 100644 --- a/packages/ove/demo/src/index.js +++ b/packages/ove/demo/src/index.jsx @@ -1,10 +1,6 @@ import React, { useMemo } from "react"; import { Provider } from "react-redux"; - import store from "./store"; -// import { createRoot } from "react-dom/client"; -import { render } from "react-dom"; - import { CircularView, RowView, @@ -13,7 +9,6 @@ import { updateEditor, EnzymeViewer } from "../../src"; - import exampleSequenceData from "./exampleData/exampleSequenceData"; import StandaloneDemo from "./StandaloneDemo"; import SimpleCircularOrLinearViewDemo from "./SimpleCircularOrLinearViewDemo"; @@ -21,10 +16,10 @@ import StandaloneAlignmentDemo from "./StandaloneAlignmentDemo"; import AlignmentDemo from "./AlignmentDemo"; import EditorDemo from "./EditorDemo"; import VersionHistoryViewDemo from "./VersionHistoryViewDemo"; - import "./style.css"; // eslint-disable-next-line @nx/enforce-module-boundaries import { DemoPage } from "@teselagen/shared-demo"; +import { createRoot } from "react-dom/client"; const demos = { Editor: { @@ -131,5 +126,5 @@ const WrapSimpleDemo = ({ children }) => { return children; }; -// createRoot(document.querySelector("#demo")).render(); -render(, document.querySelector("#demo")); +const root = createRoot(document.querySelector("#demo")); +root.render(); diff --git a/packages/ove/src/AlignmentView/index.js b/packages/ove/src/AlignmentView/index.js index 9b58635d..f632b59b 100644 --- a/packages/ove/src/AlignmentView/index.js +++ b/packages/ove/src/AlignmentView/index.js @@ -5,7 +5,7 @@ import { Draggable as DndDraggable } from "@hello-pangea/dnd"; import Clipboard from "clipboard"; -import React from "react"; +import React, { createRef } from "react"; import { connect } from "react-redux"; import { Button, @@ -33,7 +33,6 @@ import { } from "lodash-es"; import { getSequenceDataBetweenRange } from "@teselagen/sequence-utils"; import ReactList from "@teselagen/react-list"; -import ReactDOM from "react-dom"; import { NonReduxEnhancedLinearView } from "../LinearView"; import Minimap, { getTrimmedRangesToDisplay } from "./Minimap"; @@ -94,6 +93,7 @@ try { export class AlignmentView extends React.Component { bindOutsideChangeHelper = {}; + InfiniteScroller = createRef(); constructor(props) { super(props); window.scrollAlignmentToPercent = this.scrollAlignmentToPercent; @@ -389,11 +389,12 @@ export class AlignmentView extends React.Component { setVerticalScrollRange = throttle(() => { if ( this && - this.InfiniteScroller && - this.InfiniteScroller.getFractionalVisibleRange && + this.InfiniteScroller.current && + this.InfiniteScroller.current.getFractionalVisibleRange && this.easyStore ) { - let [start, end] = this.InfiniteScroller.getFractionalVisibleRange(); + let [start, end] = + this.InfiniteScroller.current.getFractionalVisibleRange(); if (this.props.hasTemplate) { end = end + 1; } @@ -495,7 +496,7 @@ export class AlignmentView extends React.Component { } }; scrollYToTrack = trackIndex => { - this.InfiniteScroller.scrollTo(trackIndex); + this.InfiniteScroller.current.scrollTo(trackIndex); }; estimateRowHeight = (index, cache) => { @@ -1328,8 +1329,8 @@ export class AlignmentView extends React.Component { ) : ( { - this.InfiniteScroller = c; - const domNode = ReactDOM.findDOMNode(c); + this.InfiniteScroller(c); + const domNode = this.InfiniteScroller.current; if (domNode instanceof HTMLElement) { drop_provided.innerRef(domNode); } diff --git a/packages/ove/src/Reflex/ReflexContainer.js b/packages/ove/src/Reflex/ReflexContainer.js index 663adebb..4e31de11 100644 --- a/packages/ove/src/Reflex/ReflexContainer.js +++ b/packages/ove/src/Reflex/ReflexContainer.js @@ -1,798 +1,584 @@ -/////////////////////////////////////////////////////////// // ReflexContainer // By Philippe Leefsma // December 2016 -// -/////////////////////////////////////////////////////////// +import React, { + Children, + useEffect, + useMemo, + useRef, + useState, + cloneElement, + useCallback +} from "react"; import ReflexSplitter from "./ReflexSplitter"; import ReflexEvents from "./ReflexEvents"; import PropTypes from "prop-types"; -import ReactDOM from "react-dom"; -import React from "react"; import { cloneDeep, round } from "lodash-es"; -class ReflexContainer extends React.Component { - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// - constructor(props) { - super(props); - - this.state = { - flexData: [] - }; - - this.events = new ReflexEvents(); - - this.onSplitterStartResize = this.onSplitterStartResize.bind(this); - - this.onSplitterStopResize = this.onSplitterStopResize.bind(this); - - this.onSplitterResize = this.onSplitterResize.bind(this); - - this.onElementSize = this.onElementSize.bind(this); - - this.children = []; - } - - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// - setPartialState(partialState) { - return new Promise(resolve => { - this.setState(Object.assign({}, this.state, partialState), () => { - resolve(); - }); - }); - } - - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// - componentDidMount() { - const flexData = this.computeFlexData(); - - this.setPartialState({ - flexData - }); - - this.events.on("splitter.startResize", this.onSplitterStartResize); - - this.events.on("splitter.stopResize", this.onSplitterStopResize); - - this.events.on("splitter.resize", this.onSplitterResize); - - this.events.on("element.size", this.onElementSize); - } - - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// - componentWillUnmount() { - this.events.off(); - } +// Determines if element is a splitter or wraps a splitter +const isSplitterElement = element => { + //https://github.com/leefsmp/Re-Flex/issues/49 + return process.env.NODE_ENV === "development" + ? element.type === React.createElement(ReflexSplitter).type + : element.type === ReflexSplitter; +}; - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// - getValidChildren(props = this.props) { - return this.toArray(props.children).filter(child => { - return !!child; - }); - } - - ///////////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////////// - UNSAFE_componentWillReceiveProps(props) { - const children = this.getValidChildren(props); - - if ( - children.length !== this.state.flexData.length || - this.flexHasChanged(props) - ) { - const flexData = this.computeFlexData(children); - - this.setPartialState({ - flexData - }); +const computeFreeFlex = flexData => { + return flexData.reduce((sum, entry) => { + if (!isSplitterElement(entry) && entry.constrained) { + return sum - entry.flex; } - } - - ///////////////////////////////////////////////////////// - // Check if flex has changed: this allows updating the - // component when different flex is passed as property - // to one or several children - // - ///////////////////////////////////////////////////////// - flexHasChanged(props) { - const nextChildrenFlex = this.getValidChildren(props).map(child => { - return child.props.flex || 0; - }); - - const childrenFlex = this.getValidChildren().map(child => { - return child.props.flex || 0; - }); - - return !childrenFlex.every((flex, idx) => { - return flex === nextChildrenFlex[idx]; - }); - } - - ///////////////////////////////////////////////////////// - // Returns size of a ReflexElement - // - ///////////////////////////////////////////////////////// - getSize(element) { - const ref = element.ref ? this.refs[element.ref] : element; - - const domElement = ReactDOM.findDOMNode(ref); - - switch (this.props.orientation) { - case "horizontal": - return domElement.offsetHeight; + return sum; + }, 1); +}; - case "vertical": - default: - return domElement.offsetWidth; +const computeFreeElements = flexData => { + return flexData.reduce((sum, entry) => { + if (!isSplitterElement(entry) && !entry.constrained) { + return sum + 1; } - } + return sum; + }, 0); +}; - ///////////////////////////////////////////////////////// - // Computes offset from pointer position - // - ///////////////////////////////////////////////////////// - getOffset(event) { - const pos = event.changedTouches ? event.changedTouches[0] : event; +// Utility method to ensure given argument is returned as an array +const toArray = obj => { + return obj ? (Array.isArray(obj) ? obj : [obj]) : []; +}; - switch (this.props.orientation) { - case "horizontal": - return pos.pageY - this.previousPos; +const ReflexContainer = ({ + className: _className, + children: _children, + onPanelCollapse, + orientation, + style +}) => { + const divRef = useRef(null); + const validChildren = useMemo( + () => toArray(_children).filter(child => child), + [_children] + ); + const events = useMemo(() => new ReflexEvents(), []); + const previousPos = useRef(0); + const hasCollapsed = useRef(false); + const elements = useRef(null); + const isNegativeWhenCollapsing = useRef(false); + const flexDataBeforeCollapse = useRef(null); + const refMap = useRef({}); - case "vertical": - default: - return pos.pageX - this.previousPos; - } - } + // Returns flex value for unit pixel + const computePixelFlex = useCallback(() => { + const domElement = divRef.current; - ///////////////////////////////////////////////////////// - // Handles splitter startResize event - // - ///////////////////////////////////////////////////////// - onSplitterStartResize(data) { - const pos = data.event.changedTouches - ? data.event.changedTouches[0] - : data.event; - - switch (this.props.orientation) { + switch (orientation) { case "horizontal": - document.body.style.cursor = "row-resize"; - this.previousPos = pos.pageY; - break; + if (domElement.offsetHeight === 0.0) { + console.warn( + "Found ReflexContainer with height=0, " + + "this will cause invalid behavior..." + ); + console.warn(domElement); + return 0.0; + } - case "vertical": + return 1.0 / domElement.offsetHeight; default: - document.body.style.cursor = "col-resize"; - this.previousPos = pos.pageX; - break; - } + if (domElement.offsetWidth === 0.0) { + console.warn( + "Found ReflexContainer with width=0, " + + "this will cause invalid behavior..." + ); + console.warn(domElement); + return 0.0; + } - const idx = data.splitter.props.index; + return 1.0 / domElement.offsetWidth; + } + }, [orientation]); + + const [flexData, setFlexData] = useState([]); + + useEffect(() => { + // Computes initial flex data based on provided flex + // properties. By default each ReflexElement gets + // evenly arranged within its container + const computeFlexData = () => { + const pixelFlex = computePixelFlex(); + const flexDataInit = validChildren.map(child => { + const props = child.props; + return { + maxFlex: (props.maxSize || Number.MAX_VALUE) * pixelFlex, + sizeFlex: (props.size || Number.MAX_VALUE) * pixelFlex, + minFlex: (props.minSize || 1) * pixelFlex, + constrained: props.flex !== undefined, + flex: props.flex || 0, + type: child.type + }; + }); - this.elements = [this.children[idx - 1], this.children[idx + 1]]; + const computeFlexDataRec = flexDataIn => { + let hasContrain = false; + const freeElements = computeFreeElements(flexDataIn); + const freeFlex = computeFreeFlex(flexDataIn); + const flexDataOut = flexDataIn.map(entry => { + if (isSplitterElement(entry)) { + return entry; + } + const proposedFlex = !entry.constrained + ? freeFlex / freeElements + : entry.flex; + const constrainedFlex = Math.min( + entry.sizeFlex, + Math.min(entry.maxFlex, Math.max(entry.minFlex, proposedFlex)) + ); + const constrained = constrainedFlex !== proposedFlex; + hasContrain = hasContrain || constrained; + return Object.assign({}, entry, { + flex: constrainedFlex, + constrained + }); + }); + return hasContrain ? computeFlexDataRec(flexDataOut) : flexDataOut; + }; - this.emitElementsEvent(this.elements, "onStartResize"); - } + const flexData = computeFlexDataRec(flexDataInit); + return flexData.map(entry => { + return { + flex: !isSplitterElement(entry) ? entry.flex : 0.0 + }; + }); + }; + setFlexData(computeFlexData()); + }, [computePixelFlex, validChildren]); - ///////////////////////////////////////////////////////// - // Handles splitter resize event - // - ///////////////////////////////////////////////////////// - onSplitterResize(data) { - const idx = data.splitter.props.index; + const children = useMemo( + () => + Children.map(validChildren, (child, idx) => { + if (idx > flexData.length - 1) { + return
; + } - const offset = this.getOffset(data.event); + const newProps = Object.assign({}, child.props, { + maxSize: child.props.maxSize || Number.MAX_VALUE, + orientation: orientation, + minSize: child.props.minSize || 1, + events: events, + //tnr: this rounding is necessary because flex was getting computed very slightly + // off (eg -1.423432e-17). This corrects for that + flex: round(flexData[idx].flex, 5), + ref: n => (refMap.current[idx] = n), + index: idx + }); - let availableOffset = this.computeAvailableOffset(idx, offset); + return cloneElement(child, newProps); + }), + [events, flexData, orientation, validChildren] + ); - if (this.hasCollapsed) { - if (this.isNegativeWhenCollapsing ? offset > 0 : offset < 0) { - this.hasCollapsed = false; - this.setPartialState(this.stateBeforeCollapse).then(() => { - this.emitElementsEvent(this.elements, "onResize"); + // Emits given if event for each given element if present in the component props + const emitElementsEvent = (selectedElements, event) => { + toArray(selectedElements).forEach(element => { + if (element.props[event]) { + const ref = refMap.current[element.index]; + selectedElements.props[event]({ + domElement: ref, + component: element }); } - return; - } + }); + }; - if (!availableOffset) { - this.closeThreshold = 40; - const shrink = this.computeAvailableShrink(idx, offset); - - if (shrink === 0 && Math.abs(offset) > this.closeThreshold) { - const childIdx = offset > 0 ? idx + 1 : idx - 1; - - const child = this.children[childIdx]; - const size = this.getSize(child); - this.stateBeforeCollapse = cloneDeep(this.state); - this.isNegativeWhenCollapsing = offset < 0; - availableOffset = size * (offset > 0 ? 1 : -1); - this.hasCollapsed = child.props; - this.elements = this.dispatchOffset(idx, availableOffset); - this.adjustFlex(this.elements); - this.setPartialState(this.state).then(() => { - this.emitElementsEvent(this.elements, "onResize"); - }); - } - } else if (availableOffset) { + // Handles splitter startResize event + const onSplitterStartResize = useCallback( + data => { const pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event; - - switch (this.props.orientation) { + switch (orientation) { case "horizontal": - this.previousPos = pos.pageY; + document.body.style.cursor = "row-resize"; + previousPos.current = pos.pageY; break; - - case "vertical": default: - this.previousPos = pos.pageX; + document.body.style.cursor = "col-resize"; + previousPos.current = pos.pageX; break; } + const idx = data.splitter.props.index; + elements.current = [children[idx - 1], children[idx + 1]]; + emitElementsEvent(elements.current, "onStartResize"); + }, + [children, orientation] + ); - this.elements = this.dispatchOffset(idx, availableOffset); - - this.adjustFlex(this.elements); - this.setPartialState(this.state).then(() => { - this.emitElementsEvent(this.elements, "onResize"); - }); - } - } - - ///////////////////////////////////////////////////////// - // Determines if element is a splitter - // or wraps a splitter - // - ///////////////////////////////////////////////////////// - isSplitterElement(element) { - //https://github.com/leefsmp/Re-Flex/issues/49 - return process.env.NODE_ENV === "development" - ? element.type === React.createElement(ReflexSplitter).type - : element.type === ReflexSplitter; - } - - ///////////////////////////////////////////////////////// // Handles splitter stopResize event - // - ///////////////////////////////////////////////////////// - onSplitterStopResize() { - if (this.hasCollapsed) { - this.props.onPanelCollapse && - this.props.onPanelCollapse(this.hasCollapsed); - this.hasCollapsed = false; + const onSplitterStopResize = useCallback(() => { + if (hasCollapsed.current) { + onPanelCollapse && onPanelCollapse(hasCollapsed.current); + hasCollapsed.current = false; } - document.body.style.cursor = "auto"; - - const resizedRefs = this.elements.map(element => { + const resizedRefs = elements.current.map(element => { return element.ref; }); - - const elements = this.children.filter(child => { - return !this.isSplitterElement(child) && resizedRefs.includes(child.ref); + elements.current = children.filter(child => { + return !isSplitterElement(child) && resizedRefs.includes(child.ref); }); + emitElementsEvent(elements.current, "onStopResize"); + }, [children, onPanelCollapse]); - this.emitElementsEvent(elements, "onStopResize"); - } - - ///////////////////////////////////////////////////////// - // Handles element size modified event - // - ///////////////////////////////////////////////////////// - onElementSize(data) { - return new Promise(resolve => { - try { - const idx = data.element.props.index; - - const size = this.getSize(this.children[idx]); - - const offset = data.size - size; - - const dir = data.direction; - - const splitterIdx = idx + dir; - - const availableOffset = this.computeAvailableOffset( - splitterIdx, - dir * offset - ); - - this.elements = null; - - if (availableOffset) { - this.elements = this.dispatchOffset(splitterIdx, availableOffset); - - this.adjustFlex(this.elements); - } - - this.setPartialState(this.state).then(() => { - this.emitElementsEvent(this.elements, "onResize"); - - resolve(); - }); - } catch (ex) { - // TODO handle exception ... - } - }); - } - - ///////////////////////////////////////////////////////// - // Adjusts flex after a dispatch to make sure - // total flex of modified elements remains the same - // - ///////////////////////////////////////////////////////// - adjustFlex(elements) { - const diffFlex = elements.reduce((sum, element) => { - const idx = element.props.index; - - const previousFlex = element.props.flex; - - const nextFlex = this.state.flexData[idx].flex; - - return sum + (previousFlex - nextFlex) / elements.length; - }, 0); - - elements.forEach(element => { - // eslint-disable-next-line react/no-direct-mutation-state - this.state.flexData[element.props.index].flex += diffFlex; - }); - } - - ///////////////////////////////////////////////////////// - // Returns available offset for a given raw offset value - // This checks how much the panes can be stretched and - // shrink, then returns the min - // - ///////////////////////////////////////////////////////// - computeAvailableOffset(idx, offset) { - const stretch = this.computeAvailableStretch(idx, offset); - - const shrink = this.computeAvailableShrink(idx, offset); - - const availableOffset = Math.min(stretch, shrink) * Math.sign(offset); - - return availableOffset; - } - - ///////////////////////////////////////////////////////// - // Returns true if the next splitter than the one at idx - // can propagate the drag. This can happen if that - // next element is actually a splitter and it has - // propagate=true property set - // - ///////////////////////////////////////////////////////// - checkPropagate(idx, direction) { - if (direction > 0) { - if (idx < this.children.length - 2) { - const child = this.children[idx + 2]; - - const typeCheck = this.isSplitterElement(child); - - return typeCheck && child.props.propagate; - } - } else { - if (idx > 2) { - const child = this.children[idx - 2]; - - const typeCheck = this.isSplitterElement(child); + // Computes offset from pointer position + const getOffset = useCallback( + event => { + const pos = event.changedTouches ? event.changedTouches[0] : event; + switch (orientation) { + case "horizontal": + return pos.pageY - previousPos.current; - return typeCheck && child.props.propagate; + case "vertical": + default: + return pos.pageX - previousPos.current; } - } - - return false; - } - - ///////////////////////////////////////////////////////// - // Recursively computes available stretch at splitter - // idx for given raw offset - // - ///////////////////////////////////////////////////////// - computeAvailableStretch(idx, offset) { - const childIdx = offset < 0 ? idx + 1 : idx - 1; + }, + [orientation] + ); - const child = this.children[childIdx]; - - const size = this.getSize(child); - - const maxSize = child.props.maxSize; - - const availableStretch = maxSize - size; - - if (availableStretch < Math.abs(offset)) { - if (this.checkPropagate(idx, -1 * offset)) { - const nextOffset = - Math.sign(offset) * (Math.abs(offset) - availableStretch); - - return ( - availableStretch + - this.computeAvailableStretch( - offset < 0 ? idx + 2 : idx - 2, - nextOffset - ) - ); + // Returns size of a ReflexElement + const getSize = useCallback( + element => { + const domElement = refMap.current(element.index); + switch (orientation) { + case "horizontal": + return domElement.offsetHeight; + default: + return domElement.offsetWidth; } - } - - return Math.min(availableStretch, Math.abs(offset)); - } - - ///////////////////////////////////////////////////////// - // Recursively computes available shrink at splitter - // idx for given raw offset - // - ///////////////////////////////////////////////////////// - computeAvailableShrink(idx, offset) { - const childIdx = offset > 0 ? idx + 1 : idx - 1; - - const child = this.children[childIdx]; - - const size = this.getSize(child); - - const minSize = Math.max(child.props.minSize, 0); - - const availableShrink = size - minSize; - - if (availableShrink < Math.abs(offset)) { - if (this.checkPropagate(idx, offset)) { - const nextOffset = - Math.sign(offset) * (Math.abs(offset) - availableShrink); - - return ( - availableShrink + - this.computeAvailableShrink( - offset > 0 ? idx + 2 : idx - 2, - nextOffset - ) - ); + }, + [orientation] + ); + + // Returns true if the next splitter than the one at idx can propagate + // the drag. This can happen if that next element is actually a + // splitter and it has propagate=true property set + const checkPropagate = useCallback( + (idx, direction) => { + if (direction > 0) { + if (idx < children.length - 2) { + const child = children[idx + 2]; + const typeCheck = isSplitterElement(child); + return typeCheck && child.props.propagate; + } + } else { + if (idx > 2) { + const child = children[idx - 2]; + const typeCheck = isSplitterElement(child); + return typeCheck && child.props.propagate; + } } - } - - return Math.min(availableShrink, Math.abs(offset)); - } - - ///////////////////////////////////////////////////////// - // Returns flex value for unit pixel - // - ///////////////////////////////////////////////////////// - computePixelFlex() { - const domElement = ReactDOM.findDOMNode(this); - - switch (this.props.orientation) { - case "horizontal": - if (domElement.offsetHeight === 0.0) { - console.warn( - "Found ReflexContainer with height=0, " + - "this will cause invalid behavior..." + return false; + }, + [children] + ); + + // Recursively computes available stretch at splitter idx for given raw offset + const computeAvailableStretch = useCallback( + (idx, offset) => { + const childIdx = offset < 0 ? idx + 1 : idx - 1; + const child = children[childIdx]; + const size = getSize(child); + const maxSize = child.props.maxSize; + const availableStretch = maxSize - size; + if (availableStretch < Math.abs(offset)) { + if (checkPropagate(idx, -1 * offset)) { + const nextOffset = + Math.sign(offset) * (Math.abs(offset) - availableStretch); + + return ( + availableStretch + + computeAvailableStretch(offset < 0 ? idx + 2 : idx - 2, nextOffset) ); - console.warn(domElement); - return 0.0; } + } - return 1.0 / domElement.offsetHeight; - - case "vertical": - default: - if (domElement.offsetWidth === 0.0) { - console.warn( - "Found ReflexContainer with width=0, " + - "this will cause invalid behavior..." + return Math.min(availableStretch, Math.abs(offset)); + }, + [checkPropagate, children, getSize] + ); + + // Recursively computes available shrink at splitter idx for given raw offset + const computeAvailableShrink = useCallback( + (idx, offset) => { + const childIdx = offset > 0 ? idx + 1 : idx - 1; + const child = children[childIdx]; + const size = getSize(child); + const minSize = Math.max(child.props.minSize, 0); + const availableShrink = size - minSize; + if (availableShrink < Math.abs(offset)) { + if (checkPropagate(idx, offset)) { + const nextOffset = + Math.sign(offset) * (Math.abs(offset) - availableShrink); + + return ( + availableShrink + + computeAvailableShrink(offset > 0 ? idx + 2 : idx - 2, nextOffset) ); - console.warn(domElement); - return 0.0; } + } - return 1.0 / domElement.offsetWidth; - } - } + return Math.min(availableShrink, Math.abs(offset)); + }, + [checkPropagate, children, getSize] + ); + + // Returns available offset for a given raw offset value. This checks how much + // the panes can be stretched and shrink, then returns the min + const computeAvailableOffset = useCallback( + (idx, offset) => { + const stretch = computeAvailableStretch(idx, offset); + const shrink = computeAvailableShrink(idx, offset); + const availableOffset = Math.min(stretch, shrink) * Math.sign(offset); + return availableOffset; + }, + [computeAvailableShrink, computeAvailableStretch] + ); - ///////////////////////////////////////////////////////// // Adds offset to a given ReflexElement - // - ///////////////////////////////////////////////////////// - addOffset(element, offset) { - const size = this.getSize(element); - - const idx = element.props.index; - - const newSize = Math.max(size + offset, 0); - - const currentFlex = this.state.flexData[idx].flex; - - const newFlex = - currentFlex > 0 - ? (currentFlex * newSize) / size - : this.computePixelFlex() * newSize; - - // eslint-disable-next-line react/no-direct-mutation-state - this.state.flexData[idx].flex = - !isFinite(newFlex) || isNaN(newFlex) ? 0 : newFlex; - } + const addOffset = useCallback( + (element, offset) => { + const size = getSize(element); + const idx = element.props.index; + const newSize = Math.max(size + offset, 0); + const currentFlex = flexData[idx].flex; + const newFlex = + currentFlex > 0 + ? (currentFlex * newSize) / size + : computePixelFlex() * newSize; + + setFlexData(prev => { + const newFlexData = cloneDeep(prev); + newFlexData[idx].flex = + !isFinite(newFlex) || isNaN(newFlex) ? 0 : newFlex; + return newFlexData; + }); + }, + [computePixelFlex, flexData, getSize] + ); - ///////////////////////////////////////////////////////// // Recursively dispatches stretch offset across // children elements starting at splitter idx - // - ///////////////////////////////////////////////////////// - dispatchStretch(idx, offset) { - const childIdx = offset < 0 ? idx + 1 : idx - 1; - - if (childIdx < 0 || childIdx > this.children.length - 1) { - return []; - } - - const child = this.children[childIdx]; - - const size = this.getSize(child); - - const newSize = Math.min(child.props.maxSize, size + Math.abs(offset)); - - const dispatchedStretch = newSize - size; - - this.addOffset(child, dispatchedStretch); - - if (dispatchedStretch < Math.abs(offset)) { - const nextIdx = idx - Math.sign(offset) * 2; - - const nextOffset = - Math.sign(offset) * (Math.abs(offset) - dispatchedStretch); - - return [child, ...this.dispatchStretch(nextIdx, nextOffset)]; - } - - return [child]; - } - - ///////////////////////////////////////////////////////// - // Recursively dispatches shrink offset across - // children elements starting at splitter idx - // - ///////////////////////////////////////////////////////// - dispatchShrink(idx, offset) { - const childIdx = offset > 0 ? idx + 1 : idx - 1; - - if (childIdx < 0 || childIdx > this.children.length - 1) { - return []; - } - - const child = this.children[childIdx]; - - const size = this.getSize(child); - - const newSize = Math.max( - // child.props.minSize, //tnr: not sure what to do about this.. I had to comment this out to make the collapsing work - size - Math.abs(offset) - ); - - const dispatchedShrink = newSize - size; - - this.addOffset(child, dispatchedShrink); - - if (Math.abs(dispatchedShrink) < Math.abs(offset)) { - const nextIdx = idx + Math.sign(offset) * 2; + const dispatchStretch = useCallback( + (idx, offset) => { + const childIdx = offset < 0 ? idx + 1 : idx - 1; + if (childIdx < 0 || childIdx > children.length - 1) { + return []; + } + const child = children[childIdx]; + const size = getSize(child); + const newSize = Math.min(child.props.maxSize, size + Math.abs(offset)); + const dispatchedStretch = newSize - size; + addOffset(child, dispatchedStretch); + + if (dispatchedStretch < Math.abs(offset)) { + const nextIdx = idx - Math.sign(offset) * 2; + const nextOffset = + Math.sign(offset) * (Math.abs(offset) - dispatchedStretch); + return [child, ...dispatchStretch(nextIdx, nextOffset)]; + } + return [child]; + }, + [addOffset, children, getSize] + ); + + // Recursively dispatches shrink offset across children elements + // starting at splitter idx + const dispatchShrink = useCallback( + (idx, offset) => { + const childIdx = offset > 0 ? idx + 1 : idx - 1; + + if (childIdx < 0 || childIdx > children.length - 1) { + return []; + } - const nextOffset = - Math.sign(offset) * (Math.abs(offset) + dispatchedShrink); + const child = children[childIdx]; + const size = getSize(child); + const newSize = Math.max( + // child.props.minSize, //tnr: not sure what to do about this.. I had to comment this out to make the collapsing work + size - Math.abs(offset) + ); - return [child, ...this.dispatchShrink(nextIdx, nextOffset)]; - } + const dispatchedShrink = newSize - size; + addOffset(child, dispatchedShrink); - return [child]; - } + if (Math.abs(dispatchedShrink) < Math.abs(offset)) { + const nextIdx = idx + Math.sign(offset) * 2; + const nextOffset = + Math.sign(offset) * (Math.abs(offset) + dispatchedShrink); + return [child, ...dispatchShrink(nextIdx, nextOffset)]; + } + return [child]; + }, + [addOffset, children, getSize] + ); - ///////////////////////////////////////////////////////// // Dispatch offset at splitter idx - // - ///////////////////////////////////////////////////////// - dispatchOffset(idx, offset) { - return [ - ...this.dispatchStretch(idx, offset), - ...this.dispatchShrink(idx, offset) - ]; - } - - ///////////////////////////////////////////////////////// - // Emits given if event for each given element - // if present in the component props - // - ///////////////////////////////////////////////////////// - emitElementsEvent(elements, event) { - this.toArray(elements).forEach(element => { - if (element.props[event]) { - const ref = this.refs[element.ref]; + const dispatchOffset = useCallback( + (idx, offset) => { + return [...dispatchStretch(idx, offset), ...dispatchShrink(idx, offset)]; + }, + [dispatchShrink, dispatchStretch] + ); - element.props[event]({ - domElement: ReactDOM.findDOMNode(ref), - component: element + // Adjusts flex after a dispatch to make sure + // total flex of modified elements remains the same + const adjustFlex = useCallback( + elements => { + const diffFlex = elements.reduce((sum, element) => { + const idx = element.props.index; + const previousFlex = element.props.flex; + const nextFlex = flexData[idx].flex; + return sum + (previousFlex - nextFlex) / elements.length; + }, 0); + setFlexData(prev => { + const newFlexData = cloneDeep(prev); + elements.forEach(element => { + newFlexData[element.props.index].flex += diffFlex; }); - } - }); - } - - ///////////////////////////////////////////////////////// - // Computes initial flex data based on provided flex - // properties. By default each ReflexElement gets - // evenly arranged within its container - // - ///////////////////////////////////////////////////////// - computeFlexData(children = this.getValidChildren()) { - const pixelFlex = this.computePixelFlex(); - - const computeFreeFlex = flexData => { - return flexData.reduce((sum, entry) => { - if (!this.isSplitterElement(entry) && entry.constrained) { - return sum - entry.flex; - } - return sum; - }, 1); - }; + return newFlexData; + }); + }, + [flexData] + ); - const computeFreeElements = flexData => { - return flexData.reduce((sum, entry) => { - if (!this.isSplitterElement(entry) && !entry.constrained) { - return sum + 1; + // Handles splitter resize event + const onSplitterResize = useCallback( + data => { + const idx = data.splitter.props.index; + + const offset = getOffset(data.event); + let availableOffset = computeAvailableOffset(idx, offset); + if (hasCollapsed.current) { + if (isNegativeWhenCollapsing.current ? offset > 0 : offset < 0) { + hasCollapsed.current = false; + setFlexData(flexDataBeforeCollapse.current).then(() => { + emitElementsEvent(elements.current, "onResize"); + }); } - return sum; - }, 0); - }; - - const flexDataInit = children.map(child => { - const props = child.props; - - return { - maxFlex: (props.maxSize || Number.MAX_VALUE) * pixelFlex, - sizeFlex: (props.size || Number.MAX_VALUE) * pixelFlex, - minFlex: (props.minSize || 1) * pixelFlex, - constrained: props.flex !== undefined, - guid: props.ref || this.guid(), - flex: props.flex || 0, - type: child.type - }; - }); - - const computeFlexDataRec = flexDataIn => { - let hasContrain = false; - - const freeElements = computeFreeElements(flexDataIn); - - const freeFlex = computeFreeFlex(flexDataIn); + return; + } - const flexDataOut = flexDataIn.map(entry => { - if (this.isSplitterElement(entry)) { - return entry; + if (!availableOffset) { + const closeThreshold = 40; + const shrink = computeAvailableShrink(idx, offset); + + if (shrink === 0 && Math.abs(offset) > closeThreshold) { + const childIdx = offset > 0 ? idx + 1 : idx - 1; + + const child = children[childIdx]; + const size = getSize(child); + flexDataBeforeCollapse.current = cloneDeep(flexData); + isNegativeWhenCollapsing.current = offset < 0; + availableOffset = size * (offset > 0 ? 1 : -1); + hasCollapsed.current = child.props; + elements.current = dispatchOffset(idx, availableOffset); + adjustFlex(elements.current); + emitElementsEvent(elements.current, "onResize"); + } + } else if (availableOffset) { + const pos = data.event.changedTouches + ? data.event.changedTouches[0] + : data.event; + + switch (orientation) { + case "horizontal": + previousPos.current = pos.pageY; + break; + default: + previousPos.current = pos.pageX; + break; } - const proposedFlex = !entry.constrained - ? freeFlex / freeElements - : entry.flex; - - const constrainedFlex = Math.min( - entry.sizeFlex, - Math.min(entry.maxFlex, Math.max(entry.minFlex, proposedFlex)) - ); - - const constrained = constrainedFlex !== proposedFlex; - - hasContrain = hasContrain || constrained; - - return Object.assign({}, entry, { - flex: constrainedFlex, - constrained - }); - }); + elements.current = this.dispatchOffset(idx, availableOffset); - return hasContrain ? computeFlexDataRec(flexDataOut) : flexDataOut; - }; + adjustFlex(elements.current); + emitElementsEvent(elements.current, "onResize"); + } + }, + [ + adjustFlex, + children, + computeAvailableOffset, + computeAvailableShrink, + dispatchOffset, + flexData, + getOffset, + getSize, + orientation + ] + ); - const flexData = computeFlexDataRec(flexDataInit); + // Handles element size modified event + const onElementSize = useCallback( + data => { + return new Promise(resolve => { + try { + const idx = data.element.props.index; + const size = getSize(children[idx]); + const offset = data.size - size; + const dir = data.direction; + const splitterIdx = idx + dir; + const availableOffset = computeAvailableOffset( + splitterIdx, + dir * offset + ); + elements.current = null; + if (availableOffset) { + elements.current = dispatchOffset(splitterIdx, availableOffset); + adjustFlex(elements.current); + } - return flexData.map(entry => { - return { - flex: !this.isSplitterElement(entry) ? entry.flex : 0.0, - guid: entry.guid - }; - }); - } - - ///////////////////////////////////////////////////////// - // Utility method that generates a new unique GUID - // - ///////////////////////////////////////////////////////// - guid(format = "xxxx-xxxx") { - let d = new Date().getTime(); - - return format.replace(/[xy]/g, function (c) { - const r = (d + Math.random() * 16) % 16 | 0; - d = Math.floor(d / 16); - // eslint-disable-next-line eqeqeq - return (c == "x" ? r : (r & 0x7) | 0x8).toString(16); - }); - } - - ///////////////////////////////////////////////////////// - // Utility method to ensure given argument is - // returned as an array - // - ///////////////////////////////////////////////////////// - toArray(obj) { - return obj ? (Array.isArray(obj) ? obj : [obj]) : []; - } - - ///////////////////////////////////////////////////////// - // Render container. This will clone all original child - // components in order to pass some internal properties - // used to handle resizing logic - // - ///////////////////////////////////////////////////////// - render() { - const classNames = [ - "reflex-layout", - "reflex-container", - this.props.orientation, - ...this.props.className.split(" ") - ]; - - this.children = React.Children.map( - this.getValidChildren(), - (child, idx) => { - if (idx > this.state.flexData.length - 1) { - return
; + emitElementsEvent(elements.current, "onResize"); + resolve(); + } catch (error) { + console.error(error); } + }); + }, + [adjustFlex, children, computeAvailableOffset, dispatchOffset, getSize] + ); + + useEffect(() => { + events.on("splitter.startResize", onSplitterStartResize); + events.on("splitter.stopResize", onSplitterStopResize); + events.on("splitter.resize", onSplitterResize); + events.on("element.size", onElementSize); + }, [ + events, + onElementSize, + onSplitterResize, + onSplitterStartResize, + onSplitterStopResize + ]); + + const classNames = [ + "reflex-layout", + "reflex-container", + orientation, + ..._className.split(" ") + ]; + + return ( +
+ {children} +
+ ); +}; - const flexData = this.state.flexData[idx]; - - const newProps = Object.assign({}, child.props, { - maxSize: child.props.maxSize || Number.MAX_VALUE, - orientation: this.props.orientation, - minSize: child.props.minSize || 1, - events: this.events, - flex: round(flexData.flex, 5), //tnr: this rounding is necessary because flex was getting computed very slightly off (eg -1.423432e-17). This corrects for that - ref: flexData.guid, - index: idx - }); - - return React.cloneElement(child, newProps); - } - ); - - return ( -
- {this.children} -
- ); - } -} - -///////////////////////////////////////////////////////// -// -// -///////////////////////////////////////////////////////// ReflexContainer.propTypes = { orientation: PropTypes.oneOf(["horizontal", "vertical"]), className: PropTypes.string, style: PropTypes.object }; -///////////////////////////////////////////////////////// -// -// -///////////////////////////////////////////////////////// ReflexContainer.defaultProps = { orientation: "horizontal", className: "", diff --git a/packages/ove/src/Reflex/ReflexSplitter.js b/packages/ove/src/Reflex/ReflexSplitter.js index 760809ec..3cc7bee5 100644 --- a/packages/ove/src/Reflex/ReflexSplitter.js +++ b/packages/ove/src/Reflex/ReflexSplitter.js @@ -1,19 +1,12 @@ -/////////////////////////////////////////////////////////// // ReflexSplitter // By Philippe Leefsma // December 2016 -// -/////////////////////////////////////////////////////////// import PropTypes from "prop-types"; -import ReactDOM from "react-dom"; import Browser from "./Browser"; -import React from "react"; +import React, { createRef } from "react"; export default class ReflexSplitter extends React.Component { - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// + divRef = createRef(); static propTypes = { children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), @@ -27,10 +20,6 @@ export default class ReflexSplitter extends React.Component { style: PropTypes.object }; - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// static defaultProps = { document: typeof document === "undefined" ? null : document, onStartResize: null, @@ -41,10 +30,6 @@ export default class ReflexSplitter extends React.Component { style: {} }; - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// constructor(props) { super(props); @@ -59,10 +44,6 @@ export default class ReflexSplitter extends React.Component { this.document = props.document; } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// componentDidMount() { if (!this.document) { return; @@ -81,10 +62,6 @@ export default class ReflexSplitter extends React.Component { }); } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// componentWillUnmount() { if (!this.document) { return; @@ -99,10 +76,6 @@ export default class ReflexSplitter extends React.Component { this.document.removeEventListener("touchmove", this.onMouseMove); } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// onMouseMove(event) { if (this.state.active) { this.props.events.emit("splitter.resize", { @@ -112,7 +85,7 @@ export default class ReflexSplitter extends React.Component { if (this.props.onResize) { this.props.onResize({ - domElement: ReactDOM.findDOMNode(this), + domElement: this.divRef.current, component: this }); } @@ -122,10 +95,6 @@ export default class ReflexSplitter extends React.Component { } } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// onMouseDown(event) { this.setState({ active: true @@ -137,7 +106,7 @@ export default class ReflexSplitter extends React.Component { // to onStartResize if ( this.props.onStartResize({ - domElement: ReactDOM.findDOMNode(this), + domElement: this.divRef.current, component: this }) ) { @@ -151,10 +120,6 @@ export default class ReflexSplitter extends React.Component { }); } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// onMouseUp(event) { if (this.state.active) { this.setState({ @@ -163,7 +128,7 @@ export default class ReflexSplitter extends React.Component { if (this.props.onStopResize) { this.props.onStopResize({ - domElement: ReactDOM.findDOMNode(this), + domElement: this.divRef.current, component: this }); } @@ -175,10 +140,6 @@ export default class ReflexSplitter extends React.Component { } } - ///////////////////////////////////////////////////////// - // - // - ///////////////////////////////////////////////////////// render() { const classNames = ["reflex-splitter", ...this.props.className.split(" ")]; @@ -197,6 +158,7 @@ export default class ReflexSplitter extends React.Component { onMouseDown={this.onMouseDown} style={this.props.style} id={this.props.id} + ref={this.divRef} > {this.props.children}
diff --git a/packages/ove/src/createVectorEditor/index.js b/packages/ove/src/createVectorEditor/index.jsx similarity index 89% rename from packages/ove/src/createVectorEditor/index.js rename to packages/ove/src/createVectorEditor/index.jsx index a54b602c..d8143487 100644 --- a/packages/ove/src/createVectorEditor/index.js +++ b/packages/ove/src/createVectorEditor/index.jsx @@ -1,7 +1,6 @@ import React from "react"; import { Provider } from "react-redux"; import makeStore from "./makeStore"; -import { render, unmountComponentAtNode } from "react-dom"; import Editor from "../Editor"; import updateEditor from "../updateEditor"; @@ -9,6 +8,7 @@ import addAlignment from "../addAlignment"; import AlignmentView from "../AlignmentView"; import sizeMe from "react-sizeme"; import VersionHistoryView from "../VersionHistoryView"; +import { createRoot } from "react-dom/client"; let store; @@ -64,12 +64,12 @@ export default function createVectorEditor( node = _node; } const editor = {}; - editor.renderResponse = render( - , - node - ); + + editor.root = createRoot(node); + editor.root.render(); + editor.close = () => { - unmountComponentAtNode(node); + editor.root.unmount(); node.remove(); }; editor.updateEditor = values => { @@ -92,10 +92,12 @@ export function createVersionHistoryView( if (!store) { store = makeStore(); } + const editor = {}; - editor.renderResponse = render( - , - node + + editor.root = createRoot(node); + editor.root.render( + ); editor.updateEditor = values => { @@ -114,7 +116,9 @@ export function createAlignmentView(node, props = {}) { store = makeStore(); } const editor = {}; - editor.renderResponse = render(, node); + + editor.root = createRoot(node); + editor.root.render(); editor.updateAlignment = values => { addAlignment(store, values); diff --git a/packages/ove/src/helperComponents/PrintDialog/index.js b/packages/ove/src/helperComponents/PrintDialog/index.js index eeca3a1e..f738532b 100644 --- a/packages/ove/src/helperComponents/PrintDialog/index.js +++ b/packages/ove/src/helperComponents/PrintDialog/index.js @@ -1,20 +1,17 @@ -import React from "react"; -import { findDOMNode } from "react-dom"; +import React, { createRef } from "react"; import PropTypes from "prop-types"; - import { reduxForm } from "redux-form"; - import { wrapDialog } from "@teselagen/ui"; import { compose } from "redux"; import { Button, Classes, ButtonGroup } from "@blueprintjs/core"; import classNames from "classnames"; - import withEditorProps from "../../withEditorProps"; import CircularView from "../../CircularView"; import LinearView from "../../LinearView"; import "./style.css"; class PrintDialog extends React.Component { + componentRef = createRef(); state = { circular: null }; @@ -68,7 +65,7 @@ class PrintDialog extends React.Component { fullscreen={this.state && this.state.fullscreen} circular={isCirc} editorName={editorName || "StandaloneEditor"} - ref={el => (this.componentRef = el)} + ref={this.componentRef.current} />
{!hidePrintButton && ( @@ -235,7 +232,7 @@ class ReactToPrint extends React.Component { // if (printPreview) defaultPageStyle += " " - const contentNodes = findDOMNode(contentEl); + const contentNodes = contentEl; const linkNodes = document.querySelectorAll('link[rel="stylesheet"]'); this.linkTotal = linkNodes.length || 0; @@ -367,30 +364,9 @@ class ReactToPrint extends React.Component { } } -class ComponentToPrint extends React.Component { - // componentDidMount() { - // let ctx = this.canvasEl.getContext("2d"); - // ctx.beginPath(); - // ctx.arc(95, 50, 40, 0, 2 * Math.PI); - // ctx.stroke(); - // } - - render() { - const { editorName, circular } = this.props; - - // let w = window, - // d = document, - // e = d.documentElement, - // g = d.getElementsByTagName("body")[0], - // width = w.innerWidth || e.clientWidth || g.clientWidth, - // height = w.innerHeight || e.clientHeight || g.clientHeight; - - // const width = 670; - // const height = 900; - return circular ? ( - - ) : ( - - ); - } -} +const ComponentToPrint = ({ circular, editorName }) => + circular ? ( + + ) : ( + + ); diff --git a/packages/ove/src/withEditorInteractions/createSequenceInputPopup.js b/packages/ove/src/withEditorInteractions/createSequenceInputPopup.js index 4fb9d1ac..0cadceb1 100644 --- a/packages/ove/src/withEditorInteractions/createSequenceInputPopup.js +++ b/packages/ove/src/withEditorInteractions/createSequenceInputPopup.js @@ -1,7 +1,4 @@ -import { render, unmountComponentAtNode, findDOMNode } from "react-dom"; - import { getRangeLength } from "@teselagen/range-utils"; -// import Tether from "tether"; import Popper from "popper.js"; import { @@ -9,16 +6,18 @@ import { convertDnaCaretPositionOrRangeToAA, filterSequenceString } from "@teselagen/sequence-utils"; -import React from "react"; +import React, { createRef } from "react"; import { divideBy3 } from "../utils/proteinUtils"; import "./createSequenceInputPopupStyle.css"; import { Classes } from "@blueprintjs/core"; import { getNodeToRefocus } from "../utils/editorUtils"; import { noop } from "lodash-es"; +import { createRoot } from "react-dom/client"; let div; class SequenceInputNoHotkeys extends React.Component { + divRef = createRef(); state = { charsToInsert: "", hasTempError: false @@ -37,7 +36,7 @@ class SequenceInputNoHotkeys extends React.Component { ); } handleUnmountIfClickOustidePopup = e => { - const n = findDOMNode(this); + const n = this.divRef.current; if (!n) return; const node = n.parentNode; if (!node) return; @@ -46,17 +45,19 @@ class SequenceInputNoHotkeys extends React.Component { } this.handleUnmount(); }; + handleUnmount = () => { setTimeout(() => { - const n = findDOMNode(this); + const n = this.divRef.current; if (!n) return; const node = n.parentNode; if (!node) return; - unmountComponentAtNode(node); + this.props.unmountRoot && this.props.unmountRoot(); this.props.nodeToReFocus && this.props.nodeToReFocus.focus(); document.getElementById("sequenceInputBubble").outerHTML = ""; }); }; + handleInsert() { const { handleInsert = noop, isProtein } = this.props; const { charsToInsert } = this.state; @@ -118,8 +119,9 @@ class SequenceInputNoHotkeys extends React.Component { ); } + return ( -
+
{ @@ -225,15 +227,19 @@ export default function createSequenceInputPopup(props) { div.style.zIndex = "400000"; div.id = "sequenceInputBubble"; document.body.appendChild(div); + const root = createRoot(div); + + const unmount = () => root.unmount(); const innerEl = ( ); - render(innerEl, div); + root.render(innerEl); if (!caretEl) { return console.error( diff --git a/packages/ui/demo/src/examples/UploadCsvWizard.js b/packages/ui/demo/src/examples/UploadCsvWizard.js index 3f68dd88..c76c8fca 100644 --- a/packages/ui/demo/src/examples/UploadCsvWizard.js +++ b/packages/ui/demo/src/examples/UploadCsvWizard.js @@ -6,7 +6,7 @@ import { FileUploadField } from "../../../src"; import DemoWrapper from "../DemoWrapper"; import { reduxForm } from "redux-form"; import { useToggle } from "../useToggle"; -import getIdOrCodeOrIndex from "../../../src/DataTable/utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "../../../src/DataTable/utils"; const simpleValidateAgainst = { fields: [{ path: "name" }, { path: "description" }, { path: "sequence" }] diff --git a/packages/ui/demo/src/index.js b/packages/ui/demo/src/index.js index adad2335..6e11ad22 100644 --- a/packages/ui/demo/src/index.js +++ b/packages/ui/demo/src/index.js @@ -25,7 +25,6 @@ import ScrollToTopDemo from "./examples/ScrollToTop"; import showAppSpinnerDemo from "./examples/showAppSpinnerDemo"; import EditableCellTable from "./examples/EditableCellTable"; import React from "react"; -import { render } from "react-dom"; import { Provider } from "react-redux"; import store from "./store"; import { FocusStyleManager } from "@blueprintjs/core"; @@ -33,6 +32,7 @@ import AdvancedOptionsDemo from "./examples/AdvancedOptionsDemo"; import FormComponents from "./examples/FormComponents"; import UploadCsvWizard from "./examples/UploadCsvWizard"; import TagSelectDemo from "./examples/TagSelectDemo"; +import { createRoot } from "react-dom/client"; FocusStyleManager.onlyShowFocusOnTabs(); @@ -261,4 +261,5 @@ const Demo = () => { ); }; -render(, document.querySelector("#demo")); +const root = createRoot(document.querySelector("#demo")); +root.render(); diff --git a/packages/ui/src/DataTable/CellDragHandle.js b/packages/ui/src/DataTable/CellDragHandle.js index 154918d7..99afd707 100644 --- a/packages/ui/src/DataTable/CellDragHandle.js +++ b/packages/ui/src/DataTable/CellDragHandle.js @@ -1,7 +1,6 @@ import { flatMap } from "lodash-es"; import { forEach } from "lodash-es"; import React, { useRef } from "react"; -import ReactDOM from "react-dom"; export function CellDragHandle({ thisTable, @@ -15,7 +14,7 @@ export function CellDragHandle({ const rectangleCellPaths = useRef(); const handleDrag = useRef(e => { - const table = ReactDOM.findDOMNode(thisTable).querySelector(".rt-table"); + const table = thisTable.querySelector(".rt-table"); const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [rowId, path] = cellId.split(":"); const selectedTr = table.querySelector( @@ -83,7 +82,7 @@ export function CellDragHandle({ const mouseup = useRef(() => { clearTimeout(timeoutkey.current); - const table = ReactDOM.findDOMNode(thisTable); + const table = thisTable; const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`); const [, path] = cellId.split(":"); //remove the dashed borders diff --git a/packages/ui/src/DataTable/PagingTool.js b/packages/ui/src/DataTable/PagingTool.js index d13335ec..f422bef5 100644 --- a/packages/ui/src/DataTable/PagingTool.js +++ b/packages/ui/src/DataTable/PagingTool.js @@ -5,7 +5,7 @@ import { noop, get, toInteger } from "lodash-es"; import { Button, Classes } from "@blueprintjs/core"; import { onEnterOrBlurHelper } from "../utils/handlerHelpers"; import { defaultPageSizes } from "./utils/queryParams"; -import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./utils"; function PagingInput({ disabled, onBlur, defaultPage }) { const [page, setPage] = useState(defaultPage); diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index c49f533c..c02d4a1b 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -1,5 +1,4 @@ -import React from "react"; -import ReactDOM from "react-dom"; +import React, { createRef } from "react"; import { invert, toNumber, @@ -27,7 +26,6 @@ import { every } from "lodash-es"; import joinUrl from "url-join"; - import { Button, Menu, @@ -124,6 +122,8 @@ const itemSizeEstimators = { class DataTable extends React.Component { constructor(props) { super(props); + + this.tableRef = createRef(); if (this.props.helperProp) { this.props.helperProp.updateValidationHelper = this.updateValidationHelper; @@ -209,7 +209,6 @@ class DataTable extends React.Component { columns: [], fullscreen: false }; - static defaultProps = defaultProps; handleEnterStartCellEdit = e => { @@ -219,7 +218,7 @@ class DataTable extends React.Component { flashTableBorder = () => { try { - const table = ReactDOM.findDOMNode(this.table); + const table = this.tableRef.current.tableRef; table.classList.add("tgBorderBlue"); setTimeout(() => { table.classList.remove("tgBorderBlue"); @@ -291,7 +290,6 @@ class DataTable extends React.Component { reduxFormExpandedEntityIdMap, change } = newProps; - const table = ReactDOM.findDOMNode(this.table); const idMap = reduxFormSelectedEntityIdMap; @@ -357,7 +355,8 @@ class DataTable extends React.Component { // if not changing selectedIds then we just want to make sure selected entities // stored in redux are in proper format // if selected ids have changed then it will handle redux selection - const tableScrollElement = table.getElementsByClassName("rt-table")[0]; + const tableScrollElement = + this.tableRef.current.tableRef.getElementsByClassName("rt-table")[0]; const { entities: oldEntities = [], reduxFormSelectedEntityIdMap: oldIdMap @@ -406,8 +405,9 @@ class DataTable extends React.Component { const entityIndexToScrollTo = entities.findIndex( e => e.id === idToScrollTo || e.code === idToScrollTo ); - if (entityIndexToScrollTo === -1 || !table) return; - const tableBody = table.querySelector(".rt-tbody"); + if (entityIndexToScrollTo === -1 || !this.tableRef.current) return; + const tableBody = + this.tableRef.current.tableRef.querySelector(".rt-tbody"); if (!tableBody) return; const rowEl = tableBody.getElementsByClassName("rt-tr-group")[entityIndexToScrollTo]; @@ -504,7 +504,7 @@ class DataTable extends React.Component { if (!entities.length && !isLoading && !showForcedHiddenColumns) { setShowForcedHidden(true); } - // const table = ReactDOM.findDOMNode(this.table); + // const table = this.tableRef.current.tableRef; // let theads = table.getElementsByClassName("rt-thead"); // let tbody = table.getElementsByClassName("rt-tbody")[0]; @@ -1694,9 +1694,7 @@ class DataTable extends React.Component { )} { - if (n) this.table = n; - }} + ref={this.tableRef} // additionalBodyEl={} className={classNames({ isCellEditable, @@ -2268,7 +2266,7 @@ class DataTable extends React.Component { refocusTable = () => { setTimeout(() => { - const table = ReactDOM.findDOMNode(this.table)?.closest( + const table = this.tableRef.current?.tableRef?.closest( ".data-table-container>div" ); table?.focus(); @@ -2687,7 +2685,7 @@ class DataTable extends React.Component { : isSelectedCell === PRIMARY_SELECTED_VAL) && ( { this.insertRows({ above: true }); }} - > + /> { this.insertRows({}); }} - > + /> 1 ? "s" : ""}`} @@ -3393,7 +3391,7 @@ class DataTable extends React.Component { }} indeterminate={isIndeterminate} checked={isChecked} - > + /> ); } diff --git a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js index bcbf17c1..2a210198 100644 --- a/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js +++ b/packages/ui/src/DataTable/utils/getIdOrCodeOrIndex.js @@ -1,4 +1,4 @@ -export default (record, rowIndex) => { +export const getIdOrCodeOrIndex = (record, rowIndex) => { if (record.id || record.id === 0) { return record.id; } else if (record.code) { diff --git a/packages/ui/src/DataTable/utils/handleCopyColumn.js b/packages/ui/src/DataTable/utils/handleCopyColumn.js index d306053e..4aa04870 100644 --- a/packages/ui/src/DataTable/utils/handleCopyColumn.js +++ b/packages/ui/src/DataTable/utils/handleCopyColumn.js @@ -1,5 +1,5 @@ import { getAllRows } from "./getAllRows"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import { handleCopyRows } from "./handleCopyRows"; export const handleCopyColumn = (e, cellWrapper, selectedRecords) => { diff --git a/packages/ui/src/DataTable/utils/index.js b/packages/ui/src/DataTable/utils/index.js index 48307406..2d85f6cb 100644 --- a/packages/ui/src/DataTable/utils/index.js +++ b/packages/ui/src/DataTable/utils/index.js @@ -1,7 +1,7 @@ import { isEntityClean } from "./isEntityClean"; import { getSelectedRowsFromEntities } from "./selection"; import { removeCleanRows } from "./removeCleanRows"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import computePresets from "./computePresets"; import { getRecordsFromIdMap } from "./withSelectedEntities"; import { formatPasteData } from "./formatPasteData"; diff --git a/packages/ui/src/DataTable/utils/isEntityClean.js b/packages/ui/src/DataTable/utils/isEntityClean.js index acf34e37..f9d507d4 100644 --- a/packages/ui/src/DataTable/utils/isEntityClean.js +++ b/packages/ui/src/DataTable/utils/isEntityClean.js @@ -1,13 +1,15 @@ export function isEntityClean(e) { + if (typeof e !== "object" || e === null) { + return true; // or return false depending on what you want for non-object inputs + } let isClean = true; - e.some((val, key) => { - if (key === "id") return false; - if (key === "_isClean") return false; + for (const [key, val] of Object.entries(e)) { + if (key === "id") continue; + if (key === "_isClean") continue; if (val) { isClean = false; - return true; + break; } - return false; - }); + } return isClean; } diff --git a/packages/ui/src/DataTable/utils/removeCleanRows.js b/packages/ui/src/DataTable/utils/removeCleanRows.js index b95f5552..5958166a 100644 --- a/packages/ui/src/DataTable/utils/removeCleanRows.js +++ b/packages/ui/src/DataTable/utils/removeCleanRows.js @@ -1,7 +1,7 @@ import { isEntityClean } from "./isEntityClean"; import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; -export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { +export const removeCleanRows = (reduxFormEntities, reduxFormCellValidation) => { const toFilterOut = {}; const entsToUse = (reduxFormEntities || []).filter(e => { if (!(e._isClean || isEntityClean(e))) return true; @@ -12,11 +12,11 @@ export function removeCleanRows(reduxFormEntities, reduxFormCellValidation) { }); const validationToUse = {}; - reduxFormCellValidation.forEach((v, k) => { + Object.entries(reduxFormCellValidation || {}).forEach(([k, v]) => { const [rowId] = k.split(":"); if (!toFilterOut[rowId]) { validationToUse[k] = v; } }); return { entsToUse, validationToUse }; -} +}; diff --git a/packages/ui/src/DataTable/utils/rowClick.js b/packages/ui/src/DataTable/utils/rowClick.js index 8c4c5570..e4ee123a 100644 --- a/packages/ui/src/DataTable/utils/rowClick.js +++ b/packages/ui/src/DataTable/utils/rowClick.js @@ -1,6 +1,6 @@ import { isEmpty, forEach, range } from "lodash-es"; import { getSelectedRowsFromEntities } from "./selection"; -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import { getRecordsFromIdMap } from "./withSelectedEntities"; export default function rowClick(e, rowInfo, entities, props) { diff --git a/packages/ui/src/DataTable/utils/selection.js b/packages/ui/src/DataTable/utils/selection.js index ea4a08d8..39757606 100644 --- a/packages/ui/src/DataTable/utils/selection.js +++ b/packages/ui/src/DataTable/utils/selection.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; export const getSelectedRowsFromEntities = (entities, idMap) => { if (!idMap) return []; diff --git a/packages/ui/src/DataTable/utils/utils.js b/packages/ui/src/DataTable/utils/utils.js index b15021e3..fe7b4d1c 100644 --- a/packages/ui/src/DataTable/utils/utils.js +++ b/packages/ui/src/DataTable/utils/utils.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; export const getFieldPathToIndex = schema => { const fieldToIndex = {}; diff --git a/packages/ui/src/DataTable/validateTableWideErrors.js b/packages/ui/src/DataTable/validateTableWideErrors.js index 11c2eb93..c29a36b2 100644 --- a/packages/ui/src/DataTable/validateTableWideErrors.js +++ b/packages/ui/src/DataTable/validateTableWideErrors.js @@ -1,4 +1,4 @@ -import getIdOrCodeOrIndex from "./utils/getIdOrCodeOrIndex"; +import { getIdOrCodeOrIndex } from "./utils"; import { getCellVal } from "./getCellVal"; import { forEach, isArray } from "lodash-es"; import { startCase } from "lodash-es"; diff --git a/packages/ui/src/FillWindow.js b/packages/ui/src/FillWindow.js index ab25adc5..c333c342 100644 --- a/packages/ui/src/FillWindow.js +++ b/packages/ui/src/FillWindow.js @@ -1,6 +1,5 @@ -import React from "react"; +import React, { createPortal } from "react"; import { isFunction } from "lodash-es"; -import reactDom from "react-dom"; import rerenderOnWindowResize from "./rerenderOnWindowResize"; import "./FillWindow.css"; @@ -63,7 +62,7 @@ export default class FillWindow extends React.Component { : this.props.children}
); - if (asPortal) return reactDom.createPortal(inner, window.document.body); + if (asPortal) return createPortal(inner, window.document.body); return inner; } } diff --git a/packages/ui/src/FormComponents/Uploader.js b/packages/ui/src/FormComponents/Uploader.js index e6c08cfa..20afed7c 100644 --- a/packages/ui/src/FormComponents/Uploader.js +++ b/packages/ui/src/FormComponents/Uploader.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button, Callout, @@ -16,7 +16,6 @@ import classnames from "classnames"; import { nanoid } from "nanoid"; import papaparse, { unparse } from "papaparse"; import downloadjs from "downloadjs"; -import { configure, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import UploadCsvWizardDialog, { SimpleInsertDataDialog @@ -30,7 +29,7 @@ import { removeExt } from "@teselagen/file-utils"; import tryToMatchSchemas from "./tryToMatchSchemas"; -import { forEach, isArray, isFunction, isPlainObject, noop } from "lodash-es"; +import { isArray, isFunction, isPlainObject, noop } from "lodash-es"; import { flatMap } from "lodash-es"; import urljoin from "url-join"; import popoverOverflowModifiers from "../utils/popoverOverflowModifiers"; @@ -38,14 +37,12 @@ import writeXlsxFile from "write-excel-file"; import { startCase } from "lodash-es"; import { getNewName } from "./getNewName"; import { isObject } from "lodash-es"; -import { connect } from "react-redux"; +import { useDispatch } from "react-redux"; import { initialize } from "redux-form"; import classNames from "classnames"; -import { compose } from "recompose"; import convertSchema from "../DataTable/utils/convertSchema"; import { LoadingDots } from "./LoadingDots"; -configure({ isolateGlobalState: true }); const helperText = [ `How to Use This Template to Upload New Data`, `1. Go to the first tab and delete the example data.`, @@ -64,58 +61,108 @@ const helperSchema = [ } ]; -class ValidateAgainstSchema { - fields = []; - - constructor() { - makeObservable(this, { - fields: observable.shallow - }); +const setValidateAgainstSchema = newValidateAgainstSchema => { + if (!newValidateAgainstSchema) { + return []; } - - setValidateAgainstSchema(newValidateAgainstSchema) { - if (!newValidateAgainstSchema) { - this.fields = []; - return; - } - const schema = convertSchema(newValidateAgainstSchema); - if ( - schema.fields.some(f => { - if (f.path === "id") { - return true; - } - return false; - }) - ) { - throw new Error( - `Uploader was passed a validateAgainstSchema with a fields array that contains a field with a path of "id". This is not allowed.` - ); - } - forEach(schema, (v, k) => { - this[k] = v; - }); + const schema = convertSchema(newValidateAgainstSchema); + if ( + schema.fields.some(f => { + if (f.path === "id") { + return true; + } + return false; + }) + ) { + throw new Error( + `Uploader was passed a validateAgainstSchema with a fields array that contains a field with a path of "id". This is not allowed.` + ); } -} - -// autorun(() => { -// console.log( -// `validateAgainstSchemaStore?.fields:`, -// JSON.stringify(validateAgainstSchemaStore?.fields, null, 4) -// ); -// }); -// validateAgainstSchemaStore.fields = ["hahah"]; -// validateAgainstSchemaStore.fields.push("yaa"); + return schema; +}; -// const validateAgainstSchema = observable.shallow({ -// fields: [] -// }) - -// validateAgainstSchema.fields = ["hahah"]; +const InnerDropZone = ({ + getRootProps, + getInputProps, + isDragAccept, + isDragReject, + isDragActive, + className, + minimal, + dropzoneDisabled, + contentOverride, + simpleAccept, + innerIcon, + innerText, + validateAgainstSchema, + handleManuallyEnterData, + noBuildCsvOption, + showFilesCount, + fileList + // isDragActive + // isDragReject + // isDragAccept +}) => ( +
+
+ + {contentOverride || ( +
+ {innerIcon || } + {innerText || (minimal ? "Upload" : "Click or drag to upload")} + {validateAgainstSchema && !noBuildCsvOption && ( +
+ ...or {manualEnterMessage} + {/*
+ {manualEnterSubMessage} +
*/} +
+ )} +
+ )} +
-// wink wink -const emptyPromise = Promise.resolve.bind(Promise); + {showFilesCount ? ( +
+ Files: {fileList ? fileList.length : 0} +
+ ) : null} +
+); -function UploaderInner({ +const UploaderInner = ({ accept: __accept, contentOverride: maybeContentOverride, innerIcon, @@ -130,7 +177,9 @@ function UploaderInner({ showUploadList = true, beforeUpload, fileList, //list of files with options: {name, loading, error, url, originalName, downloadName} - onFileSuccess = emptyPromise, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! + onFileSuccess = async () => { + return; + }, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise! onFieldSubmit = noop, //called when all files have successfully uploaded // fileFinished = noop, onRemove = noop, //called when a file has been selected to be removed @@ -141,14 +190,13 @@ function UploaderInner({ autoUnzip, disabled: _disabled, noBuildCsvOption, - initializeForm, showFilesCount, threeDotMenuItems, onPreviewClick -}) { +}) => { + const dispatch = useDispatch(); let dropzoneDisabled = _disabled; let _accept = __accept; - const validateAgainstSchemaStore = useRef(new ValidateAgainstSchema()); const [acceptLoading, setAcceptLoading] = useState(); const [resolvedAccept, setResolvedAccept] = useState(); if (resolvedAccept) { @@ -169,9 +217,11 @@ function UploaderInner({ ); } }, [__accept, isAcceptPromise]); + if (isAcceptPromise && !resolvedAccept) { _accept = []; } + if (acceptLoading) dropzoneDisabled = true; const accept = !_accept ? undefined @@ -188,16 +238,10 @@ function UploaderInner({ _validateAgainstSchema || accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema; - useEffect(() => { - // validateAgainstSchema - validateAgainstSchemaStore.current.setValidateAgainstSchema( - validateAgainstSchemaToUse - ); - }, [validateAgainstSchemaToUse]); - let validateAgainstSchema; - if (validateAgainstSchemaToUse) { - validateAgainstSchema = validateAgainstSchemaStore.current; - } + const validateAgainstSchema = useMemo( + () => setValidateAgainstSchema(validateAgainstSchemaToUse), + [validateAgainstSchemaToUse] + ); if ( (validateAgainstSchema || autoUnzip) && @@ -215,6 +259,7 @@ function UploaderInner({ const { showDialogPromise: showUploadCsvWizardDialog, comp } = useDialog({ ModalComponent: UploadCsvWizardDialog }); + const { showDialogPromise: showSimpleInsertDataDialog, comp: comp2 } = useDialog({ ModalComponent: SimpleInsertDataDialog @@ -553,7 +598,7 @@ function UploaderInner({ } {...getFileDownloadAttr(exampleFile)} key={i} - > + /> ); } )} @@ -619,7 +664,7 @@ function UploaderInner({ }} size={10} icon="download" - > + /> )} @@ -631,7 +676,8 @@ function UploaderInner({ // make the dots below "load" <> - Accept Loading + Accept Loading + ) : ( <>Accepts {simpleAccept} @@ -650,135 +696,135 @@ function UploaderInner({ .join(", ") : undefined } - {...{ - onDrop: async (_acceptedFiles, rejectedFiles) => { - let acceptedFiles = []; - for (const file of _acceptedFiles) { - if ((validateAgainstSchema || autoUnzip) && isZipFile(file)) { - const files = await filterFilesInZip( - file, - simpleAccept - ?.split(", ") - ?.map(a => (a.startsWith(".") ? a : "." + a)) || [] - ); - acceptedFiles.push(...files.map(f => f.originFileObj)); - } else { - acceptedFiles.push(file); - } - } - cleanupFiles(); - if (rejectedFiles.length) { - let msg = ""; - rejectedFiles.forEach(file => { - if (msg) msg += "\n"; - msg += - `${file.file.name}: ` + - file.errors.map(err => err.message).join(", "); - }); - window.toastr && - window.toastr.warning( -
{msg}
- ); + onDrop={async (_acceptedFiles, rejectedFiles) => { + let acceptedFiles = []; + for (const file of _acceptedFiles) { + if ((validateAgainstSchema || autoUnzip) && isZipFile(file)) { + const files = await filterFilesInZip( + file, + simpleAccept + ?.split(", ") + ?.map(a => (a.startsWith(".") ? a : "." + a)) || [] + ); + acceptedFiles.push(...files.map(f => f.originFileObj)); + } else { + acceptedFiles.push(file); } - if (!acceptedFiles.length) return; - setLoading(true); - acceptedFiles = trimFiles(acceptedFiles, fileLimit); - - acceptedFiles.forEach(file => { - file.preview = URL.createObjectURL(file); - file.loading = true; - if (!file.id) { - file.id = nanoid(); - } - filesToClean.current.push(file); + } + cleanupFiles(); + if (rejectedFiles.length) { + let msg = ""; + rejectedFiles.forEach(file => { + if (msg) msg += "\n"; + msg += + `${file.file.name}: ` + + file.errors.map(err => err.message).join(", "); }); - - if (readBeforeUpload) { - acceptedFiles = await Promise.all( - acceptedFiles.map(file => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsText(file, "UTF-8"); - reader.onload = evt => { - file.parsedString = evt.target.result; - resolve(file); - }; - reader.onerror = err => { - console.error("err:", err); - reject(err); - }; - }); - }) + window.toastr && + window.toastr.warning( +
{msg}
); + } + if (!acceptedFiles.length) return; + setLoading(true); + acceptedFiles = trimFiles(acceptedFiles, fileLimit); + + acceptedFiles.forEach(file => { + file.preview = URL.createObjectURL(file); + file.loading = true; + if (!file.id) { + file.id = nanoid(); } - const cleanedAccepted = acceptedFiles.map(file => { - return { - originFileObj: file, - originalFileObj: file, - id: file.id, - lastModified: file.lastModified, - lastModifiedDate: file.lastModifiedDate, - loading: file.loading, - name: file.name, - preview: file.preview, - size: file.size, - type: file.type, - ...(file.parsedString - ? { parsedString: file.parsedString } - : {}) - }; - }); + filesToClean.current.push(file); + }); - const toKeep = []; - if (validateAgainstSchema) { - const filesWIssues = []; - const filesWOIssues = []; - for (const [i, file] of cleanedAccepted.entries()) { - if (isCsvOrExcelFile(file)) { - let parsedF; - try { - parsedF = await parseCsvOrExcelFile(file, { - csvParserOptions: isFunction( - validateAgainstSchema.csvParserOptions - ) - ? validateAgainstSchema.csvParserOptions({ - validateAgainstSchema - }) - : validateAgainstSchema.csvParserOptions - }); - } catch (error) { - console.error("error:", error); - window.toastr && - window.toastr.error( - `There was an error parsing your file. Please try again. ${ - error.message || error - }` - ); - return; - } + if (readBeforeUpload) { + acceptedFiles = await Promise.all( + acceptedFiles.map(file => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = evt => { + file.parsedString = evt.target.result; + resolve(file); + }; + reader.onerror = err => { + console.error("err:", err); + reject(err); + }; + }); + }) + ); + } + const cleanedAccepted = acceptedFiles.map(file => { + return { + originFileObj: file, + originalFileObj: file, + id: file.id, + lastModified: file.lastModified, + lastModifiedDate: file.lastModifiedDate, + loading: file.loading, + name: file.name, + preview: file.preview, + size: file.size, + type: file.type, + ...(file.parsedString + ? { parsedString: file.parsedString } + : {}) + }; + }); - const { - csvValidationIssue: _csvValidationIssue, - matchedHeaders, - userSchema, - searchResults, - ignoredHeadersMsg - } = await tryToMatchSchemas({ - incomingData: parsedF.data, - validateAgainstSchema + const toKeep = []; + if (validateAgainstSchema) { + const filesWIssues = []; + const filesWOIssues = []; + for (const [i, file] of cleanedAccepted.entries()) { + if (isCsvOrExcelFile(file)) { + let parsedF; + try { + parsedF = await parseCsvOrExcelFile(file, { + csvParserOptions: isFunction( + validateAgainstSchema.csvParserOptions + ) + ? validateAgainstSchema.csvParserOptions({ + validateAgainstSchema + }) + : validateAgainstSchema.csvParserOptions }); - if (userSchema?.userData?.length === 0) { - console.error( - `userSchema, parsedF.data:`, - userSchema, - parsedF.data + } catch (error) { + console.error("error:", error); + window.toastr && + window.toastr.error( + `There was an error parsing your file. Please try again. ${ + error.message || error + }` ); - } else { - toKeep.push(file); - let csvValidationIssue = _csvValidationIssue; - if (csvValidationIssue) { - if (isObject(csvValidationIssue)) { - initializeForm( + return; + } + + const { + csvValidationIssue: _csvValidationIssue, + matchedHeaders, + userSchema, + searchResults, + ignoredHeadersMsg + } = await tryToMatchSchemas({ + incomingData: parsedF.data, + validateAgainstSchema + }); + if (userSchema?.userData?.length === 0) { + console.error( + `userSchema, parsedF.data:`, + userSchema, + parsedF.data + ); + } else { + toKeep.push(file); + let csvValidationIssue = _csvValidationIssue; + if (csvValidationIssue) { + if (isObject(csvValidationIssue)) { + dispatch( + initialize( `editableCellTable${ cleanedAccepted.length > 1 ? `-${i}` : "" }`, @@ -790,149 +836,142 @@ function UploaderInner({ keepValues: true, updateUnregisteredFields: true } + ) + ); + const err = Object.values(csvValidationIssue)[0]; + // csvValidationIssue = `It looks like there was an error with your data - \n\n${ + // err && err.message ? err.message : err + // }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string + const errMsg = err && err.message ? err.message : err; + if (isPlainObject(errMsg)) { + throw new Error( + `errMsg is an object ${JSON.stringify( + errMsg, + null, + 4 + )}` ); - const err = Object.values(csvValidationIssue)[0]; - // csvValidationIssue = `It looks like there was an error with your data - \n\n${ - // err && err.message ? err.message : err - // }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string - const errMsg = - err && err.message ? err.message : err; - if (isPlainObject(errMsg)) { - throw new Error( - `errMsg is an object ${JSON.stringify( - errMsg, - null, - 4 - )}` - ); - } - csvValidationIssue = ( + } + csvValidationIssue = ( +
-
- It looks like there was an error with your - data (Correct on the Review Data page): -
-
{errMsg}
-
- Please review your headers and then correct - any errors on the next page. -
+ It looks like there was an error with your data + (Correct on the Review Data page):
- ); - } - filesWIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults - }); - } else { - filesWOIssues.push({ - file, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - userSchema, - searchResults - }); - const newFileName = removeExt(file.name) + `.csv`; - - const { newFile, cleanedEntities } = getNewCsvFile( - userSchema.userData, - newFileName +
{errMsg}
+
+ Please review your headers and then correct any + errors on the next page. +
+
); - - file.meta = parsedF.meta; - file.hasEditClick = true; - file.parsedData = cleanedEntities; - file.name = newFileName; - file.originFileObj = newFile; - file.originalFileObj = newFile; } - } - } else { - toKeep.push(file); - } - } - if (filesWIssues.length) { - const { file } = filesWIssues[0]; - const allFiles = [...filesWIssues, ...filesWOIssues]; - const doAllFilesHaveSameHeaders = allFiles.every(f => { - if (f.userSchema.fields && f.userSchema.fields.length) { - return f.userSchema.fields.every((h, i) => { - return ( - h.path === allFiles[0].userSchema.fields[i].path - ); + filesWIssues.push({ + file, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + userSchema, + searchResults }); - } - return false; - }); - const multipleFiles = allFiles.length > 1; - const { res } = await showUploadCsvWizardDialog( - "onUploadWizardFinish", - { - dialogProps: { - title: `Fix Up File${multipleFiles ? "s" : ""} ${ - multipleFiles - ? "" - : file.name - ? `"${file.name}"` - : "" - }` - }, - doAllFilesHaveSameHeaders, - filesWIssues: allFiles, - validateAgainstSchema - } - ); + } else { + filesWOIssues.push({ + file, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + userSchema, + searchResults + }); + const newFileName = removeExt(file.name) + `.csv`; - if (!res) { - window.toastr.warning(`File Upload Aborted`); - return; - } else { - allFiles.forEach(({ file }, i) => { - const newEntities = res[i]; - // const newFileName = removeExt(file.name) + `_updated.csv`; - //swap out file with a new csv file const { newFile, cleanedEntities } = getNewCsvFile( - newEntities, - file.name + userSchema.userData, + newFileName ); + file.meta = parsedF.meta; file.hasEditClick = true; file.parsedData = cleanedEntities; - // file.name = newFileName; + file.name = newFileName; file.originFileObj = newFile; file.originalFileObj = newFile; - }); - setTimeout(() => { - //inside a timeout for cypress purposes - window.toastr.success( - `Added Fixed Up File${ - allFiles.length > 1 ? "s" : "" - } ${allFiles.map(({ file }) => file.name).join(", ")}` - ); - }, 200); + } } + } else { + toKeep.push(file); } - } else { - toKeep.push(...cleanedAccepted); } + if (filesWIssues.length) { + const { file } = filesWIssues[0]; + const allFiles = [...filesWIssues, ...filesWOIssues]; + const doAllFilesHaveSameHeaders = allFiles.every(f => { + if (f.userSchema.fields && f.userSchema.fields.length) { + return f.userSchema.fields.every((h, i) => { + return h.path === allFiles[0].userSchema.fields[i].path; + }); + } + return false; + }); + const multipleFiles = allFiles.length > 1; + const { res } = await showUploadCsvWizardDialog( + "onUploadWizardFinish", + { + dialogProps: { + title: `Fix Up File${multipleFiles ? "s" : ""} ${ + multipleFiles ? "" : file.name ? `"${file.name}"` : "" + }` + }, + doAllFilesHaveSameHeaders, + filesWIssues: allFiles, + validateAgainstSchema + } + ); - if (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 (!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); + } } - const cleanedFileList = trimFiles( - [...toKeep, ...fileListToUse], - fileLimit - ); - handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList }); + } else { + toKeep.push(...cleanedAccepted); } + + if (toKeep.length === 0) { + window.toastr && + window.toastr.error( + `It looks like there wasn't any data in your file. Please add some data and try again` + ); + } + const cleanedFileList = trimFiles( + [...toKeep, ...fileListToUse], + fileLimit + ); + handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList }); }} {...dropzoneProps} > @@ -942,71 +981,26 @@ function UploaderInner({ isDragAccept, isDragReject, isDragActive - // isDragActive - // isDragReject - // isDragAccept }) => ( -
-
- - {contentOverride || ( -
- {innerIcon || ( - - )} - {innerText || - (minimal ? "Upload" : "Click or drag to upload")} - {validateAgainstSchema && !noBuildCsvOption && ( -
- ...or {manualEnterMessage} - {/*
- {manualEnterSubMessage} -
*/} -
- )} -
- )} -
- - {showFilesCount ? ( -
- Files: {fileList ? fileList.length : 0} -
- ) : null} -
+ )} {/* {validateAgainstSchema && } */} @@ -1188,12 +1182,9 @@ function UploaderInner({
); -} +}; -const Uploader = compose( - connect(undefined, { initializeForm: initialize }), - observer -)(UploaderInner); +const Uploader = observer(UploaderInner); export default Uploader; diff --git a/packages/ui/src/TgSelect/index.js b/packages/ui/src/TgSelect/index.js index 34728a1e..b6403e9d 100644 --- a/packages/ui/src/TgSelect/index.js +++ b/packages/ui/src/TgSelect/index.js @@ -512,7 +512,6 @@ export const itemListPredicate = (_queryString = "", items, isSimpleSearch) => { export function simplesearch(needle, haystack) { return (haystack || "").indexOf(needle) !== -1; } - function tagOptionRender(vals) { if (vals.noTagStyle) return vals.label; return ; diff --git a/packages/ui/src/UploadCsvWizard.js b/packages/ui/src/UploadCsvWizard.js index 92595324..bf9a7918 100644 --- a/packages/ui/src/UploadCsvWizard.js +++ b/packages/ui/src/UploadCsvWizard.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { reduxForm, change, formValueSelector, destroy } from "redux-form"; import { Callout, Icon, Intent, Tab, Tabs } from "@blueprintjs/core"; import immer from "immer"; @@ -12,10 +12,11 @@ import { tgFormValueSelector } from "./utils/tgFormValues"; import { some } from "lodash-es"; import { times } from "lodash-es"; import DialogFooter from "./DialogFooter"; -import DataTable, { removeCleanRows } from "./DataTable"; +import DataTable from "./DataTable"; +import { removeCleanRows } from "./DataTable/utils"; import wrapDialog from "./wrapDialog"; import { omit } from "lodash-es"; -import { connect } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { MatchHeaders } from "./MatchHeaders"; import { isEmpty } from "lodash-es"; import { addSpecialPropToAsyncErrs } from "./FormComponents/tryToMatchSchemas"; @@ -35,64 +36,62 @@ const UploadCsvWizardDialog = compose( reduxForm({ form: "UploadCsvWizardDialog" }), - connect( - (state, props) => { - if (props.filesWIssues.length > 0) { - const reduxFormEntitiesArray = []; - const finishedFiles = props.filesWIssues.map((f, i) => { - const { reduxFormEntities, reduxFormCellValidation } = - formValueSelector(`editableCellTable-${i}`)( - state, - "reduxFormEntities", - "reduxFormCellValidation" - ); - reduxFormEntitiesArray.push(reduxFormEntities); - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation - ); - return ( - entsToUse && - entsToUse.length && - !some(validationToUse, v => v) && - entsToUse - ); - }); - return { - reduxFormEntitiesArray, - finishedFiles - }; - } - }, - { changeForm: change, destroyForms: destroy } - ), observer )(function UploadCsvWizardDialogOuter({ - validateAgainstSchema, - reduxFormEntitiesArray, - filesWIssues: _filesWIssues, - finishedFiles, - onUploadWizardFinish, - doAllFilesHaveSameHeaders, - destroyForms, csvValidationIssue, + doAllFilesHaveSameHeaders, + filesWIssues: _filesWIssues, + flippedMatchedHeaders, ignoredHeadersMsg, - searchResults, matchedHeaders, + onUploadWizardFinish, + searchResults, userSchema, - flippedMatchedHeaders, - changeForm + validateAgainstSchema }) { + const dispatch = useDispatch(); // will unmount state hook - React.useEffect(() => { + useEffect(() => { return () => { - destroyForms( - "editableCellTable", - ...times(_filesWIssues.length, i => `editableCellTable-${i}`) + dispatch( + destroy( + "editableCellTable", + ...times(_filesWIssues.length, i => `editableCellTable-${i}`) + ) ); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [_filesWIssues.length, dispatch]); + + const changeForm = (...args) => dispatch(change(...args)); + const { reduxFormEntitiesArray, finishedFiles } = useSelector(state => { + if (_filesWIssues.length > 0) { + const reduxFormEntitiesArray = []; + const finishedFiles = _filesWIssues.map((f, i) => { + const { reduxFormEntities, reduxFormCellValidation } = + formValueSelector(`editableCellTable-${i}`)( + state, + "reduxFormEntities", + "reduxFormCellValidation" + ); + reduxFormEntitiesArray.push(reduxFormEntities); + const { entsToUse, validationToUse } = removeCleanRows( + reduxFormEntities, + reduxFormCellValidation + ); + return ( + entsToUse && + entsToUse.length && + !some(validationToUse, v => v) && + entsToUse + ); + }); + return { + reduxFormEntitiesArray, + finishedFiles + }; + } + }); + const [hasSubmittedOuter, setSubmittedOuter] = useState(); const [steps, setSteps] = useState(getInitialSteps(true)); @@ -118,7 +117,6 @@ const UploadCsvWizardDialog = compose( > {filesWIssues.map((f, i) => { const isGood = finishedFiles[i]; - const isThisTheLastBadFile = finishedFiles.every((ff, j) => { if (i === j) { return true; @@ -135,108 +133,98 @@ const UploadCsvWizardDialog = compose( {" "} + />{" "} {f.file.name}
} panel={ { - setSubmittedOuter(false); - setSteps(getInitialSteps(true)); - }), - onMultiFileUploadSubmit: async () => { - let nextUnfinishedFile; - //find the next unfinished file - for ( - let j = (i + 1) % finishedFiles.length; - j < finishedFiles.length; - j++ - ) { - if (j === i) { - break; - } else if (!finishedFiles[j]) { - nextUnfinishedFile = j; - break; - } else if (j === finishedFiles.length - 1) { - j = -1; - } + isThisTheLastBadFile={isThisTheLastBadFile} + onBackClick={ + doAllFilesHaveSameHeaders && + (() => { + setSubmittedOuter(false); + setSteps(getInitialSteps(true)); + }) + } + onMultiFileUploadSubmit={async () => { + let nextUnfinishedFile; + //find the next unfinished file + for ( + let j = (i + 1) % finishedFiles.length; + j < finishedFiles.length; + j++ + ) { + if (j === i) { + break; + } else if (!finishedFiles[j]) { + nextUnfinishedFile = j; + break; + } else if (j === finishedFiles.length - 1) { + j = -1; } - - if (nextUnfinishedFile !== undefined) { - //do async validation here if needed - - const currentEnts = - reduxFormEntitiesArray[focusedTab]; - + } + if (nextUnfinishedFile !== undefined) { + //do async validation here if needed + const currentEnts = reduxFormEntitiesArray[focusedTab]; + if ( + await asyncValidateHelper( + validateAgainstSchema, + currentEnts, + changeForm, + `editableCellTable-${focusedTab}` + ) + ) + return; + setFocusedTab(nextUnfinishedFile); + } else { + //do async validation here if needed + for (const [i, ents] of finishedFiles.entries()) { if ( await asyncValidateHelper( validateAgainstSchema, - currentEnts, + ents, changeForm, - `editableCellTable-${focusedTab}` + `editableCellTable-${i}` ) ) return; - - setFocusedTab(nextUnfinishedFile); - } else { - //do async validation here if needed - - for (const [i, ents] of finishedFiles.entries()) { - if ( - await asyncValidateHelper( - validateAgainstSchema, - ents, - changeForm, - `editableCellTable-${i}` - ) - ) - return; - } - - //we are done - onUploadWizardFinish({ - res: finishedFiles.map(ents => { - return maybeStripIdFromEntities( - ents, - f.validateAgainstSchema - ); - }) - }); } - }, - validateAgainstSchema, - reduxFormEntitiesArray, - filesWIssues, - finishedFiles, - onUploadWizardFinish, - doAllFilesHaveSameHeaders, - destroyForms, - setFilesWIssues, - csvValidationIssue, - ignoredHeadersMsg, - searchResults, - matchedHeaders, - userSchema, - flippedMatchedHeaders, - // reduxFormEntities, - changeForm, - fileIndex: i, - form: `correctCSVHeadersForm-${i}`, - datatableFormName: `editableCellTable-${i}`, - ...f, - ...(doAllFilesHaveSameHeaders && { - csvValidationIssue: false - }) + //we are done + onUploadWizardFinish({ + res: finishedFiles.map(ents => { + return maybeStripIdFromEntities( + ents, + f.validateAgainstSchema + ); + }) + }); + } }} + validateAgainstSchema={validateAgainstSchema} + reduxFormEntitiesArray={reduxFormEntitiesArray} + filesWIssues={filesWIssues} + finishedFiles={finishedFiles} + onUploadWizardFinish={onUploadWizardFinish} + doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} + setFilesWIssues={setFilesWIssues} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeader={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + changeForm={changeForm} + fileIndex={i} + form={`correctCSVHeadersForm-${i}`} + datatableFormName={`editableCellTable-${i}`} + {...f} + {...(doAllFilesHaveSameHeaders && { + csvValidationIssue: false + })} /> } - > + /> ); })} @@ -248,34 +236,27 @@ const UploadCsvWizardDialog = compose( comp = ( <> {doAllFilesHaveSameHeaders && ( - + )} {!hasSubmittedOuter && ( { - return `editableCellTable-${i}`; - }), - reduxFormEntitiesArray, - // onMultiFileUploadSubmit, - csvValidationIssue, - ignoredHeadersMsg, - searchResults, - matchedHeaders, - userSchema, - flippedMatchedHeaders, - // reduxFormEntities, - changeForm, - setFilesWIssues, - filesWIssues, - fileIndex: 0, - ...filesWIssues[0] - }} + doAllFilesHaveSameHeaders={doAllFilesHaveSameHeaders} + datatableFormNames={filesWIssues.map((f, i) => { + return `editableCellTable-${i}`; + })} + reduxFormEntitiesArray={reduxFormEntitiesArray} + csvValidationIssue={csvValidationIssue} + ignoredHeadersMsg={ignoredHeadersMsg} + searchResults={searchResults} + matchedHeaders={matchedHeaders} + userSchema={userSchema} + flippedMatchedHeaders={flippedMatchedHeaders} + changeForm={changeForm} + setFilesWIssues={setFilesWIssues} + filesWIssues={filesWIssues} + fileIndex={0} + {...filesWIssues[0]} /> )} {hasSubmittedOuter && tabs} @@ -287,230 +268,200 @@ const UploadCsvWizardDialog = compose( setSteps(getInitialSteps(false)); }} text="Review and Edit Data" - > + /> )} ); } - return ( -
- {comp} -
- ); + return
{comp}
; } else { return ( ); } }); -const UploadCsvWizardDialogInner = compose( - reduxForm(), - connect((state, props) => { - return formValueSelector(props.datatableFormName || "editableCellTable")( - state, - "reduxFormEntities", - "reduxFormCellValidation" - ); - }) -)(function UploadCsvWizardDialogInner({ - validateAgainstSchema, - userSchema, - searchResults, - onUploadWizardFinish, - csvValidationIssue, - ignoredHeadersMsg, - matchedHeaders, - //fromRedux: - handleSubmit, - fileIndex, - reduxFormEntities, - onBackClick, - reduxFormCellValidation, - changeForm, - setFilesWIssues, - doAllFilesHaveSameHeaders, - filesWIssues, - datatableFormName = "editableCellTable", - onMultiFileUploadSubmit, - isThisTheLastBadFile, - submitting -}) { - const [hasSubmitted, setSubmitted] = useState(!csvValidationIssue); - const [steps, setSteps] = useState(getInitialSteps(csvValidationIssue)); +const UploadCsvWizardDialogInner = reduxForm()( + function UploadCsvWizardDialogInner({ + validateAgainstSchema, + userSchema, + searchResults, + onUploadWizardFinish, + csvValidationIssue, + ignoredHeadersMsg, + matchedHeaders, + handleSubmit, + fileIndex, + onBackClick, + changeForm, + setFilesWIssues, + doAllFilesHaveSameHeaders, + filesWIssues, + datatableFormName = "editableCellTable", + onMultiFileUploadSubmit, + isThisTheLastBadFile, + submitting + }) { + const [hasSubmitted, setSubmitted] = useState(!csvValidationIssue); + const [steps, setSteps] = useState(getInitialSteps(csvValidationIssue)); - let inner; - if (hasSubmitted) { - inner = ( - + const { reduxFormEntities, reduxFormCellValidation } = useSelector(state => + formValueSelector(datatableFormName)( + state, + "reduxFormEntities", + "reduxFormCellValidation" + ) ); - } else { - inner = ( - + + let inner; + if (hasSubmitted) { + inner = ( + + ); + } else { + inner = ( + + ); + } + const { entsToUse, validationToUse } = removeCleanRows( + reduxFormEntities, + reduxFormCellValidation ); - } - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation - ); - return ( -
- {!doAllFilesHaveSameHeaders && ( - - )} -
{inner}
- v)) - } - intent={ - hasSubmitted && onMultiFileUploadSubmit && isThisTheLastBadFile - ? Intent.SUCCESS - : Intent.PRIMARY - } - noCancel={onMultiFileUploadSubmit} - {...(hasSubmitted && { - onBackClick: - onBackClick || - (() => { + return ( +
+ {!doAllFilesHaveSameHeaders && ( + + )} +
{inner}
+ v)) + } + intent={ + hasSubmitted && onMultiFileUploadSubmit && isThisTheLastBadFile + ? Intent.SUCCESS + : Intent.PRIMARY + } + noCancel={onMultiFileUploadSubmit} + {...(hasSubmitted && { + onBackClick: + onBackClick || + (() => { + setSteps( + immer(steps, draft => { + draft[0].active = true; + draft[0].completed = false; + draft[1].active = false; + }) + ); + setSubmitted(false); + }) + })} + onClick={handleSubmit(async function () { + if (!hasSubmitted) { + //step 1 submit setSteps( immer(steps, draft => { - draft[0].active = true; - draft[0].completed = false; - draft[1].active = false; + draft[0].active = false; + draft[0].completed = true; + draft[1].active = true; }) ); - setSubmitted(false); - }) - })} - onClick={handleSubmit(async function () { - if (!hasSubmitted) { - //step 1 submit - setSteps( - immer(steps, draft => { - draft[0].active = false; - draft[0].completed = true; - draft[1].active = true; - }) - ); - setSubmitted(true); - } else { - if (!onMultiFileUploadSubmit) { - //do async validation here if needed - if ( - await asyncValidateHelper( - validateAgainstSchema, - entsToUse, - changeForm, - `editableCellTable` + setSubmitted(true); + } else { + if (!onMultiFileUploadSubmit) { + //do async validation here if needed + if ( + await asyncValidateHelper( + validateAgainstSchema, + entsToUse, + changeForm, + `editableCellTable` + ) ) - ) - return; + return; + } + //step 2 submit + const payload = maybeStripIdFromEntities( + entsToUse, + validateAgainstSchema + ); + return onMultiFileUploadSubmit + ? await onMultiFileUploadSubmit() + : onUploadWizardFinish({ res: [payload] }); } - //step 2 submit - const payload = maybeStripIdFromEntities( - entsToUse, - validateAgainstSchema - ); - return onMultiFileUploadSubmit - ? await onMultiFileUploadSubmit() - : onUploadWizardFinish({ res: [payload] }); - } - })} - style={{ alignSelf: "end" }} - > -
- ); -}); + })} + style={{ alignSelf: "end" }} + /> +
+ ); + } +); export default UploadCsvWizardDialog; const exampleData = { userData: times(5).map(() => ({ _isClean: true })) }; -export const PreviewCsvData = observer(function (props) { + +export const PreviewCsvData = observer(props => { const { matchedHeaders, isEditingExistingFile, showDoesDataLookCorrectMsg, headerMessage, datatableFormName, - // onlyShowRowsWErrors, validateAgainstSchema, userSchema = exampleData, initialEntities } = props; const rerenderKey = useRef(0); rerenderKey.current = rerenderKey.current + 1; - // const useExampleData = userSchema === exampleData; - // const [loading, setLoading] = useState(true); - // useEffect(() => { - // // simulate layout change outside of React lifecycle - // setTimeout(() => { - // setLoading(false); - // }, 400); - // }, []); - - // const [val, forceUpdate] = useForceUpdate(); - const data = userSchema.userData && userSchema.userData.length && @@ -575,7 +526,7 @@ export const PreviewCsvData = observer(function (props) { + /> )} + /> ); }); @@ -608,14 +559,12 @@ export const SimpleInsertDataDialog = compose( "reduxFormEntities", "reduxFormCellValidation" ), - connect(undefined, { changeForm: change }), observer )(function SimpleInsertDataDialog({ onSimpleInsertDialogFinish, reduxFormEntities, reduxFormCellValidation, validateAgainstSchema, - changeForm, submitting, isEditingExistingFile, matchedHeaders, @@ -625,11 +574,14 @@ export const SimpleInsertDataDialog = compose( userSchema, initialEntities }) { + const dispatch = useDispatch(); const { entsToUse, validationToUse } = removeCleanRows( reduxFormEntities, reduxFormCellValidation ); + const changeForm = (...args) => dispatch(change(...args)); + return ( <>
@@ -642,20 +594,17 @@ export const SimpleInsertDataDialog = compose( label="File Name:" defaultValue={"manual_data_entry"} name="fileName" - > + /> + matchedHeaders={matchedHeaders} + isEditingExistingFile={isEditingExistingFile} + showDoesDataLookCorrectMsg={showDoesDataLookCorrectMsg} + headerMessage={headerMessage} + validateAgainstSchema={validateAgainstSchema} + userSchema={userSchema} + initialEntities={initialEntities} + datatableFormName={"simpleInsertEditableTable"} + />
e)} text={isEditingExistingFile ? "Edit Data" : "Add File"} - > + /> ); }); @@ -715,11 +664,3 @@ function maybeStripIdFromEntities(ents, validateAgainstSchema) { } return toRet?.map(e => omit(e, ["_isClean"])); } - -//create your forceUpdate hook -// function useForceUpdate() { -// const [val, setValue] = useState(0); // integer state -// return [val, () => setValue(value => value + 1)]; // update state to force render -// // A function that increment 👆🏻 the previous state like here -// // is better than directly setting `setValue(value + 1)` -// } diff --git a/packages/ui/src/index.js b/packages/ui/src/index.js index 72816cc1..ca5addc5 100644 --- a/packages/ui/src/index.js +++ b/packages/ui/src/index.js @@ -18,11 +18,11 @@ export { } from "./DataTable/utils/withSelectedEntities"; export { default as DataTable, - ConnectedPagingTool as PagingTool, - removeCleanRows + ConnectedPagingTool as PagingTool } from "./DataTable"; +export { removeCleanRows } from "./DataTable/utils"; -export { default as getIdOrCodeOrIndex } from "./DataTable/utils/getIdOrCodeOrIndex"; +export { getIdOrCodeOrIndex } from "./DataTable/utils"; export { default as convertSchema } from "./DataTable/utils/convertSchema"; export { default as Loading } from "./Loading"; export { throwFormError } from "./throwFormError"; diff --git a/packages/ui/src/showDialogOnDocBody.js b/packages/ui/src/showDialogOnDocBody.js index 4d4f8bd9..2f1452d4 100644 --- a/packages/ui/src/showDialogOnDocBody.js +++ b/packages/ui/src/showDialogOnDocBody.js @@ -1,4 +1,4 @@ -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import React from "react"; // import withDialog from "./enhancers/withDialog"; import { Dialog } from "@blueprintjs/core"; @@ -19,19 +19,15 @@ export default function showDialogOnDocBody(DialogComp, options = {}) { DialogCompToUse = props => { return ( - + ); }; } else { DialogCompToUse = DialogComp; } - ReactDOM.render( - , - dialogHolder + const root = createRoot(dialogHolder); + root.render( + ); } diff --git a/packages/ui/src/useDialog.js b/packages/ui/src/useDialog.js index 38b1f65e..a9f559c6 100644 --- a/packages/ui/src/useDialog.js +++ b/packages/ui/src/useDialog.js @@ -1,6 +1,6 @@ import React, { useState } from "react"; -/* +/* const {toggleDialog, comp} = useDialog({ ModalComponent: SimpleInsertData, @@ -31,12 +31,14 @@ export const useDialog = ({ ModalComponent, ...rest }) => { ...rest?.dialogProps, ...additionalProps?.dialogProps }} - > + /> ); + const toggleDialog = () => { setOpen(!isOpen); }; - async function showDialogPromise(handlerName, moreProps = {}) { + + const showDialogPromise = async (handlerName, moreProps = {}) => { return new Promise(resolve => { //return a promise that can be awaited setAdditionalProps({ @@ -59,6 +61,7 @@ export const useDialog = ({ ModalComponent, ...rest }) => { }); setOpen(true); //open the dialog }); - } + }; + return { comp, showDialogPromise, toggleDialog, setAdditionalProps }; }; diff --git a/packages/ui/src/utils/renderOnDoc.js b/packages/ui/src/utils/renderOnDoc.js index 2ce4b142..e36f1e14 100644 --- a/packages/ui/src/utils/renderOnDoc.js +++ b/packages/ui/src/utils/renderOnDoc.js @@ -1,29 +1,32 @@ -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; export function renderOnDoc(fn) { const elemDiv = document.createElement("div"); elemDiv.style.cssText = "position:absolute;width:100%;height:100%;top:0px;opacity:0.3;z-index:0;"; document.body.appendChild(elemDiv); + const root = createRoot(elemDiv); const handleClose = () => { setTimeout(() => { - ReactDOM.unmountComponentAtNode(elemDiv); + root.unmount(elemDiv); document.body.removeChild(elemDiv); }); }; - return ReactDOM.render(fn(handleClose), elemDiv); + root.render(fn(handleClose)); } + export function renderOnDocSimple(el) { const elemDiv = document.createElement("div"); elemDiv.style.cssText = "position:absolute;width:100%;height:100%;top:0px;opacity:1;z-index:10000;"; document.body.appendChild(elemDiv); + const root = createRoot(elemDiv); + root.render(el); const handleClose = () => { setTimeout(() => { - ReactDOM.unmountComponentAtNode(elemDiv); + root.unmount(); document.body.removeChild(elemDiv); }); }; - ReactDOM.render(el, elemDiv); return handleClose; }