From 215d93e668ee8fac426ce8fcf51e04e6d2612d9a Mon Sep 17 00:00:00 2001 From: Thomas Rich Date: Mon, 29 Jan 2024 15:22:51 -0800 Subject: [PATCH] more work adding excel style functionality to table --- packages/ui/cypress/e2e/ExcelTable.spec.js | 9 ++ .../ui/demo/src/examples/EditableCellTable.js | 54 +------ packages/ui/demo/src/examples/ExcelTable.js | 147 ++++++++++++++++++ packages/ui/demo/src/index.js | 4 + packages/ui/src/DataTable/editCellHelper.js | 99 ++++++++++-- packages/ui/src/DataTable/index.js | 80 +++++----- 6 files changed, 291 insertions(+), 102 deletions(-) create mode 100644 packages/ui/cypress/e2e/ExcelTable.spec.js create mode 100644 packages/ui/demo/src/examples/ExcelTable.js diff --git a/packages/ui/cypress/e2e/ExcelTable.spec.js b/packages/ui/cypress/e2e/ExcelTable.spec.js new file mode 100644 index 00000000..3d9ec855 --- /dev/null +++ b/packages/ui/cypress/e2e/ExcelTable.spec.js @@ -0,0 +1,9 @@ +describe("ExcelTable.spec", () => { + it(`adding rows should update formula correctly`, () => { + cy.visit("#/DataTable%20-%20ExcelTable"); + cy.get(`[data-test="tgCell_Thing 1"]:contains(88)`).rightclick(); + cy.contains("Add Row Above").click(); + + }); + +}); diff --git a/packages/ui/demo/src/examples/EditableCellTable.js b/packages/ui/demo/src/examples/EditableCellTable.js index 8b8f6016..a6483451 100644 --- a/packages/ui/demo/src/examples/EditableCellTable.js +++ b/packages/ui/demo/src/examples/EditableCellTable.js @@ -7,7 +7,6 @@ import DemoWrapper from "../DemoWrapper"; import { useToggle } from "../useToggle"; import OptionsSection from "../OptionsSection"; import { toNumber } from "lodash"; -// import ExcelCell from "packages/ui/src/ExcelCell"; const chance = new Chance(); function getEnts(num) { @@ -54,58 +53,8 @@ export default function SimpleTable(p) { const [allowFormulas, allowFormulasComp] = useToggle({ type: "allowFormulas" }); - // const [tagValuesAsObjects, tagValuesAsObjectsComp] = useToggle({ - // type: "tagValuesAsObjects" - // }); - let entsToUse; const [entities, setEnts] = useState([]); - entsToUse = entities; - - // const depGraph = { - // a1: ["a3"], - // a2: ["a1"], - // a3: [], - // b1: ["a1", "a2"], - // b2: ["a2"], - // b3: ["a3"] - // }; const schema = useMemo(() => { - if (allowFormulas) { - // eslint-disable-next-line react-hooks/exhaustive-deps - entsToUse = [ - // { - // id: 'asdfoi', - // a: "=sum(b1,a2)", - // b: 44 - // }, - // { - // id: 'f22f2f', - // a: "=sum(b2)", - // b: 44 - // }, - { - id: "asdfoi", - a: "=sum(b1,a2)", - b: 44 - }, - { - id: "f22f2f", - a: "=sum(b1,b2,a1)", - b: 44 - } - // { - // id: '22f3f', - // a: "=sum(a1,b3)", - // b: 44 - // } - ]; - return { - fields: [ - { path: "a", allowFormulas: true }, - { path: "b", allowFormulas: true } - ] - }; - } return { fields: [ { @@ -163,7 +112,6 @@ export default function SimpleTable(p) { }, [defaultValAsFunc, allowFormulas]); return (
- {/* */} {numComp} {defaultValAsFuncComp} @@ -177,7 +125,7 @@ export default function SimpleTable(p) { formName="editableCellTable" isSimple isCellEditable - entities={entsToUse} + entities={entities} schema={schema} // isEntityDisabled={ // isEntityDisabled diff --git a/packages/ui/demo/src/examples/ExcelTable.js b/packages/ui/demo/src/examples/ExcelTable.js new file mode 100644 index 00000000..97058678 --- /dev/null +++ b/packages/ui/demo/src/examples/ExcelTable.js @@ -0,0 +1,147 @@ +import React, { useMemo, useRef, useState } from "react"; +import DataTable from "../../../src/DataTable"; +import DemoWrapper from "../DemoWrapper"; +import { useToggle } from "../useToggle"; +import OptionsSection from "../OptionsSection"; +import { nanoid } from "nanoid"; +import { forEach, map } from "lodash"; +import { getColLetFromIndex } from "packages/ui/src/DataTable/editCellHelper"; +// import ExcelCell from "packages/ui/src/ExcelCell"; + +export default function SimpleTable(p) { + const key = useRef(0); + const [simpleCircularLoop, simpleCircularLoopComp] = useToggle({ + type: "simpleCircularLoop" + }); + const [manyColumns, manyColumnsComp] = useToggle({ + type: "manyColumns" + }); + const [simpleRangeExample, simpleRangeExampleComp] = useToggle({ + type: "simpleRangeExample" + }); + const [entities, _setEnts] = useState([]); + const setEnts = ents => { + _setEnts(ents.map(e => ({ id: nanoid(), ...e }))); + }; + const schema = useMemo(() => { + key.current++; + if (simpleCircularLoop) { + setEnts([ + { + a: "=sum(b1,a2)", + b: 44 + }, + { + a: "=sum(b1,b2,a1)", + b: 44 + } + ]); + return { + fields: [ + { path: "a", allowFormulas: true }, + { path: "b", allowFormulas: true } + ] + }; + } + if (manyColumns) { + setEnts([ + { + a: "=sum(cc1:cc3)", + b: 44, + cc: 87 + }, + { + a: "=sum(b1,b2,a1)", + b: 44, + aa: 42, + cc: 88 + } + ,{ + a: "=sum(1:1)", + aa: 42, + cc: 89, + } + ,{ + a: "=sum(aa:aa)", + aa: 42, + cc: 89, + } + ]); + return { + fields: [ + ...map(new Array(100), (v, i) => ({ + path: getColLetFromIndex(i).toLowerCase(), + allowFormulas: true + })) + ] + }; + } + if (simpleRangeExample) { + setEnts([ + { + a: "=sum(b1:b3)", + b: 44 + }, + { + a: "=sum(B:B)", + b: 44 + }, + { + a: "=sum(2:2)", + b: 44 + } + ]); + return { + fields: [ + { path: "a", allowFormulas: true }, + { path: "b", allowFormulas: true }, + { path: "c", allowFormulas: true } + ] + }; + } + setEnts([ + { + "Thing 1": "=sum(b1,a2)", + thing2: 44 + }, + { + "Thing 1": "=sum(b1,c1)", + thing2: 44, + c: "=e1" + } + ]); + + return { + fields: [ + { path: "Thing 1", allowFormulas: true }, + { path: "thing2", allowFormulas: true }, + { path: "c", allowFormulas: true }, + { path: "d", allowFormulas: true }, + { path: "e", allowFormulas: true } + ] + }; + }, [simpleCircularLoop, manyColumns, simpleRangeExample]); + return ( +
+ {/* */} + + {simpleCircularLoopComp} + {manyColumnsComp} + {simpleRangeExampleComp} + + + + +
+ ); +} diff --git a/packages/ui/demo/src/index.js b/packages/ui/demo/src/index.js index 540de0f2..b7ed754f 100644 --- a/packages/ui/demo/src/index.js +++ b/packages/ui/demo/src/index.js @@ -26,6 +26,7 @@ import ScrollToTopDemo from "./examples/ScrollToTop"; import showAppSpinnerDemo from "./examples/showAppSpinnerDemo"; import EditableCellTable from "./examples/EditableCellTable"; +import ExcelTable from "./examples/ExcelTable"; import "./style.css"; import React from "react"; import { render } from "react-dom"; @@ -49,6 +50,9 @@ const demos = { "DataTable - EditableCellTable": { demo: EditableCellTable }, + "DataTable - ExcelTable": { + demo: ExcelTable + }, "DataTable - SimpleTable": { demo: SimpleTable }, diff --git a/packages/ui/src/DataTable/editCellHelper.js b/packages/ui/src/DataTable/editCellHelper.js index e9348ee5..e5fc70da 100644 --- a/packages/ui/src/DataTable/editCellHelper.js +++ b/packages/ui/src/DataTable/editCellHelper.js @@ -1,4 +1,4 @@ -import { isString, set } from "lodash"; +import { get, isNumber, isString, set, toNumber } from "lodash"; import { defaultValidators } from "./defaultValidators"; import { defaultFormatters } from "./defaultFormatters"; import { evaluate } from "mathjs"; @@ -66,7 +66,6 @@ export const editCellHelper = ({ let hasFormula = false; if (colSchema.allowFormulas && typeof nv === "string" && nv[0] === "=") { const ogFormula = nv; - // console.log(`ogFormula:`, ogFormula); // if the nv is missing a closing paren, add it // count the number of open parens // count the number of close parens @@ -80,20 +79,32 @@ export const editCellHelper = ({ // if the nv is not a valid formula, return the error // fill in any variables with their values let error; - nv = nv.toLowerCase().replace(/([A-Z]+[0-9]+)/gi, _match => { + // if nv contains : then it is a range + // if (nv.includes(":")) { + // // replace the range with the the values of the range + // // get the start and end of the range + + // } + const { rangeErr, replacedFormula } = replaceFormulaRanges({ + formula: nv, + schema, + entities + }); + error = rangeErr; + nv = replacedFormula; + nv = nv.replace(/([A-Z]+[0-9]+)/gi, _match => { const match = _match.toUpperCase(); if (updateGroup[match] === "__Currently&&Updating__") { error = `Circular Loop Detected between ${cellAlphaNum} and ${match}`; return "circular_loop_detected"; } - - // match = E12 or B4 + // match will equal E12 or B4 for example const [letter, rowIndex] = match.split(/(\d+)/); const entity = entities.find((e, i) => { return i === rowIndex - 1; }); const columns = schema.fields; - const letterIndex = letter.toUpperCase().charCodeAt(0) - 65; + const letterIndex = lettersToNumber(letter); const col = columns[letterIndex]; if (!col) { return match; @@ -102,7 +113,7 @@ export const editCellHelper = ({ if (!entity) return match; let val = entity[path]; - if (val === undefined) return match; + if (val === undefined) return 0; if (val?.formula) { val = val.formula; } @@ -124,8 +135,11 @@ export const editCellHelper = ({ ..._errors }; val = value?.formula ? value.value : value; + } else if (!isNaN(toNumber(val))) { + return val + } else if (isString(val)) { + return 0 } - return val; }); @@ -232,7 +246,7 @@ export const editCellHelper = ({ function getCellAlphaNum({ entities, entity, colSchema, schema }) { const rowIndex = entities.indexOf(entity) + 1; const colIndex = schema.fields.indexOf(colSchema); - const colLetter = String.fromCharCode(65 + colIndex); + const colLetter = getColLetFromIndex(colIndex); const cellAlphaNum = `${colLetter}${rowIndex}`; return cellAlphaNum; } @@ -240,3 +254,70 @@ function getCellAlphaNum({ entities, entity, colSchema, schema }) { const hasErrors = errors => { return Object.values(errors).some(e => e); }; + +export const getColLetFromIndex = index => { + if (index > 25) + return ( + getColLetFromIndexHelper(index / 26 - 1) + + getColLetFromIndexHelper(index % 26) + ); + return getColLetFromIndexHelper(index); +}; +const getColLetFromIndexHelper = index => { + return String.fromCharCode(65 + index); +}; + +const lettersToNumber = letters => { + let n = 0; + for (let p = 0; p < letters.length; p++) { + n = letters[p].charCodeAt() - 64 + n * 26; + } + return n - 1; +}; + +export const replaceFormulaRanges = ({ formula, schema, entities }) => { + let error; + const replaced = formula + .toLowerCase() + .replace(/([A-Z]*[0-9]*:[A-Z]*[0-9]*)/gi, _match => { + // if (_match.includes(":")) { + // console.log(`_match:`, _match); + // } + const match = _match.toUpperCase(); + const [start, end] = match.split(":"); + const [startLetter, _startRowIndex] = start.split(/(\d+)/); + const [endLetter, _endRowIndex] = end.split(/(\d+)/); + let startRowIndex = parseInt(_startRowIndex); + let endRowIndex = parseInt(_endRowIndex); + let toRet = ""; + + if (startLetter !== endLetter) { + error = `Ranges must be in the same column`; + return "range_in_different_columns"; + } + if (!startLetter && !endLetter && _startRowIndex === _endRowIndex) { + // we have a range like 1:1 + const rowIndex = startRowIndex; + const startColIndex = 1; + const endColIndex = schema.fields.length; + for (let i = startColIndex; i <= endColIndex; i++) { + const colLet = getColLetFromIndex(i - 1); + toRet += `${colLet}${rowIndex}${i === endColIndex ? "" : ","}`; + } + return toRet; + } + if (_startRowIndex === undefined && _endRowIndex === undefined) { + // we have a range like A:A + startRowIndex = 1; + endRowIndex = entities.length; + } + for (let j = startRowIndex; j <= endRowIndex; j++) { + toRet += `${startLetter}${j}${j === endRowIndex ? "" : ","}`; + } + return toRet; + }); + return { + replacedFormula: replaced, + error + }; +}; diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 0f39d13a..20d215bd 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -84,7 +84,11 @@ import { CellDragHandle } from "./CellDragHandle"; import { nanoid } from "nanoid"; import { SwitchField } from "../FormComponents"; import { validateTableWideErrors } from "./validateTableWideErrors"; -import { editCellHelper } from "./editCellHelper"; +import { + editCellHelper, + getColLetFromIndex, + replaceFormulaRanges +} from "./editCellHelper"; import { getCellVal } from "./getCellVal"; import { getVals } from "./getVals"; import { throwFormError } from "../throwFormError"; @@ -400,6 +404,7 @@ class DataTable extends React.Component { entities, { useDefaultValues, indexToStartAt } = {} ) => { + console.log(`entities:`, entities) const { schema } = this.props; const editableFields = schema.fields.filter(f => !f.isNotEditable); let validationErrors = {}; @@ -449,7 +454,12 @@ class DataTable extends React.Component { const val = ent[field.path]; if (val && typeof val === "string" && val[0] === "=") { const formula = val.slice(1); - const deps = formula.match(/[A-Z]+[0-9]+/gi); + const { error, replacedFormula } = replaceFormulaRanges({ + formula, + entities: ents, + schema + }); + const deps = replacedFormula.match(/[A-Z]+[0-9]+/gi); if (deps) { deps.forEach(_dep => { const dep = _dep.toUpperCase(); @@ -458,7 +468,7 @@ class DataTable extends React.Component { depGraph[dep] = []; } // convert the field index to a letter - const fieldLetter = String.fromCharCode(65 + fi); + const fieldLetter = getColLetFromIndex(fi); depGraph[dep].push(`${fieldLetter}${i + 1}`); }); } @@ -665,6 +675,7 @@ class DataTable extends React.Component { } else if (e.clipboardData && e.clipboardData.getData) { toPaste = e.clipboardData.getData("text/plain"); } + const htmlToPaste = e.clipboardData.getData("text/html"); const jsonToPaste = e.clipboardData.getData("application/json"); let hasReplace = false; @@ -2647,33 +2658,6 @@ class DataTable extends React.Component { if (val.formula) { return val.value; } - // if (val.startsWith("=")) { - // // fill in any variables with their values - // val = val.toLowerCase().replace(/([A-Z]+[0-9]+)/gi, match => { - // // match = E12 or B4 - // const [letter, rowIndex] = match.split(/(\d+)/); - // const entity = entities.find((e, i) => { - // return i === rowIndex - 1; - // }); - // const letterIndex = letter.toUpperCase().charCodeAt(0) - 65; - // const col = columns[letterIndex]; - // if (!col) { - // return match; - // } - // const { path } = col; - // if (!entity) return match; - // const val = entity[path]; - // if (val === undefined) return match; - // return val; - // }); - // const toEval = val.slice(1); - // try { - // val = evaluate(toEval); - // return val; - // } catch (e) { - // return "#ERROR"; - // } - // } return val; }; } else if (column.render) { @@ -3259,6 +3243,7 @@ class DataTable extends React.Component { }); const insertIndex = above ? indexToInsert : indexToInsert + 1; const insertIndexToUse = appendToBottom ? entities.length : insertIndex; + console.log(`yarr`) let { newEnts, validationErrors } = this.formatAndValidateEntities( newEntities, { @@ -3266,35 +3251,42 @@ class DataTable extends React.Component { indexToStartAt: insertIndexToUse } ); + console.log(`jarr`) newEnts = newEnts.map(e => ({ ...e, _isClean: true })); + entities.forEach(e => { + if (e.formula) { + console.log(`e.formula:`, e.formula); + } + }); this.updateValidation(entities, { ...reduxFormCellValidation, ...validationErrors }); + // we need to make sure any entities with formulas are updated entities.splice(insertIndexToUse, 0, ...newEnts); }); this.refocusTable(); }; - insertColumns = ({ above, numRows = 1, appendToBottom } = {}) => { + insertColumns = ({ toTheLeft, numColumns = 1, appendToEnd } = {}) => { const { entities = [], reduxFormCellValidation } = computePresets( this.props ); const primaryCellId = this.getPrimarySelectedCellId(); - const [rowId] = primaryCellId?.split(":") || []; + const [rowId, columnName] = primaryCellId?.split(":") || []; this.updateEntitiesHelper(entities, entities => { - const newEntities = times(numRows).map(() => ({ id: nanoid() })); + const newEntities = times(numColumns).map(() => ({ id: nanoid() })); const indexToInsert = entities.findIndex((e, i) => { return getIdOrCodeOrIndex(e, i) === rowId; }); - const insertIndex = above ? indexToInsert : indexToInsert + 1; - const insertIndexToUse = appendToBottom ? entities.length : insertIndex; + const insertIndex = toTheLeft ? indexToInsert : indexToInsert + 1; + const insertIndexToUse = appendToEnd ? entities.length : insertIndex; let { newEnts, validationErrors } = this.formatAndValidateEntities( newEntities, { @@ -3451,7 +3443,7 @@ class DataTable extends React.Component { text="Add Column Left" key="addColumnLeft" onClick={() => { - this.insertRows({ above: true }); + this.insertColumns({ toTheLeft: true }); }} > { - this.insertRows({}); + this.insertColumns({}); }} > { + const letter = getColLetFromIndex(index); + if (inner?.toUpperCase() === letter) { + inner = letter; + return null; + } + return
{getColLetFromIndex(index)}:
; + }; return (
{maybeCheckbox} - {allowFormulas &&
{String.fromCharCode(65 + index)}:
} + {allowFormulas && getLetterifiedColumnTitle()} - {renderTitleInner ? renderTitleInner : columnTitle}{" "} + {inner}{" "} )}