From 6546a4031d798199938b4414a1f582cde42c8638 Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Tue, 9 Jul 2024 12:08:53 -0400 Subject: [PATCH] refactor --- packages/ove/demo/src/utils/renderToggle.js | 70 +- packages/ove/src/AutoAnnotate.js | 27 +- packages/ove/src/CreateAnnotationsPage.js | 4 +- packages/ove/src/DigestTool/DigestTool.js | 12 +- .../GenericAnnotationProperties.js | 8 +- packages/shared-demo/src/DemoPage.js | 4 +- .../ui/cypress/e2e/EditableCellTable.spec.js | 6 +- packages/ui/cypress/e2e/dataTable.spec.js | 37 +- packages/ui/cypress/support/index.js | 8 +- packages/ui/demo/src/examples/DataTable.js | 1359 ++-- .../ui/demo/src/examples/EditableCellTable.js | 32 +- packages/ui/demo/src/index.js | 2 +- packages/ui/demo/src/renderToggle.js | 71 +- packages/ui/demo/src/useToggle.js | 89 +- packages/ui/src/DataTable/DisplayOptions.js | 361 +- packages/ui/src/DataTable/EditabelCell.js | 10 +- packages/ui/src/DataTable/PagingTool.js | 93 +- packages/ui/src/DataTable/SortableColumns.js | 83 +- packages/ui/src/DataTable/ThComponent.js | 43 + .../ui/src/DataTable/dataTableEnhancer.js | 329 +- packages/ui/src/DataTable/defaultProps.js | 45 - packages/ui/src/DataTable/index.js | 5675 +++++++++-------- .../ui/src/DataTable/utils/computePresets.js | 42 - .../ui/src/DataTable/utils/convertSchema.js | 4 +- .../ui/src/DataTable/utils/handleCopyTable.js | 16 + packages/ui/src/DataTable/utils/index.js | 4 +- packages/ui/src/DataTable/utils/rowClick.js | 32 +- .../ui/src/DataTable/utils/useTableParams.js | 368 ++ .../ui/src/DataTable/utils/withTableParams.js | 29 +- packages/ui/src/InfoHelper/index.js | 4 +- packages/ui/src/UploadCsvWizard.js | 11 +- packages/ui/src/utils/hotkeyUtils.js | 2 +- ....timestamp-1720444595414-4387a609f20aa.mjs | 196 + 33 files changed, 4724 insertions(+), 4352 deletions(-) create mode 100644 packages/ui/src/DataTable/ThComponent.js delete mode 100644 packages/ui/src/DataTable/defaultProps.js delete mode 100644 packages/ui/src/DataTable/utils/computePresets.js create mode 100644 packages/ui/src/DataTable/utils/handleCopyTable.js create mode 100644 packages/ui/src/DataTable/utils/useTableParams.js create mode 100644 packages/ui/vite.config.ts.timestamp-1720444595414-4387a609f20aa.mjs diff --git a/packages/ove/demo/src/utils/renderToggle.js b/packages/ove/demo/src/utils/renderToggle.js index 984d1370..4a69c923 100644 --- a/packages/ove/demo/src/utils/renderToggle.js +++ b/packages/ove/demo/src/utils/renderToggle.js @@ -68,34 +68,25 @@ export default function renderToggle({ } } if (isButton) { - toggleOrButton = ( - + /> ) : ( -
+
)} - + ); } diff --git a/packages/ove/src/AutoAnnotate.js b/packages/ove/src/AutoAnnotate.js index 4822f96e..53944b5e 100644 --- a/packages/ove/src/AutoAnnotate.js +++ b/packages/ove/src/AutoAnnotate.js @@ -165,8 +165,9 @@ export const AutoAnnotateModal = compose( > download example - ) with the following columns:

-

+ ) with the following columns: +
+
name,description,sequence,type, @@ -185,15 +186,15 @@ export const AutoAnnotateModal = compose( regex matching } - > + /> ,matchType
-

+
{annotationType !== "feature" && ( <> Note: the "type" column is optional -

+
)}
@@ -203,12 +204,12 @@ export const AutoAnnotateModal = compose( fileLimit={1} isRequired accept=".csv" - > + /> } id="csvFile" title="CSV" - > + /> @@ -234,7 +235,7 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`, name="apeFile" isRequired accept=".txt" - > + /> {error && (
{error}
)} @@ -242,10 +243,10 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`, } id="apeFile" title="ApE File" - >
+ /> {getCustomAutoAnnotateList && (loadingCustomAnnList ? ( - + ) : ( customAnnResponse && customAnnResponse.list && ( @@ -263,13 +264,13 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`, : customAnnsSchemaNoType } entities={customAnnResponse.list} - > + /> ) : (
No Annotations Found
) } - >
+ /> ) ))} @@ -451,7 +452,7 @@ FRT GAAGTTCCTATTCTCTAGAAAGTATAGGAACTTC misc_recomb orchid pink 0 0`, } })} text="Annotate" - > + /> ); }); diff --git a/packages/ove/src/CreateAnnotationsPage.js b/packages/ove/src/CreateAnnotationsPage.js index 7e7a9838..b5458d34 100644 --- a/packages/ove/src/CreateAnnotationsPage.js +++ b/packages/ove/src/CreateAnnotationsPage.js @@ -48,7 +48,7 @@ export default compose( }))} withCheckboxes schema={annotationType === "feature" ? schemaFeatures : schemaOther} - > + /> + /> ); }); diff --git a/packages/ove/src/DigestTool/DigestTool.js b/packages/ove/src/DigestTool/DigestTool.js index 2db162ff..3aee6f47 100644 --- a/packages/ove/src/DigestTool/DigestTool.js +++ b/packages/ove/src/DigestTool/DigestTool.js @@ -18,7 +18,7 @@ import { } from "@blueprintjs/core"; import withEditorInteractions from "../withEditorInteractions"; import { userDefinedHandlersAndOpts } from "../Editor/userDefinedHandlersAndOpts"; -import { noop, pick } from "lodash-es"; +import { pick } from "lodash-es"; const MAX_DIGEST_CUTSITES = 50; const MAX_PARTIAL_DIGEST_CUTSITES = 10; @@ -30,7 +30,7 @@ export class DigestTool extends React.Component { // height = 100, dimensions = {}, lanes, - digestTool: { selectedFragment, computePartialDigest }, + digestTool: { computePartialDigest }, onDigestSave, computePartialDigestDisabled, computeDigestDisabled, @@ -131,14 +131,6 @@ export class DigestTool extends React.Component { onSingleRowSelect={({ onFragmentSelect }) => { onFragmentSelect(); }} - reduxFormSelectedEntityIdMap={{ - input: { - value: { - [selectedFragment]: true - }, - onChange: noop - } - }} formName="digestInfoTable" entities={lanes[0].map( ({ id, cut1, cut2, start, end, size, ...rest }) => { diff --git a/packages/ove/src/helperComponents/PropertiesDialog/GenericAnnotationProperties.js b/packages/ove/src/helperComponents/PropertiesDialog/GenericAnnotationProperties.js index bffd3470..201f5526 100644 --- a/packages/ove/src/helperComponents/PropertiesDialog/GenericAnnotationProperties.js +++ b/packages/ove/src/helperComponents/PropertiesDialog/GenericAnnotationProperties.js @@ -233,7 +233,7 @@ const genericAnnotationProperties = ({ ) }); }} - > + /> + /> {["feature"].includes(annotationType) && ( @@ -291,7 +291,7 @@ const genericAnnotationProperties = ({ intent="danger" disabled={!annotationPropertiesSelectedEntities.length} icon="trash" - > + /> )} @@ -399,6 +399,6 @@ export function getVisFilter(submenu) { className="propertiesVisFilter" data-tip="Visibility Filter" menu={{submenu}} - > + /> ); } diff --git a/packages/shared-demo/src/DemoPage.js b/packages/shared-demo/src/DemoPage.js index 6c5ed778..dd235055 100644 --- a/packages/shared-demo/src/DemoPage.js +++ b/packages/shared-demo/src/DemoPage.js @@ -68,7 +68,7 @@ const DemoPage = ({ moduleName, demos, showComponentList }) => { minimal intent="primary" icon="chevron-right" - > + /> ) } packageName={`${moduleName}`} @@ -173,7 +173,7 @@ const DemoComponentWrapper = ({ } else { component = ( <> - + {!!props.length && ( <>
{ it(`cell checkboxes and the header checkbox should work`, () => { @@ -170,7 +171,7 @@ describe("EditableCellTable.spec", () => { // }); it(`arrow keys should work together with shift and dragging should work`, () => { cy.visit("#/DataTable%20-%20EditableCellTable"); - cy.get(`[data-test="tgCell_howMany"]`).eq(3).click({ force: true }); + cy.get(`[data-test="tgCell_howMany"]`).eq(3).click(); cy.focused().type(`{leftArrow}`); cy.get( `.rt-td.isSelectedCell.isPrimarySelected [data-test="tgCell_weather"]` @@ -239,8 +240,7 @@ describe("EditableCellTable.spec", () => { ); }); it(`undo/redo should work`, () => { - const IS_LINUX = - window.navigator.platform.toLowerCase().search("linux") > -1; + const IS_LINUX = os.platform().toLowerCase().search("linux") > -1; const undoCmd = IS_LINUX ? `{alt}z` : "{meta}z"; const redoCmd = IS_LINUX ? `{alt}{shift}z` : "{meta}{shift}z"; cy.visit("#/DataTable%20-%20EditableCellTable"); diff --git a/packages/ui/cypress/e2e/dataTable.spec.js b/packages/ui/cypress/e2e/dataTable.spec.js index 33f1eef8..9eb95b72 100644 --- a/packages/ui/cypress/e2e/dataTable.spec.js +++ b/packages/ui/cypress/e2e/dataTable.spec.js @@ -1,7 +1,9 @@ +import { version } from "../../package.json"; + describe("dataTable.spec", () => { it("can click the first row of a table that has a scroll bar (aka cypress should not incorrectly scroll the top row under the header)", () => { cy.visit("#/DataTable?pageSize=100"); - cy.get(`.rt-tr-group[data-test-id="1"] .rt-tr .rt-td`).first().click(); + cy.get(`.rt-tr-group .rt-tr .rt-td`).first().click(); }); it("can add a custom class name to a row in the table", () => { cy.visit("#/DataTable"); @@ -13,14 +15,19 @@ describe("dataTable.spec", () => { cy.visit("#/DataTable"); cy.contains("0 Selected"); //select first entity - cy.get(`[data-test="tgCell_type.special"]`).first().click(); + cy.get(`.rt-tr-group .rt-tr .rt-td`).first().click(); cy.contains("1 Selected"); //go to next page cy.get(".data-table-footer .paging-arrow-right").click(); + cy.contains("1 Selected"); //select another entity - cy.get(`[data-test="tgCell_type.special"]`).first().click(); + cy.get(`.rt-tr-group .rt-tr .rt-td`).first().click(); cy.contains("2 Selected"); + // go to previous page and deselect the first entity + cy.get(".data-table-footer .paging-arrow-left").click(); + cy.get(`.rt-tr-group .rt-tr .rt-td`).first().click(); + cy.contains("1 Selected"); }); it('should be "normal" (normal===tg-compact-table) by default and have 3 modes, compact===tg-extra-compact-table, normal=tg-compact-table, comfortable=NOTHING_HERE ', () => { cy.visit("#/DataTable"); @@ -42,8 +49,8 @@ describe("dataTable.spec", () => { // - copying a single row (selected or not) cy.get(`[data-test="tgCell_type.special"]`).first().click(); //tnr: typing both so that the hotkey is triggered even when running on tests on linux in CI (maybe it will be solved some day https://github.com/cypress-io/cypress/issues/8961) - cy.get(".data-table-container").type("{meta}c"); - cy.get(".data-table-container").type("{ctrl}c"); + cy.get(".data-table-container").type("{meta+c}"); + cy.get(".data-table-container").type("{ctrl+c}"); cy.contains("Selected rows copied"); }); it(`it can copy a single row, selected rows, or cells to the clipboard`, () => { @@ -91,7 +98,8 @@ describe("dataTable.spec", () => { cy.visit("#/DataTable"); cy.get(`[data-test="Hunger Level"]`) .find(".tg-filter-menu-button") - .click({ force: true }); + .invoke("show") + .click(); cy.get(".bp3-popover input").type("989"); cy.get(".bp3-popover").contains("Filter").click(); //the clear filter button should show up and we can click it @@ -157,6 +165,7 @@ describe("dataTable.spec", () => { }); }; checkIndices("lessThan"); + cy.contains(version); cy.dragBetween(".rt-th:contains(Name)", ".rt-th:contains(Weather)"); checkIndices("greaterThan"); }); @@ -178,21 +187,19 @@ describe("dataTable.spec", () => { ); }); + // jgespinosa10: This test is tricky, when pressing {shift} the {downArrow} is also + // pressed before so it doesn't work as expected, this is a Cypress error. it("can use the keyboard to move up/down and select rows", () => { cy.visit("#/DataTable?pageSize=10"); - cy.contains("label", "withCheckboxes").click(); - cy.contains(".rt-td", "row 3").click(); + cy.contains("label", "With Checkboxes").click(); + cy.contains(".rt-td", "row 1").click(); cy.get(".rt-tr-group.selected").should("have.length", 1); - cy.get(".data-table-container").type("{shift}{downArrow}"); - cy.get(".rt-tr-group.selected").should("have.length", 2); - cy.get(".data-table-container").type("{shift}{downArrow}"); + cy.get(".data-table-container").type("{shift}{downArrow}{downArrow}"); cy.get(".rt-tr-group.selected").should("have.length", 3); - cy.contains(".rt-td", "row 2").click(); + cy.contains(".rt-td", "row 1").click(); cy.get(".rt-tr-group.selected").should("have.length", 1); - cy.contains("label", "isSingleSelect").click(); + cy.contains("label", "Is Single Select").click(); cy.get(".data-table-container").type("{shift}{downArrow}"); cy.get(".rt-tr-group.selected").should("have.length", 1); - cy.get(".rt-tr-group:contains(row 2)").should("not.have.class", "selected"); - cy.get(".rt-tr-group:contains(row 3)").should("not.have.class", "selected"); }); }); diff --git a/packages/ui/cypress/support/index.js b/packages/ui/cypress/support/index.js index f09f7121..748bca06 100644 --- a/packages/ui/cypress/support/index.js +++ b/packages/ui/cypress/support/index.js @@ -43,10 +43,10 @@ Cypress.Commands.add("dragBetween", (dragSelector, dropSelector) => { }) : cy.wrap(selector); - getOrWrap(dragSelector) - .trigger("mousedown") - .trigger("mousemove", 10, 10, { force: true }); - getOrWrap(dropSelector).trigger("mousemove").trigger("mouseup"); + getOrWrap(dragSelector).trigger("mousedown"); + getOrWrap(dragSelector).trigger("mousemove", 10, 10, { force: true }); + getOrWrap(dropSelector).trigger("mousemove"); + getOrWrap(dropSelector).trigger("mouseup"); }); Cypress.Commands.add( diff --git a/packages/ui/demo/src/examples/DataTable.js b/packages/ui/demo/src/examples/DataTable.js index 4cf70a61..24ccdc4a 100644 --- a/packages/ui/demo/src/examples/DataTable.js +++ b/packages/ui/demo/src/examples/DataTable.js @@ -1,3 +1,4 @@ +import React, { useCallback, useState } from "react"; import { Button, Checkbox, @@ -9,16 +10,44 @@ import { } from "@blueprintjs/core"; import { Chance } from "chance"; import { times } from "lodash-es"; -import React from "react"; import ReactMarkdown from "react-markdown"; -import { withRouter } from "react-router-dom"; import { DataTable, PagingTool, withTableParams } from "../../../src"; import DemoWrapper from "../DemoWrapper"; import OptionsSection from "../OptionsSection"; -import renderToggle from "../renderToggle"; +import { useToggle } from "../useToggle"; const controlled_page_size = 33; +const defaultNumOfEntities = 60; const chance = new Chance(); + +const generateFakeRows = num => { + return times(num).map((a, index) => + generateFakeRow({ id: index.toString() }) + ); +}; + +const generateFakeRow = ({ id, name }) => ({ + id: id, + notDisplayedField: chance.name(), + name: name || chance.name(), + color: chance.color(), + hungerLevel: chance.integer(), + isShared: chance.pickone([true, false]), + user: { + age: chance.d100(), + lastName: chance.name(), + status: { + name: chance.pickone(["pending", "added", "confirmed"]) + } + }, + type: { + special: "row " + (id + 1) + }, + addedBy: chance.name(), + updatedAt: chance.date(), + createdAt: chance.date() +}); + const schema = { model: "material", fields: [ @@ -44,7 +73,7 @@ const schema = { type: "boolean", displayName: (
- Is Shared? + Is Shared?
) }, @@ -70,9 +99,6 @@ const schema = { ) }, - - { path: "createdAt", type: "timestamp", displayName: "Date Created" }, - { path: "updatedAt", type: "timestamp", displayName: "Last Edited" }, { type: "lookup", displayName: "User Status", @@ -98,8 +124,6 @@ const schema = { ); } }, - { path: "createdAt", type: "timestamp", displayName: "Date Created" }, - { path: "updatedAt", type: "timestamp", displayName: "Last Edited" }, { type: "lookup", displayName: "User Status", @@ -119,61 +143,548 @@ const schema = { ] }; -export default class DataTableDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - renderUnconnectedTable: false, - urlConnected: true, - onlyOneFilter: false, - inDialog: false, - withSelectedEntities: false - // ...JSON.parse(localStorage.tableWrapperState || "{}") - }; - this.closeDialog = this.closeDialog.bind(this); - } +const noop = () => { + return; +}; - // componentDidUpdate() { - // localStorage.tableWrapperState = JSON.stringify(this.state); - // } +const SubComp = row => ( +
!!Row Index: {row.index}
+); - UNSAFE_componentWillMount() { - //tnr: the following code allows the DataTable test to set defaults on the demo (which is used in the testing) - this.setState(this.props); - } +const DataTableDemo = () => { + const [renderUnconnectedTable, renderUnconnectedTableSwitch] = useToggle({ + type: "renderUnconnectedTable", + description: + "Render the table without the withTableParams wrapper." + + " It's just a simple disconnected react component. You'll" + + " need to handle paging/sort/filters yourself. Try hitting" + + " isInfinite to see something actually show up with it" + }); + const [urlConnected, urlConnectedSwitch] = useToggle({ + type: "urlConnected", + description: + "Turn off urlConnected if you don't want the url to be updated by the table" + }); + const [onlyOneFilter, onlyOneFilterSwitch] = useToggle({ + type: "onlyOneFilter", + description: + "Setting this true makes the table only keep 1 filter/search term in memory instead of allowing multiple" + }); + const [inDialog, setInDialog] = useState(false); + const [, inDialogSwitch] = useToggle({ + type: "inDialog", + description: "Render the table in a dialog", + controlledValue: inDialog, + setControlledValue: setInDialog + }); + const [withSelectedEntities, withSelectedEntitiesSwitch] = useToggle({ + type: "withSelectedEntities", + description: "Setting this true makes the table pass the selectedEntities" + }); - closeDialog() { - this.setState({ - inDialog: false + const [_additionalFilters, _additionalFiltersSwitch] = useToggle({ + type: "additionalFilters", + description: + "Filters can be added by passing an additionalFilters prop. You can even filter on non-displayed fields" + }); + const [compact, compactSwitch] = useToggle({ + type: "compact", + defaultValue: true + }); + const [controlledPaging, controlledPagingSwitch] = useToggle({ + type: "controlledPaging" + }); + const [disabled, disabledSwitch] = useToggle({ type: "disabled" }); + const [disableSetPageSize, disableSetPageSizeSwitch] = useToggle({ + type: "disableSetPageSize" + }); + const [doNotShowEmptyRows, doNotShowEmptyRowsSwitch] = useToggle({ + type: "doNotShowEmptyRows" + }); + + const [expandAllByDefault, expandAllByDefaultSwitch] = useToggle({ + type: "expandAllByDefault" + }); + const [extraCompact, extraCompactSwitch] = useToggle({ + type: "extraCompact" + }); + const [forceNoNextPage, forceNoNextPageSwitch] = useToggle({ + type: "forceNoNextPage" + }); + const [getRowClassName, getRowClassNameSwitch] = useToggle({ + type: "getRowClassName" + }); + const [hideDisplayOptionsIcon, hideDisplayOptionsIconSwitch] = useToggle({ + type: "hideDisplayOptionsIcon", + description: + "use this in conjunction with withDisplayOptions=true to have display options but not allow the user to see or edit them" + }); + const [hidePageSizeWhenPossible, hidePageSizeWhenPossibleSwitch] = useToggle({ + type: "hidePageSizeWhenPossible" + }); + const [hideSelectedCount, hideSelectedCountSwitch] = useToggle({ + type: "hideSelectedCount" + }); + const [hideSetPageSize, hideSetPageSizeSwitch] = useToggle({ + type: "hideSetPageSize" + }); + const [hideTotalPages, hideTotalPagesSwitch] = useToggle({ + type: "hideTotalPages" + }); + const [isCopyable, isCopyableSwitch] = useToggle({ + type: "isCopyable", + defaultValue: true + }); + + const [isInfinite, setIsInfinite] = useState(false); + const [, isInfiniteSwitch] = useToggle({ + type: "isInfinite", + controlledValue: isInfinite, + setControlledValue: setIsInfinite + }); + const [isLoading, isLoadingSwitch] = useToggle({ + type: "isLoading" + }); + const [isOpenable, isOpenableSwitch] = useToggle({ + type: "isOpenable" + }); + const [isSimple, isSimpleSwitch] = useToggle({ + type: "isSimple", + description: ` This sets: + expandAllByDefault: false, + hidePageSizeWhenPossible: true, + hideSelectedCount: true, + isCopyable: false, + isInfinite: true, + noFooter: true, + noFullscreenButton: true, + noHeader: true, + noPadding: true, + selectAllByDefault: false, + withExpandAndCollapseAllButton: false, + withFilter: false, + withPaging: false, + withSearch: false, + withTitle: false, + by default, but they are all + individually overridable (which + is why nothing changes when this is toggled here) + ` + }); + const [isSingleSelect, isSingleSelectSwitch] = useToggle({ + type: "isSingleSelect" + }); + const [isViewable, isViewableSwitch] = useToggle({ + type: "isViewable", + description: "Make sure withCheckboxes is off when using this" + }); + const [keepSelectionOnPageChange, keepSelectionOnPageChangeSwitch] = + useToggle({ + type: "keepSelectionOnPageChange" }); - } + const [maxHeight, maxHeightSwitch] = useToggle({ + type: "maxHeight", - render() { - let ConnectedTable = withTableParams({ - //tnrtodo: this should be set up as an enhancer instead - formName: "example 1", //this should be a unique name - schema, - defaults: { - order: ["isShared"], //default sort specified here! - pageSize: 5 - }, - urlConnected: this.state.urlConnected, - onlyOneFilter: this.state.onlyOneFilter, - withSelectedEntities: this.state.withSelectedEntities - })(DataTableInstance); - ConnectedTable = withRouter(ConnectedTable); + description: + "By default every table has a max height of 800px. Setting this true changes it to 200px" + }); + const [minimalStyle, minimalStyleSwitch] = useToggle({ + type: "minimalStyle", - return ( + description: "Make the datatable blend into the background" + }); + const [mustClickCheckboxToSelect, mustClickCheckboxToSelectSwitch] = + useToggle({ + type: "mustClickCheckboxToSelect" + }); + const [noDeselectAll, noDeselectAllSwitch] = useToggle({ + type: "noDeselectAll", + description: + "Prevent the table from being fully deselected. Useful when you want at least 1 entity selected" + }); + const [noFooter, noFooterSwitch] = useToggle({ + type: "noFooter" + }); + const [noFullscreenButton, noFullscreenButtonSwitch] = useToggle({ + type: "noFullscreenButton" + }); + const [noHeader, noHeaderSwitch] = useToggle({ + type: "noHeader" + }); + const [noPadding, noPaddingSwitch] = useToggle({ + type: "noPadding" + }); + const [noSelect, noSelectSwitch] = useToggle({ + type: "noSelect" + }); + const [noRowsFoundMessage, noRowsFoundMessageSwitch] = useToggle({ + type: "noRowsFoundMessage" + }); + const [numOfEntities, setNumOfEntities] = useState(defaultNumOfEntities); + const [onDoubleClick, onDoubleClickSwitch] = useToggle({ + type: "onDoubleClick" + }); + const [selectAllByDefault, selectAllByDefaultSwitch] = useToggle({ + type: "selectAllByDefault" + }); + const [selectedIds, setSelectedIds] = useState(undefined); + const [showCount, showCountSwitch] = useToggle({ + type: "showCount" + }); + const [withCheckboxes, withCheckboxesSwitch] = useToggle({ + type: "withCheckboxes", + defaultValue: true + }); + const [withDisplayOptions, withDisplayOptionsSwitch] = useToggle({ + type: "withDisplayOptions", + defaultValue: true + }); + const [withExpandAndCollapseAllButton, withExpandAndCollapseAllButtonSwitch] = + useToggle({ + type: "withExpandAndCollapseAllButton" + }); + const [withFilter, withFilterSwitch] = useToggle({ + type: "withFilter", + defaultValue: true + }); + const [withPaging, withPagingSwitch] = useToggle({ + type: "withPaging", + defaultValue: true + }); + const [withSearch, withSearchSwitch] = useToggle({ + type: "withSearch", + defaultValue: true + }); + const [withSort, withSortSwitch] = useToggle({ + type: "withSort", + defaultValue: true + }); + const [withSubComponent, withSubComponentSwitch] = useToggle({ + type: "withSubComponent" + }); + const [withTitle, withTitleSwitch] = useToggle({ + type: "withTitle", + defaultValue: true + }); + + const [entities, setEntities] = useState( + generateFakeRows(defaultNumOfEntities) + ); + + const changeNumEntities = numOfEntities => { + setNumOfEntities(numOfEntities); + setEntities(generateFakeRows(numOfEntities)); + }; + + const [, updateSelectedAndChangeNumEntsButton] = useToggle({ + type: "updateSelectedAndChangeNumEnts", + label: "Update Selection and Entities Multiple times", + isButton: true, + hook: () => { + setSelectedIds(["lala", "23r2", "asdf"]); + setIsInfinite(true); + setEntities([ + generateFakeRow({ id: "lala" }), + generateFakeRow({ id: "23r2" }), + generateFakeRow({ id: "asdf" }), + generateFakeRow({ id: "2g2g" }), + generateFakeRow({ id: "ahha" }) + ]); + + setTimeout(() => { + setSelectedIds(["zoioiooonk", "23r2", "asdf"]); + setIsInfinite(true); + setEntities([ + generateFakeRow({ id: "zaza" }), + generateFakeRow({ id: "23r2" }), + generateFakeRow({ id: "asdf" }), + generateFakeRow({ id: "2g2g" }), + generateFakeRow({ id: "ahha" }), + generateFakeRow({ id: "zoioiooonk", name: "zoioiooonk" }) + ]); + }, 1000); + } + }); + + const changeSelectedRecords = e => { + const val = e.target.value; + const selectedIds = ( + val.indexOf(",") > -1 + ? val.split(",").map(num => parseInt(num, 10)) + : [parseInt(val, 10)] + ).filter(val => !isNaN(val)); + setSelectedIds(selectedIds); + }; + + const closeDialog = () => { + setInDialog(false); + }; + + const onRefresh = () => { + alert("clicked refresh!"); + }; + + const DataTableInstance = useCallback( + props => { + const { tableParams, selectedEntities } = props; + const { page, pageSize, isTableParamsConnected } = tableParams; + let entitiesToPass = []; + if (isInfinite || !isTableParamsConnected) { + entitiesToPass = entities; + } else { + for ( + let i = + (page - 1) * (controlledPaging ? controlled_page_size : pageSize); + i < page * (controlledPaging ? controlled_page_size : pageSize); + i++ + ) { + entities[i] && entitiesToPass.push(entities[i]); + } + } + const additionalFilters = _additionalFilters && [ + { + filterOn: "notDisplayed", //remember this needs to be the camel cased display name + selectedFilter: "Contains", + filterValue: "aj" + } + ]; + return ( + <> + {selectedEntities && ( +
+ The following records are selected (pass withSelectedEntities: + true to withTableParams): +
+ {selectedEntities + .map(record => `${record.id}: ${record.name}`) + .join(", ")} +
+
+ )} + + PagingTool used outside of the datatable: + + +
+ +
+ { + return ( + + {value ? "True" : "False"} + + ); + } + }} + compact={compact} + contextMenu={ + function (/*{ selectedRecords, history }*/) { + return [ + , + + ]; + } + } + controlled_hasNextPage={!forceNoNextPage} + disabled={disabled} + disableSetPageSize={disableSetPageSize} + doNotShowEmptyRows={doNotShowEmptyRows} + entities={entitiesToPass} + entityCount={entities.length} + expandAllByDefault={expandAllByDefault} + extraCompact={extraCompact} + {...(getRowClassName && { + getRowClassName: rowInfo => { + console.info(`rowInfo:`, rowInfo); + return { + "custom-getRowClassName": true + }; + } + })} + {...(controlledPaging && { + controlled_setPage: () => { + console.info(`controlled_setPage hit`); + }, + controlled_setPageSize: () => { + console.info(`controlled_setPageSize hit`); + }, + controlled_page: 6, + controlled_pageSize: controlled_page_size, + controlled_total: 440, + controlled_onRefresh: () => { + console.info(`controlled_onRefresh hit`); + } + })} + hideSetPageSize={hideSetPageSize} + hideTotalPages={hideTotalPages} + hideDisplayOptionsIcon={hideDisplayOptionsIcon} + hidePageSizeWhenPossible={hidePageSizeWhenPossible} + hideSelectedCount={hideSelectedCount} + isCopyable={isCopyable} + isInfinite={isInfinite} + isLoading={isLoading} + isOpenable={isOpenable} + isSimple={isSimple} + isSingleSelect={isSingleSelect} + isViewable={isViewable} + keepSelectionOnPageChange={keepSelectionOnPageChange} + {...(maxHeight ? { maxHeight: "200px" } : {})} + minimalStyle={minimalStyle} + mustClickCheckboxToSelect={mustClickCheckboxToSelect} + noDeselectAll={noDeselectAll} + noFooter={noFooter} + noFullscreenButton={noFullscreenButton} + noHeader={noHeader} + noPadding={noPadding} + noRowsFoundMessage={ + noRowsFoundMessage || "I guess we didn't find anything .. :(" + } + noSelect={noSelect} + onDeselect={noop} + onDoubleClick={ + onDoubleClick + ? function () { + window.toastr.info("double clicked"); + } + : undefined + } + onMultiRowSelect={noop} + onRefresh={onRefresh} + onSingleRowSelect={noop} + selectAllByDefault={selectAllByDefault} + initialSelectedIds={selectedIds} + shouldShowSubComponent={r => r.id !== 1} + showCount={showCount} + SubComponent={withSubComponent ? SubComp : undefined} + title={"Demo table"} + topLeftItems={} + withCheckboxes={withCheckboxes} + withDisplayOptions={withDisplayOptions} + withExpandAndCollapseAllButton={withExpandAndCollapseAllButton} + withFilter={withFilter} + withPaging={withPaging} + withSearch={withSearch} + withSort={withSort} + withTitle={withTitle} + /> +
+
+
+
+ + ); + }, + [ + _additionalFilters, + compact, + controlledPaging, + disableSetPageSize, + disabled, + doNotShowEmptyRows, + entities, + expandAllByDefault, + extraCompact, + forceNoNextPage, + getRowClassName, + hideDisplayOptionsIcon, + hidePageSizeWhenPossible, + hideSelectedCount, + hideSetPageSize, + hideTotalPages, + isCopyable, + isInfinite, + isLoading, + isOpenable, + isSimple, + isSingleSelect, + isViewable, + keepSelectionOnPageChange, + maxHeight, + minimalStyle, + mustClickCheckboxToSelect, + noDeselectAll, + noFooter, + noFullscreenButton, + noHeader, + noPadding, + noRowsFoundMessage, + noSelect, + onDoubleClick, + selectAllByDefault, + selectedIds, + showCount, + withCheckboxes, + withDisplayOptions, + withExpandAndCollapseAllButton, + withFilter, + withPaging, + withSearch, + withSort, + withSubComponent, + withTitle + ] + ); + + const ConnectedTable = withTableParams({ + //tnrtodo: this should be set up as an enhancer instead + formName: "example 1", //this should be a unique name + schema, + defaults: { + order: ["isShared"], //default sort specified here! + pageSize: 5 + }, + urlConnected, + onlyOneFilter, + withSelectedEntities + })(DataTableInstance); + + return ( +
-
- -
- - Passing props from an unrelated query into a DataTable via - withTableParams(){" "} - - +
+ + Passing props from an unrelated query into a DataTable via + withTableParams(){" "} + + -
- - - {renderToggle({ - that: this, - - type: "renderUnconnectedTable", - description: - "Render the table without the withTableParams wrapper." + - " It's just a simple disconnected react component. You'll" + - " need to handle paging/sort/filters yourself. Try hitting" + - " isInfinite to see something actually show up with it" - })} - {renderToggle({ - that: this, - type: "inDialog", - description: "Render the table in a dialog" - })} - - - {renderToggle({ - that: this, - - type: "urlConnected", - description: - "Turn off urlConnected if you don't want the url to be updated by the table" - })} - {renderToggle({ - that: this, - - type: "onlyOneFilter", - description: - "Setting this true makes the table only keep 1 filter/search term in memory instead of allowing multiple" - })} - {renderToggle({ - that: this, - - type: "withSelectedEntities", - description: - "Setting this true makes the table pass the selectedEntities" - })} - - {this.state.inDialog ? ( - -
- -
-
- ) : this.state.renderUnconnectedTable ? ( - - ) : ( - - )} -
-
-
- ); - } -} - -const generateFakeRows = num => { - return times(num).map((a, index) => - generateFakeRow({ id: index.toString() }) - ); -}; -function generateFakeRow({ id, name }) { - return { - id: id, - notDisplayedField: chance.name(), - name: name || chance.name(), - color: chance.color(), - hungerLevel: chance.integer(), - isShared: chance.pickone([true, false]), - user: { - age: chance.d100(), - lastName: chance.name(), - status: { - name: chance.pickone(["pending", "added", "confirmed"]) - } - }, - type: { - special: "row " + (id + 1) - }, - addedBy: chance.name(), - updatedAt: chance.date(), - createdAt: chance.date() - }; -} - -const defaultNumOfEntities = 60; - -class DataTableInstance extends React.Component { - constructor(props) { - super(props); - this.state = { - additionalFilters: false, - isSimple: false, - isCopyable: true, - mustClickCheckboxToSelect: false, - noSelect: false, - withTitle: true, - isViewable: false, - isOpenable: false, - minimalStyle: false, - withSearch: true, - withPaging: true, - getRowClassName: false, - noDeselectAll: false, - expandAllByDefault: false, - withExpandAndCollapseAllButton: false, - selectAllByDefault: false, - withFilter: true, - withSort: true, - withSubComponent: false, - noHeader: false, - noFooter: false, - noPadding: false, - noFullscreenButton: false, - hideDisplayOptionsIcon: false, - withDisplayOptions: true, - isInfinite: false, - isSingleSelect: false, - maxHeight: false, - noRowsFoundMessage: false, - isLoading: false, - disabled: false, - compact: true, - extraCompact: false, - hidePageSizeWhenPossible: false, - hideSelectedCount: false, - showCount: false, - doNotShowEmptyRows: false, - withCheckboxes: true, - numOfEntities: 60, - selectedIds: undefined, - alwaysRerender: false, - entities: generateFakeRows(defaultNumOfEntities) - // ...JSON.parse(localStorage.tableState || "{}") - }; - this.changeNumEntities = this.changeNumEntities.bind(this); - this.changeSelectedRecords = this.changeSelectedRecords.bind(this); - this.onRefresh = this.onRefresh.bind(this); - } - - // componentDidUpdate() { - // localStorage.tableState = JSON.stringify(this.state); - // } - - changeNumEntities(numOfEntities) { - this.setState({ - numOfEntities, - entities: generateFakeRows(numOfEntities) - }); - } - - changeSelectedRecords(e) { - const val = e.target.value; - const selectedIds = ( - val.indexOf(",") > -1 - ? val.split(",").map(num => parseInt(num, 10)) - : [parseInt(val, 10)] - ).filter(val => !isNaN(val)); - this.setState({ - selectedIds - }); - } - - onRefresh() { - alert("clicked refresh!"); - } - - render() { - const { numOfEntities, entities, selectedIds } = this.state; - const { tableParams, selectedEntities } = this.props; - const { page, pageSize, isTableParamsConnected } = tableParams; - let entitiesToPass = []; - if (this.state.isInfinite || !isTableParamsConnected) { - entitiesToPass = entities; - } else { - for ( - let i = - (page - 1) * - (this.state.controlledPaging ? controlled_page_size : pageSize); - i < - page * (this.state.controlledPaging ? controlled_page_size : pageSize); - i++ - ) { - entities[i] && entitiesToPass.push(entities[i]); - } - } - const additionalFilters = this.state.additionalFilters && [ - { - filterOn: "notDisplayed", //remember this needs to be the camel cased display name - selectedFilter: "Contains", - filterValue: "aj" - } - ]; - return ( -
+ + + + {renderUnconnectedTableSwitch} + {inDialogSwitch} + + + {urlConnectedSwitch} + {onlyOneFilterSwitch} + {withSelectedEntitiesSwitch} + - {renderToggle({ - that: this, - - type: "additionalFilters", - description: - "Filters can be added by passing an additionalFilters prop. You can even filter on non-displayed fields" - })} + {_additionalFiltersSwitch} Set number of entities:{" "}
Select records by ids (a single number or numbers separated by ","):{" "}
- +

- {renderToggle({ - that: this, - - type: "isSimple", - description: ` This sets: - noHeader: true, - noFooter: true, - noFullscreenButton: true, - noPadding: true, - hidePageSizeWhenPossible: true, - isInfinite: true, - hideSelectedCount: true, - withTitle: false, - withSearch: false, - withPaging: false, - withExpandAndCollapseAllButton: false, - expandAllByDefault: false, - selectAllByDefault: false, - withFilter: false, - isCopyable: false, - by default, but they are all - individually overridable (which - is why nothing changes when this is toggled here) - ` - })} - {renderToggle({ - that: this, - type: "withTitle" - })} - {renderToggle({ - that: this, - type: "noSelect" - })} - {renderToggle({ - that: this, - type: "withSubComponent" - })} - {renderToggle({ - that: this, - type: "withSearch" - })} - {renderToggle({ - that: this, - type: "disableSetPageSize" - })} - {renderToggle({ - that: this, - type: "keepSelectionOnPageChange" - })} - {renderToggle({ - that: this, - type: "hideSetPageSize" - })} - {renderToggle({ - that: this, - type: "hideTotalPages" - })} - {renderToggle({ - that: this, - type: "forceNoNextPage" - })} - {renderToggle({ - that: this, - type: "isViewable", - description: "Make sure withCheckboxes is off when using this" - })} - {renderToggle({ - that: this, - type: "onDoubleClick" - })} - {renderToggle({ - that: this, - type: "isOpenable", - description: "Make sure withCheckboxes is off when using this" - })} - {renderToggle({ - that: this, - type: "minimalStyle", - description: "Make the datatable blend into the background" - })} - {renderToggle({ - that: this, - - type: "hideDisplayOptionsIcon", - description: - "use this in conjunction with withDisplayOptions=true to have display options but not allow the user to see or edit them" - })} - {renderToggle({ - that: this, - type: "withDisplayOptions" - })} - {renderToggle({ - that: this, - type: "withPaging" - })} - {renderToggle({ - that: this, - type: "getRowClassName" - })} - {renderToggle({ - that: this, - type: "controlledPaging" - })} - {renderToggle({ - that: this, - - type: "noDeselectAll", - description: - "Prevent the table from being fully deselected. Useful when you want at least 1 entity selected" - })} - {renderToggle({ - that: this, - type: "withExpandAndCollapseAllButton" - })} - {renderToggle({ - that: this, - type: "expandAllByDefault" - })} - {renderToggle({ - that: this, - type: "selectAllByDefault" - })} - {renderToggle({ - that: this, - type: "withFilter" - })} - {renderToggle({ - that: this, - type: "withSort" - })} - {renderToggle({ - that: this, - type: "noHeader" - })} - {renderToggle({ - that: this, - type: "noFooter" - })} - {renderToggle({ - that: this, - type: "noFullscreenButton" - })} - {renderToggle({ - that: this, - type: "noPadding" - })} - {renderToggle({ - that: this, - type: "isInfinite" - })} - {renderToggle({ - that: this, - type: "isLoading" - })} - {renderToggle({ - that: this, - type: "disabled" - })} - {renderToggle({ - that: this, - type: "hidePageSizeWhenPossible" - })} - {renderToggle({ - that: this, - type: "doNotShowEmptyRows" - })} - {renderToggle({ - that: this, - type: "withCheckboxes" - })} - {renderToggle({ - that: this, - type: "isSingleSelect" - })} - {renderToggle({ - that: this, - type: "noRowsFoundMessage" - })} - {renderToggle({ - that: this, - type: "hideSelectedCount" - })} - {renderToggle({ - that: this, - type: "showCount" - })} - {renderToggle({ - that: this, - type: "compact", - description: - "The table is compact by default (and this is called Normal)" - })} - {renderToggle({ - that: this, - type: "extraCompact" - })} - {renderToggle({ - that: this, - type: "isCopyable" - })} - {renderToggle({ - that: this, - type: "mustClickCheckboxToSelect" - })} - {renderToggle({ - that: this, - - type: "maxHeight", - description: - "By default every table has a max height of 800px. Setting this true changes it to 200px" - })} - {renderToggle({ - that: this, - isButton: true, - label: "Update Selection and Entities Multiple times", - type: "updateSelectedAndChangeNumEnts", - hook: () => { - this.setState({ - selectedIds: ["lala", "23r2", "asdf"], - isInfinite: true, - entities: [ - generateFakeRow({ id: "lala" }), - generateFakeRow({ id: "23r2" }), - generateFakeRow({ id: "asdf" }), - generateFakeRow({ id: "2g2g" }), - generateFakeRow({ id: "ahha" }) - ] - }); - - setTimeout(() => { - this.setState({ - selectedIds: ["zoioiooonk", "23r2", "asdf"], - isInfinite: true, - entities: [ - generateFakeRow({ id: "zaza" }), - generateFakeRow({ id: "23r2" }), - generateFakeRow({ id: "asdf" }), - generateFakeRow({ id: "2g2g" }), - generateFakeRow({ id: "ahha" }), - generateFakeRow({ id: "zoioiooonk", name: "zoioiooonk" }) - ] - }); - }, 1000); - } - })} + {isSimpleSwitch} + {withTitleSwitch} + {noSelectSwitch} + {withSubComponentSwitch} + {withSearchSwitch} + {disableSetPageSizeSwitch} + {keepSelectionOnPageChangeSwitch} + {hideSetPageSizeSwitch} + {hideTotalPagesSwitch} + {forceNoNextPageSwitch} + {isViewableSwitch} + {onDoubleClickSwitch} + {isOpenableSwitch} + {minimalStyleSwitch} + {hideDisplayOptionsIconSwitch} + {withDisplayOptionsSwitch} + {withPagingSwitch} + {getRowClassNameSwitch} + {controlledPagingSwitch} + {noDeselectAllSwitch} + {withExpandAndCollapseAllButtonSwitch} + {expandAllByDefaultSwitch} + {selectAllByDefaultSwitch} + {withFilterSwitch} + {withSortSwitch} + {noHeaderSwitch} + {noFooterSwitch} + {noFullscreenButtonSwitch} + {noPaddingSwitch} + {isInfiniteSwitch} + {isLoadingSwitch} + {disabledSwitch} + {hidePageSizeWhenPossibleSwitch} + {doNotShowEmptyRowsSwitch} + {withCheckboxesSwitch} + {isSingleSelectSwitch} + {noRowsFoundMessageSwitch} + {hideSelectedCountSwitch} + {showCountSwitch} + {compactSwitch} + {extraCompactSwitch} + {isCopyableSwitch} + {mustClickCheckboxToSelectSwitch} + {maxHeightSwitch} + {updateSelectedAndChangeNumEntsButton}

- {selectedEntities && ( -
- The following records are selected (pass withSelectedEntities: true - to withTableParams): -
- {selectedEntities - .map(record => `${record.id}: ${record.name}`) - .join(", ")} + {inDialog ? ( + +
+
-
- )} - - PagingTool used outside of the datatable: - + ) : renderUnconnectedTable ? ( + - -
- -
- r.id !== 1} - topLeftItems={} - SubComponent={this.state.withSubComponent ? SubComp : undefined} - cellRenderer={{ - isShared: value => { - return ( - - {value ? "True" : "False"} - - ); - } - }} - additionalFilters={additionalFilters} - title={"Demo table"} - contextMenu={ - function (/*{ selectedRecords, history }*/) { - return [ - , - - ]; - } - } - isViewable={this.state.isViewable} - isOpenable={this.state.isOpenable} - minimalStyle={this.state.minimalStyle} - withTitle={this.state.withTitle} - noSelect={this.state.noSelect} - isSimple={this.state.isSimple} - withSearch={this.state.withSearch} - keepSelectionOnPageChange={this.state.keepSelectionOnPageChange} - disableSetPageSize={this.state.disableSetPageSize} - hideSetPageSize={this.state.hideSetPageSize} - hideTotalPages={this.state.hideTotalPages} - controlled_hasNextPage={!this.state.forceNoNextPage} - withExpandAndCollapseAllButton={ - this.state.withExpandAndCollapseAllButton - } - expandAllByDefault={this.state.expandAllByDefault} - selectAllByDefault={this.state.selectAllByDefault} - withPaging={this.state.withPaging} - {...(this.state.getRowClassName && { - getRowClassName: rowInfo => { - console.info(`rowInfo:`, rowInfo); - return { - "custom-getRowClassName": true - }; - } - })} - {...(this.state.controlledPaging && { - controlled_setPage: () => { - console.info(`controlled_setPage hit`); - }, - controlled_setPageSize: () => { - console.info(`controlled_setPageSize hit`); - }, - controlled_page: 6, - controlled_pageSize: controlled_page_size, - controlled_total: 440, - controlled_onRefresh: () => { - console.info(`controlled_onRefresh hit`); - } - })} - noDeselectAll={this.state.noDeselectAll} - withFilter={this.state.withFilter} - withSort={this.state.withSort} - noFullscreenButton={this.state.noFullscreenButton} - noPadding={this.state.noPadding} - noHeader={this.state.noHeader} - noFooter={this.state.noFooter} - withDisplayOptions={this.state.withDisplayOptions} - hideDisplayOptionsIcon={this.state.hideDisplayOptionsIcon} - isInfinite={this.state.isInfinite} - isLoading={this.state.isLoading} - disabled={this.state.disabled} - compact={this.state.compact} - extraCompact={this.state.extraCompact} - hidePageSizeWhenPossible={this.state.hidePageSizeWhenPossible} - doNotShowEmptyRows={this.state.doNotShowEmptyRows} - withCheckboxes={this.state.withCheckboxes} - isSingleSelect={this.state.isSingleSelect} - noRowsFoundMessage={ - this.state.noRowsFoundMessage && - "I guess we didn't find anything .. :(" - } - hideSelectedCount={this.state.hideSelectedCount} - showCount={this.state.showCount} - isCopyable={this.state.isCopyable} - mustClickCheckboxToSelect={this.state.mustClickCheckboxToSelect} - {...(this.state.maxHeight - ? { - maxHeight: "200px" - } - : {})} - onRefresh={this.onRefresh} - // history={history} - onSingleRowSelect={noop} - onDeselect={noop} - onMultiRowSelect={noop} - selectedIds={selectedIds} - /> -
-
-
+ ) : ( + + )}
- ); - } -} - -// eslint-disable-next-line @typescript-eslint/no-empty-function -function noop() {} - -function SubComp(row) { - return ( -
- {" "} - !!Row Index: {row.index} - {/* */}
); -} +}; + +export default DataTableDemo; diff --git a/packages/ui/demo/src/examples/EditableCellTable.js b/packages/ui/demo/src/examples/EditableCellTable.js index 9b8f1947..582f7c16 100644 --- a/packages/ui/demo/src/examples/EditableCellTable.js +++ b/packages/ui/demo/src/examples/EditableCellTable.js @@ -34,29 +34,29 @@ function getEnts(num) { }); } -export default function SimpleTable(p) { +export default function EditableCellTable(p) { const key = useRef(0); + + const [entities, setEnts] = useState([]); const [, numComp] = useToggle({ type: "num", label: "Number of Entities", isSelect: true, defaultValue: 50, - hook: v => { + controlledValue: p.entities?.length, + setControlledValue: v => { key.current++; setEnts(getEnts(toNumber(v))); }, options: [20, 50, 100] }); + const [defaultValAsFunc, defaultValAsFuncComp] = useToggle({ type: "defaultValAsFunc" }); - // const [tagValuesAsObjects, tagValuesAsObjectsComp] = useToggle({ - // type: "tagValuesAsObjects" - // }); - const [entities, setEnts] = useState([]); - const schema = useMemo(() => { - return { + const schema = useMemo( + () => ({ fields: [ { path: "name", @@ -108,30 +108,26 @@ export default function SimpleTable(p) { defaultValue: true } ] - }; - }, [defaultValAsFunc]); + }), + [defaultValAsFunc] + ); + return (
{numComp} {defaultValAsFuncComp} - {/* {tagValuesAsObjectsComp} */} ent.name === "chris" || ent.name === "sam" - // : undefined - // } - {...p} - > + />
); diff --git a/packages/ui/demo/src/index.js b/packages/ui/demo/src/index.js index 6e11ad22..fee23a4f 100644 --- a/packages/ui/demo/src/index.js +++ b/packages/ui/demo/src/index.js @@ -256,7 +256,7 @@ const demos = { const Demo = () => { return ( - + ); }; diff --git a/packages/ui/demo/src/renderToggle.js b/packages/ui/demo/src/renderToggle.js index c5df4c3f..ceef44cb 100644 --- a/packages/ui/demo/src/renderToggle.js +++ b/packages/ui/demo/src/renderToggle.js @@ -67,34 +67,25 @@ export default function renderToggle({ } } if (isButton) { - toggleOrButton = ( -
); } + function HandleHotkeys({ combo, onKeyDown }) { const hotkeys = useMemo( () => [ @@ -165,7 +152,7 @@ function HandleHotkeys({ combo, onKeyDown }) { function ShowInfo({ description, info, type }) { const [isOpen, setOpen] = useState(false); return ( - + <> { setOpen(false); @@ -192,11 +179,11 @@ function ShowInfo({ description, info, type }) { }} minimal icon="info-sign" - > + />
) : ( -
+
)} - + ); } diff --git a/packages/ui/demo/src/useToggle.js b/packages/ui/demo/src/useToggle.js index b2e7aa88..d227da03 100644 --- a/packages/ui/demo/src/useToggle.js +++ b/packages/ui/demo/src/useToggle.js @@ -18,7 +18,7 @@ import { } from "../../src"; import { startCase } from "lodash-es"; -function HandleHotkeys({ combo, onKeyDown }) { +const HandleHotkeys = ({ combo, onKeyDown }) => { const hotkeys = useMemo( () => [ { @@ -31,12 +31,12 @@ function HandleHotkeys({ combo, onKeyDown }) { ); useHotkeys(hotkeys); return null; -} +}; -function ShowInfo({ description, info, type }) { +const ShowInfo = ({ description, info, type }) => { const [isOpen, setOpen] = useState(false); return ( - + <> { setOpen(false); @@ -63,16 +63,16 @@ function ShowInfo({ description, info, type }) { }} minimal icon="info-sign" - > + />
) : ( -
+
)} - + ); -} +}; -export function useToggle({ +const useToggle = ({ type, isButton, label, @@ -88,22 +88,28 @@ export function useToggle({ alwaysShow, hotkey, searchInput, + controlledValue, + setControlledValue, ...rest -}) { +}) => { const defaultValue = _defaultValue || options?.[0]?.value || options?.[0]; + const [val, _setVal] = useState(); + useEffect(() => { const demoState = getDemoState(); const toSet = demoState[type] || defaultValue; + setControlledValue?.(toSet); _setVal(toSet); - hook?.(toSet); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [val, _setVal] = useState(); + const value = useMemo(() => controlledValue || val, [controlledValue, val]); + const setVal = newVal => { const demoState = getDemoState(); demoState[type] = newVal; setCurrentParamsOnUrl({ [type]: newVal }, undefined, true); + if (setControlledValue) setControlledValue(newVal); _setVal(newVal); }; let comp; @@ -130,46 +136,35 @@ export function useToggle({ } } if (isButton) { - toggleOrButton = ( -
); - return [val, comp]; -} + return [value, comp]; +}; function getDemoState() { const editorDemoState = getCurrentParamsFromUrl({}, true); - // localStorage.editorDemoState = props.history.location.search; const massagedEditorDemoState = Object.keys(editorDemoState).reduce( (acc, key) => { if (editorDemoState[key] === "false") { @@ -221,3 +212,5 @@ function getDemoState() { ); return massagedEditorDemoState; } + +export { useToggle }; diff --git a/packages/ui/src/DataTable/DisplayOptions.js b/packages/ui/src/DataTable/DisplayOptions.js index b4c2c4c9..c75ca1c7 100644 --- a/packages/ui/src/DataTable/DisplayOptions.js +++ b/packages/ui/src/DataTable/DisplayOptions.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { map, isEmpty, noop } from "lodash-es"; import { Button, @@ -11,208 +11,187 @@ import { Switch } from "@blueprintjs/core"; -export default class DisplayOptions extends React.Component { - state = { - isOpen: false, - searchTerms: {} - }; - - openPopover = () => { - this.setState({ - isOpen: true - }); - }; +const DisplayOptions = ({ + compact, + extraCompact, + disabled, + hideDisplayOptionsIcon, + resetDefaultVisibility = noop, + updateColumnVisibility = noop, + updateTableDisplayDensity, + showForcedHiddenColumns, + setShowForcedHidden, + hasOptionForForcedHidden, + schema +}) => { + const [isOpen, setIsOpen] = useState(false); + const [searchTerms, setSearchTerms] = useState({}); - closePopover = () => { - this.setState({ - isOpen: false - }); - }; - - changeTableDensity = e => { - const { updateTableDisplayDensity = noop } = this.props; + const changeTableDensity = e => { updateTableDisplayDensity(e.target.value); - this.closePopover(); + setIsOpen(false); }; - toggleForcedHidden = e => this.props.setShowForcedHidden(e.target.checked); + const toggleForcedHidden = e => setShowForcedHidden(e.target.checked); + + if (hideDisplayOptionsIcon) { + return null; //don't show antyhing! + } + const { fields } = schema; + const fieldGroups = {}; + const mainFields = []; - render() { - const { isOpen, searchTerms } = this.state; - const { - schema, - updateColumnVisibility = noop, - resetDefaultVisibility = noop, - compact, - extraCompact, - disabled, - hasOptionForForcedHidden, - showForcedHiddenColumns, - hideDisplayOptionsIcon - } = this.props; - if (hideDisplayOptionsIcon) { - return null; //don't show antyhing! - } - const { fields } = schema; - const fieldGroups = {}; - const mainFields = []; + fields.forEach(field => { + if (field.hideInMenu) return; + if (!field.fieldGroup) return mainFields.push(field); + if (!fieldGroups[field.fieldGroup]) fieldGroups[field.fieldGroup] = []; + fieldGroups[field.fieldGroup].push(field); + }); - fields.forEach(field => { - if (field.hideInMenu) return; - if (!field.fieldGroup) return mainFields.push(field); - if (!fieldGroups[field.fieldGroup]) fieldGroups[field.fieldGroup] = []; - fieldGroups[field.fieldGroup].push(field); - }); + let numVisible = 0; - let numVisible = 0; + const getFieldCheckbox = (field, i) => { + const { displayName, isHidden, isForcedHidden, path } = field; + if (isForcedHidden) return; + if (!isHidden) numVisible++; + return ( + { + if (numVisible <= 1 && !isHidden) { + return window.toastr.warning( + "We have to display at least one column :)" + ); + } + updateColumnVisibility({ shouldShow: isHidden, path }); + }} + checked={!isHidden} + label={displayName} + /> + ); + }; - const getFieldCheckbox = (field, i) => { - const { displayName, isHidden, isForcedHidden, path } = field; - if (isForcedHidden) return; - if (!isHidden) numVisible++; + let fieldGroupMenu; + if (!isEmpty(fieldGroups)) { + fieldGroupMenu = map(fieldGroups, (groupFields, groupName) => { + const searchTerm = searchTerms[groupName] || ""; + const anyVisible = groupFields.some( + field => !field.isHidden && !field.isForcedHidden + ); + const anyNotForcedHidden = groupFields.some( + field => !field.isForcedHidden + ); + if (!anyNotForcedHidden) return; return ( - { - if (numVisible <= 1 && !isHidden) { - return window.toastr.warning( - "We have to display at least one column :)" - ); - } - updateColumnVisibility({ shouldShow: isHidden, path }); - }} - checked={!isHidden} - label={displayName} - /> + + { + setSearchTerms(prev => ({ + ...prev, + [groupName]: e.target.value + })); + }} + /> +
- - } - > -
+ + } + > + ; - }} - columns={this.renderColumns()} - pageSize={rowsToShow} - expanded={expandedRows} - showPagination={false} - sortable={false} - loading={isLoading || disabled} - defaultResized={resized} - onResizedChange={(newResized = []) => { - const resizedToUse = newResized.map(column => { - // have a min width of 50 so that columns don't disappear - if (column.value < 50) { - return { - ...column, - value: 50 - }; - } else { - return column; - } - }); - resizePersist(resizedToUse); - }} - TheadComponent={this.getTheadComponent} - ThComponent={this.getThComponent} - getTrGroupProps={this.getTableRowProps} - getTdProps={this.getTableCellProps} - NoDataComponent={({ children }) => - isLoading ? null : ( -
- {noRowsFoundMessage || children} -
- ) - } - LoadingComponent={props => ( - - )} - style={{ - maxHeight, - minHeight: 150, - ...style - }} - SubComponent={SubComponentToUse} - {...ReactTableProps} - /> - {isCellEditable && ( -
-
- {!onlyShowRowsWErrors && ( - - )} -
-
- )} - {!noFooter && ( -
- {selectedAndTotalMessage} -
- {additionalFooterButtons} - {!noFullscreenButton && toggleFullscreenButton} - {withDisplayOptions && ( - - )} - {shouldShowPaging && ( - - )} -
-
- )} - + const sortDown = ordering && ordering === "asc"; + const sortUp = ordering && !sortDown; + const sortComponent = + withSort && !disableSorting ? ( +
+ { + setOrder("-" + ccDisplayName, sortUp, e.shiftKey); + }} + /> + { + setOrder(ccDisplayName, sortDown, e.shiftKey); + }} + />
- - ); - } + ) : null; + const FilterMenu = column.FilterMenu || FilterAndSortMenu; - getTableRowProps = (state, rowInfo) => { - const { - reduxFormSelectedEntityIdMap, - reduxFormExpandedEntityIdMap, - withCheckboxes, - onDoubleClick, - history, - mustClickCheckboxToSelect, - entities, - isEntityDisabled, - change, - getRowClassName, - isCellEditable - } = computePresets(this.props); - if (!rowInfo) { - return { - className: "no-row-data" - }; - } - const entity = rowInfo.original; - const rowId = getIdOrCodeOrIndex(entity, rowInfo.index); - const rowSelected = reduxFormSelectedEntityIdMap[rowId]; - const isExpanded = reduxFormExpandedEntityIdMap[rowId]; - const rowDisabled = isEntityDisabled(entity); - const dataId = entity.id || entity.code; - return { - onClick: e => { - if (isCellEditable) return; - // if checkboxes are activated or row expander is clicked don't select row - if (e.target.matches(".tg-expander, .tg-expander *")) { - change("reduxFormExpandedEntityIdMap", { - ...reduxFormExpandedEntityIdMap, - [rowId]: !isExpanded - }); - return; - } else if ( - e.target.closest(".tg-react-table-checkbox-cell-container") - ) { - return; - } else if (mustClickCheckboxToSelect) { - return; - } - if (e.detail > 1) { - return; //cancel multiple quick clicks - } - rowClick(e, rowInfo, entities, computePresets(this.props)); - }, - //row right click - onContextMenu: e => { - e.preventDefault(); - if (rowId === undefined || rowDisabled || isCellEditable) return; - const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {}; - let newIdMap; - if (withCheckboxes) { - newIdMap = oldIdMap; + const filterMenu = + withFilter && !disableFiltering ? ( + + ) : null; + let maybeCheckbox; + if (isCellEditable && !isNotEditable && type === "boolean") { + let isIndeterminate = false; + let isChecked = !!entities.length; + let hasFalse; + let hasTrue; + entities.some(e => { + if (!get(e, path)) { + isChecked = false; + hasFalse = true; } else { - // if we are not using checkboxes we need to make sure - // that the id of the record gets added to the id map - newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } }; - - // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date - const keyedEntities = keyBy(entities, getIdOrCodeOrIndex); - forEach(newIdMap, (val, key) => { - const freshEntity = keyedEntities[key]; - if (freshEntity) { - newIdMap[key] = { ...newIdMap[key], entity: freshEntity }; - } - }); - - finalizeSelection({ - idMap: newIdMap, - entities, - props: computePresets(this.props) - }); - } - this.showContextMenu(e, newIdMap); - }, - className: classNames( - "with-row-data", - getRowClassName && getRowClassName(rowInfo, state, this.props), - { - disabled: rowDisabled, - selected: rowSelected && !withCheckboxes, - "rt-tr-last-row": rowInfo.index === entities.length - 1 - } - ), - "data-test-id": dataId === undefined ? rowInfo.index : dataId, - "data-index": rowInfo.index, - onDoubleClick: e => { - if (rowDisabled) return; - this.dblClickTriggered = true; - onDoubleClick && - onDoubleClick(rowInfo.original, rowInfo.index, history, e); - } - }; - }; - - getTableCellProps = (state, rowInfo, column) => { - const { - entities, - schema, - doNotValidateUntouchedRows, - reduxFormEditingCell, - isCellEditable, - reduxFormCellValidation, - reduxFormSelectedCells = {}, - isEntityDisabled, - change - } = computePresets(this.props); - if (!isCellEditable) return {}; //only allow cell selection to do stuff here - if (!rowInfo) return {}; - const entity = rowInfo.original; - const rowIndex = rowInfo.index; - const rowId = getIdOrCodeOrIndex(entity, rowIndex); - const { - cellId, - cellIdAbove, - cellIdToRight, - cellIdBelow, - cellIdToLeft, - rowDisabled, - columnIndex - } = getCellInfo({ - columnIndex: column.index, - columnPath: column.path, - rowId, - schema, - entities, - rowIndex, - isEntityDisabled, - entity - }); - - const _isClean = - (entity._isClean && doNotValidateUntouchedRows) || isEntityClean(entity); - - const err = !_isClean && reduxFormCellValidation[cellId]; - let selectedTopBorder, - selectedRightBorder, - selectedBottomBorder, - selectedLeftBorder; - if (reduxFormSelectedCells[cellId]) { - selectedTopBorder = !reduxFormSelectedCells[cellIdAbove]; - selectedRightBorder = !reduxFormSelectedCells[cellIdToRight]; - selectedBottomBorder = !reduxFormSelectedCells[cellIdBelow]; - selectedLeftBorder = !reduxFormSelectedCells[cellIdToLeft]; - } - const isPrimarySelected = - reduxFormSelectedCells[cellId] === PRIMARY_SELECTED_VAL; - const className = classNames({ - isSelectedCell: reduxFormSelectedCells[cellId], - isPrimarySelected, - isSecondarySelected: reduxFormSelectedCells[cellId] === true, - noSelectedTopBorder: !selectedTopBorder, - isCleanRow: _isClean, - noSelectedRightBorder: !selectedRightBorder, - noSelectedBottomBorder: !selectedBottomBorder, - noSelectedLeftBorder: !selectedLeftBorder, - isDropdownCell: column.type === "dropdown", - isEditingCell: reduxFormEditingCell === cellId, - hasCellError: !!err, - "no-data-tip": reduxFormSelectedCells[cellId] - }); - return { - onDoubleClick: () => { - // cell double click - if (rowDisabled) return; - this.startCellEdit(cellId); - }, - ...(err && { - "data-tip": err?.message || err, - "data-no-child-data-tip": true - }), - onContextMenu: e => { - if (!isPrimarySelected) { - const primaryCellId = this.getPrimarySelectedCellId(); - const newSelectedCells = { ...reduxFormSelectedCells }; - if (primaryCellId) { - newSelectedCells[primaryCellId] = true; - } - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - change("reduxFormSelectedCells", newSelectedCells); + hasTrue = true; } - setTimeout(() => { - //need a timeout so reduxFormSelectedCells is up to date in the context menu - this.showContextMenu(e); - }, 0); - }, - onClick: event => { - this.handleCellClick({ - event, - cellId, - rowDisabled, - rowIndex, - columnIndex - }); - }, - className - }; - }; - - handleCellClick = ({ event, cellId }) => { - if (!cellId) return; - // cell click, cellclick - const { - entities, - schema, - change, - reduxFormEditingCell, - reduxFormSelectedCells = {}, - isEntityDisabled - } = computePresets(this.props); - const [rowId, cellPath] = cellId.split(":"); - const entityMap = getEntityIdToEntity(entities); - const { e: entity, i: rowIndex } = entityMap[rowId]; - const pathToIndex = getFieldPathToIndex(schema); - const columnIndex = pathToIndex[cellPath]; - const rowDisabled = isEntityDisabled(entity); - - if (rowDisabled) return; - let newSelectedCells = { - ...reduxFormSelectedCells - }; - if (newSelectedCells[cellId] && !event.shiftKey) { - // don't deselect if editing - if (reduxFormEditingCell === cellId) return; - if (event.metaKey) { - delete newSelectedCells[cellId]; - } else { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } - } else { - const primarySelectedCellId = this.getPrimarySelectedCellId(); - if (event.metaKey) { - if (isEmpty(newSelectedCells)) { - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } else { - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - if (primarySelectedCellId) - newSelectedCells[primarySelectedCellId] = true; + if (hasFalse && hasTrue) { + isIndeterminate = true; + return true; } - } else if (event.shiftKey) { - if (primarySelectedCellId) { - const [rowId, colPath] = primarySelectedCellId.split(":"); - const primaryRowIndex = entities.findIndex((e, i) => { - return getIdOrCodeOrIndex(e, i) === rowId; - }); - const fieldToIndex = getFieldPathToIndex(schema); - const primaryColIndex = fieldToIndex[colPath]; - - if (primaryRowIndex === -1 || primaryColIndex === -1) { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } else { - const minRowIndex = min([primaryRowIndex, rowIndex]); - const minColIndex = min([primaryColIndex, columnIndex]); - const maxRowIndex = max([primaryRowIndex, rowIndex]); - const maxColIndex = max([primaryColIndex, columnIndex]); - const entitiesBetweenRows = entities.slice( - minRowIndex, - maxRowIndex + 1 - ); - const fieldsBetweenCols = schema.fields.slice( - minColIndex, - maxColIndex + 1 - ); - newSelectedCells = { - [primarySelectedCellId]: PRIMARY_SELECTED_VAL - }; - entitiesBetweenRows.forEach(e => { - const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e)); - fieldsBetweenCols.forEach(f => { - const cellId = `${rowId}:${f.path}`; - if (!newSelectedCells[cellId]) newSelectedCells[cellId] = true; + return false; + }); + maybeCheckbox = ( + { + updateEntitiesHelper(entities, ents => { + ents.forEach(e => { + delete e._isClean; + set(e, path, isIndeterminate ? true : !isChecked); }); }); - // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - // newSelectedCells[primarySelectedCellId] = true; - } - } else { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } - } else { - newSelectedCells = {}; - newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; - } + }} + indeterminate={isIndeterminate} + checked={isChecked} + /> + ); } - change("reduxFormSelectedCells", newSelectedCells); + const columnTitleTextified = getTextFromEl(columnTitle); + + return ( +
+ ${columnTitle}:
+ ${description} ${isUnique ? "
Must be unique" : ""}
` + })} + data-test={columnTitleTextified} + data-path={path} + data-copy-text={columnTitleTextified} + data-copy-json={JSON.stringify({ + __strVal: columnTitleTextified, + __isHeaderCell: true + })} + className={classNames("tg-react-table-column-header", { + "sort-active": sortUp || sortDown + })} + > + {columnTitleTextified && !noTitle && ( + <> + {maybeCheckbox} + + {renderTitleInner ? renderTitleInner : columnTitle}{" "} + + + )} +
+ {sortComponent} + {filterMenu} +
+ + ); }; - renderCheckboxHeader = () => { - const { - reduxFormSelectedEntityIdMap, - isSingleSelect, - noSelect, - noUserSelect, - entities, - isEntityDisabled - } = computePresets(this.props); + const renderCheckboxHeader = () => { const checkedRows = getSelectedRowsFromEntities( entities, reduxFormSelectedEntityIdMap @@ -2180,7 +1857,6 @@ class DataTable extends React.Component { return !isSingleSelect ? ( { const newIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {}; entities.forEach((entity, i) => { @@ -2196,24 +1872,24 @@ class DataTable extends React.Component { finalizeSelection({ idMap: newIdMap, entities, - props: computePresets(this.props) + props: { + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + noSelect, + change + } }); }} - /* eslint-enable react/jsx-no-bind */ {...checkboxProps} /> ) : null; }; - renderCheckboxCell = row => { + const renderCheckboxCell = row => { const rowIndex = row.index; - const { - reduxFormSelectedEntityIdMap, - noSelect, - noUserSelect, - entities, - isEntityDisabled - } = computePresets(this.props); const checkedRows = getSelectedRowsFromEntities( entities, reduxFormSelectedEntityIdMap @@ -2230,23 +1906,37 @@ class DataTable extends React.Component { { - rowClick(e, row, entities, computePresets(this.props)); + rowClick(e, row, entities, { + reduxFormSelectedEntityIdMap, + isSingleSelect, + noSelect, + onRowClick, + isEntityDisabled, + withCheckboxes, + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + change + }); }} checked={isSelected} /> ); }; - finishCellEdit = (cellId, newVal, doNotStopEditing) => { - const { - entities = [], - change, - schema, - reduxFormCellValidation - } = computePresets(this.props); + const refocusTable = () => { + const table = tableRef.current?.tableRef?.closest( + ".data-table-container>div" + ); + table?.focus(); + }; + + const finishCellEdit = (cellId, newVal, doNotStopEditing) => { const [rowId, path] = cellId.split(":"); - !doNotStopEditing && change("reduxFormEditingCell", null); - this.updateEntitiesHelper(entities, entities => { + !doNotStopEditing && setEditingCell(null); + updateEntitiesHelper(entities, entities => { const entity = entities.find((e, i) => { return getIdOrCodeOrIndex(e, i) === rowId; }); @@ -2257,37 +1947,67 @@ class DataTable extends React.Component { schema, newVal }); - this.updateValidation(entities, { + updateValidation(entities, { ...reduxFormCellValidation, [cellId]: error }); }); - !doNotStopEditing && this.refocusTable(); + !doNotStopEditing && refocusTable(); }; - cancelCellEdit = () => { - const { change } = computePresets(this.props); - change("reduxFormEditingCell", null); - this.refocusTable(); + const getCopyTextForCell = (val, row = {}, column = {}) => { + // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this) + let text = typeof val !== "string" ? row.value : val; + + const record = row.original; + if (column.getClipboardData) { + text = column.getClipboardData(row.value, record, row, props); + } else if (column.getValueToFilterOn) { + text = column.getValueToFilterOn(record, props); + } else if (column.render) { + text = column.render(row.value, record, row, props); + } else if (cellRenderer && cellRenderer[column.path]) { + text = cellRenderer[column.path](row.value, row.original, row, props); + } else if (text) { + text = React.isValidElement(text) ? text : String(text); + } + const getTextFromElementOrLink = text => { + if (React.isValidElement(text)) { + if (text.props?.to) { + // this will convert Link elements to url strings + return joinUrl( + window.location.origin, + window.frontEndConfig?.clientBasePath || "", + text.props.to + ); + } else { + return getTextFromEl(text); + } + } else { + return text; + } + }; + text = getTextFromElementOrLink(text); + + if (Array.isArray(text)) { + let arrText = text.map(getTextFromElementOrLink).join(", "); + // because we sometimes insert commas after links when mapping over an array of elements we will have double ,'s + arrText = arrText.replace(/, ,/g, ","); + text = arrText; + } + + const stringText = toString(text); + if (stringText === "[object Object]") return ""; + return stringText; }; - refocusTable = () => { - setTimeout(() => { - const table = this.tableRef.current?.tableRef?.closest( - ".data-table-container>div" - ); - table?.focus(); - }, 0); + const cancelCellEdit = () => { + setEditingCell(null); + refocusTable(); }; - isSelectionARectangle = () => { - const { entities, reduxFormSelectedCells, schema } = computePresets( - this.props - ); - if ( - reduxFormSelectedCells && - Object.keys(reduxFormSelectedCells).length > 1 - ) { + const isSelectionARectangle = () => { + if (selectedCells && Object.keys(selectedCells).length > 1) { const pathToIndex = getFieldPathToIndex(schema); const entityMap = getEntityIdToEntity(entities); // let primaryCellId; @@ -2297,7 +2017,7 @@ class DataTable extends React.Component { let lastRowIndex; let firstRowIndex; const selectedPaths = []; - Object.keys(reduxFormSelectedCells).forEach(key => { + Object.keys(selectedCells).forEach(key => { // if (reduxFormSelectedCells[key] === PRIMARY_SELECTED_VAL) { // primaryCellId = key; // } @@ -2336,46 +2056,225 @@ class DataTable extends React.Component { break; } } - } - } + } + } + + if (isRectangle) { + return { + isRect: true, + selectedPaths, + selectionGrid, + lastRowIndex, + lastCellIndex, + firstCellIndex, + firstRowIndex, + entityMap, + pathToIndex + }; + } else { + return {}; + } + } + return {}; + }; + + const onDragEnd = cellsToSelect => { + const [primaryRowId, primaryCellPath] = primarySelectedCellId.split(":"); + const pathToField = getFieldPathToField(schema); + const { selectedPaths, selectionGrid } = isSelectionARectangle(); + let allSelectedPaths = selectedPaths; + if (!allSelectedPaths) { + allSelectedPaths = [primaryCellPath]; + } + + updateEntitiesHelper(entities, entities => { + let newSelectedCells; + if (selectedPaths) { + newSelectedCells = { + ...selectedCells + }; + } else { + newSelectedCells = { + [primarySelectedCellId]: PRIMARY_SELECTED_VAL + }; + } + + const newCellValidate = { + ...reduxFormCellValidation + }; + const entityMap = getEntityIdToEntity(entities); + const { e: selectedEnt } = entityMap[primaryRowId]; + const firstCellToSelectRowIndex = + entityMap[cellsToSelect[0]?.split(":")[0]]?.i; + const pathToIndex = getFieldPathToIndex(schema); + + allSelectedPaths.forEach(selectedPath => { + const column = pathToField[selectedPath]; + + const selectedCellVal = getCellVal(selectedEnt, selectedPath, column); + const cellIndexOfSelectedPath = pathToIndex[selectedPath]; + let incrementStart; + let incrementPrefix; + let incrementPad = 0; + if (column.type === "string" || column.type === "number") { + const cellNumStr = getNumberStrAtEnd(selectedCellVal); + const cellNum = Number(cellNumStr); + const entityAbovePrimaryCell = + entities[entityMap[primaryRowId].i - 1]; + if (cellNumStr !== null && !isNaN(cellNum)) { + if ( + entityAbovePrimaryCell && + (!selectionGrid || selectionGrid.length <= 1) + ) { + const cellAboveVal = get( + entityAbovePrimaryCell, + selectedPath, + "" + ); + const cellAboveNumStr = getNumberStrAtEnd(cellAboveVal); + const cellAboveNum = Number(cellAboveNumStr); + if (!isNaN(cellAboveNum)) { + const isIncremental = cellNum - cellAboveNum === 1; + if (isIncremental) { + const cellTextNoNum = stripNumberAtEnd(selectedCellVal); + const sameText = + stripNumberAtEnd(cellAboveVal) === cellTextNoNum; + if (sameText) { + incrementStart = cellNum + 1; + incrementPrefix = cellTextNoNum || ""; + if (cellNumStr && cellNumStr.startsWith("0")) { + incrementPad = cellNumStr.length; + } + } + } + } + } + if (incrementStart === undefined) { + const draggingDown = + firstCellToSelectRowIndex > selectionGrid?.[0][0].rowIndex; + if (selectedPaths && draggingDown) { + let checkIncrement; + let prefix; + let maybePad; + // determine if all the cells in this column of the selectionGrid are incrementing + const allAreIncrementing = selectionGrid.every(row => { + // see if cell is selected + const cellInfo = row[cellIndexOfSelectedPath]; + if (!cellInfo) return false; + const { cellId } = cellInfo; + const [rowId] = cellId.split(":"); + const cellVal = getCellVal( + entityMap[rowId].e, + selectedPath, + pathToField[selectedPath] + ); + const cellNumStr = getNumberStrAtEnd(cellVal); + const cellNum = Number(cellNumStr); + const cellTextNoNum = stripNumberAtEnd(cellVal); + if (cellNumStr?.startsWith("0")) { + maybePad = cellNumStr.length; + } + if (cellTextNoNum && !prefix) { + prefix = cellTextNoNum; + } + if (cellTextNoNum && prefix !== cellTextNoNum) { + return false; + } + if (!isNaN(cellNum)) { + if (!checkIncrement) { + checkIncrement = cellNum; + return true; + } else { + return ++checkIncrement === cellNum; + } + } else { + return false; + } + }); + + if (allAreIncrementing) { + incrementStart = checkIncrement + 1; + incrementPrefix = prefix || ""; + incrementPad = maybePad; + } + } + } + } + } + + let firstSelectedCellRowIndex; + if (selectionGrid) { + selectionGrid[0].some(cell => { + if (cell) { + firstSelectedCellRowIndex = cell.rowIndex; + return true; + } + return false; + }); + } + + cellsToSelect.forEach(cellId => { + const [rowId, cellPath] = cellId.split(":"); + if (cellPath !== selectedPath) return; + newSelectedCells[cellId] = true; + const { e: entityToUpdate, i: rowIndex } = entityMap[rowId] || {}; + if (entityToUpdate) { + delete entityToUpdate._isClean; + let newVal; + if (incrementStart !== undefined) { + const num = incrementStart++; + newVal = incrementPrefix + padStart(num, incrementPad, "0"); + } else { + if (selectionGrid && selectionGrid.length > 1) { + // if there are multiple cells selected then we want to copy them repeating + // ex: if we have 1,2,3 selected and we drag for 5 more rows we want it to + // be 1,2,3,1,2 for the new row cells in this column + const draggingDown = rowIndex > firstSelectedCellRowIndex; + const cellIndex = pathToIndex[cellPath]; + let cellIdToCopy; + if (draggingDown) { + const { cellId } = selectionGrid[ + (rowIndex - firstSelectedCellRowIndex) % + selectionGrid.length + ].find(g => g && g.cellIndex === cellIndex); + cellIdToCopy = cellId; + } else { + const lastIndexInGrid = + selectionGrid[selectionGrid.length - 1][0].rowIndex; + const { cellId } = selectionGrid[ + (rowIndex + lastIndexInGrid + 1) % selectionGrid.length + ].find(g => g.cellIndex === cellIndex); + cellIdToCopy = cellId; + } + + const [rowIdToCopy, cellPathToCopy] = cellIdToCopy.split(":"); + newVal = getCellVal( + entityMap[rowIdToCopy].e, + cellPathToCopy, + pathToField[cellPathToCopy] + ); + } else { + newVal = selectedCellVal; + } + } + const { error } = editCellHelper({ + entity: entityToUpdate, + path: cellPath, + schema, + newVal + }); + newCellValidate[cellId] = error; + } + }); + }); - if (isRectangle) { - return { - isRect: true, - selectedPaths, - selectionGrid, - lastRowIndex, - lastCellIndex, - firstCellIndex, - firstRowIndex, - entityMap, - pathToIndex - }; - } else { - return {}; - } - } - return {}; + // select the new cells + updateValidation(entities, newCellValidate); + setSelectedCells(newSelectedCells); + }); }; - renderColumns = () => { - const { - isCellEditable, - cellRenderer, - withCheckboxes, - SubComponent, - shouldShowSubComponent, - entities, - reduxFormEditingCellSelectAll, - isEntityDisabled, - getCellHoverText, - withExpandAndCollapseAllButton, - reduxFormExpandedEntityIdMap, - change, - reduxFormSelectedCells, - reduxFormEditingCell - } = computePresets(this.props); - const { columns } = this.state; + const renderColumns = () => { if (!columns.length) { return columns; } @@ -2385,8 +2284,8 @@ class DataTable extends React.Component { ...(withExpandAndCollapseAllButton && { Header: () => { const showCollapseAll = - Object.values(reduxFormExpandedEntityIdMap).filter(i => i) - .length === entities.length; + Object.values(expandedEntityIdMap).filter(i => i).length === + entities.length; return ( { showCollapseAll - ? change("reduxFormExpandedEntityIdMap", {}) - : change( - "reduxFormExpandedEntityIdMap", - entities.reduce((acc, e) => { - acc[e.id] = true; - return acc; - }, {}) - ); + ? setExpandedEntityIdMap({}) + : setExpandedEntityIdMap(prev => { + const newMap = { ...prev }; + entities.forEach(e => { + newMap[getIdOrCodeOrIndex(e)] = true; + }); + return newMap; + }); }} className={classNames("tg-expander-all")} icon={showCollapseAll ? "chevron-down" : "chevron-right"} @@ -2440,8 +2339,8 @@ class DataTable extends React.Component { if (withCheckboxes) { columnsToRender.push({ - Header: this.renderCheckboxHeader, - Cell: this.renderCheckboxCell, + Header: renderCheckboxHeader, + Cell: renderCheckboxCell, width: 35, resizable: false, getHeaderProps: () => { @@ -2461,7 +2360,7 @@ class DataTable extends React.Component { columns.forEach(column => { const tableColumn = { ...column, - Header: this.renderColumnHeader(column), + Header: renderColumnHeader(column), accessor: column.path, getHeaderProps: () => ({ // needs to be a string because it is getting passed @@ -2480,13 +2379,13 @@ class DataTable extends React.Component { row.value, row.original, row, - this.props + props ); return val; }; } else if (column.render) { tableColumn.Cell = row => { - const val = column.render(row.value, row.original, row, this.props); + const val = column.render(row.value, row.original, row, props); return val; }; } else if (column.type === "timestamp") { @@ -2539,7 +2438,7 @@ class DataTable extends React.Component { const cellId = `${rowId}:${row.column.path}`; let val = oldFunc(...args); const oldVal = val; - const text = this.getCopyTextForCell(val, row, column); + const text = getCopyTextForCell(val, row, column); const isBool = column.type === "boolean"; const dataTest = { "data-test": "tgCell_" + column.path @@ -2547,497 +2446,235 @@ class DataTable extends React.Component { const fullValue = row.original?.[row.column.path]; if (isCellEditable && isBool) { val = ( - { - const checked = e.target.checked; - this.finishCellEdit(cellId, checked); - }} - /> - ); - noEllipsis = true; - } else { - if (reduxFormEditingCell === cellId) { - if (column.type === "genericSelect") { - const GenericSelectComp = column.GenericSelectComp; - - return ( - { - this.finishCellEdit(cellId, newVal, doNotStopEditing); - }} - dataTest={dataTest} - cancelEdit={this.cancelCellEdit} - /> - ); - } - if (column.type === "dropdown" || column.type === "dropdownMulti") { - return ( - { - this.finishCellEdit(cellId, newVal, doNotStopEditing); - }} - dataTest={dataTest} - cancelEdit={this.cancelCellEdit} - /> - ); - } else { - return ( - - change("reduxFormEditingCellSelectAll", false) - } - dataTest={dataTest} - shouldSelectAll={reduxFormEditingCellSelectAll} - cancelEdit={this.cancelCellEdit} - isNumeric={column.type === "number"} - initialValue={ - this.state.editableCellInitialValue.length - ? this.state.editableCellInitialValue - : text - } - isEditableCellInitialValue={ - !!this.state.editableCellInitialValue.length - } - finishEdit={newVal => { - this.finishCellEdit(cellId, newVal); - }} - /> - ); - } - } - } - - //wrap the original tableColumn.Cell function in another div in order to add a title attribute - let title = text; - if (getCellHoverText) title = getCellHoverText(...args); - else if (column.getTitleAttr) title = column.getTitleAttr(...args); - const isSelectedCell = reduxFormSelectedCells?.[cellId]; - // if (isSelectedCell) { - // const [rowId2, path] = cellId.split(":"); - // const selectedEnt = entities.find((e, i) => { - // return getIdOrCodeOrIndex(e, i) === rowId2; - // }); - // } - // if () - const { - isRect, - selectionGrid, - lastRowIndex, - lastCellIndex, - entityMap, - pathToIndex - } = this.isSelectionARectangle(); - // const __isHeaderCell = - return ( - <> -
- {val} -
- {isCellEditable && - (column.type === "dropdown" || - column.type === "dropdownMulti" || - column.type === "genericSelect") && ( - { - this.startCellEdit(cellId); - }} - /> - )} - - {isSelectedCell && - (isRect - ? isBottomRightCornerOfRectangle({ - cellId, - selectionGrid, - lastRowIndex, - lastCellIndex, - entityMap, - pathToIndex - }) - : isSelectedCell === PRIMARY_SELECTED_VAL) && ( - - )} - - ); - }; - - columnsToRender.push(tableColumn); - }); - return columnsToRender; - }; - - onDragEnd = cellsToSelect => { - const { - entities, - schema, - reduxFormCellValidation, - change, - reduxFormSelectedCells - } = this.props; - const primaryCellId = this.getPrimarySelectedCellId(); - const [primaryRowId, primaryCellPath] = primaryCellId.split(":"); - const pathToField = getFieldPathToField(schema); - const { selectedPaths, selectionGrid } = this.isSelectionARectangle(); - let allSelectedPaths = selectedPaths; - if (!allSelectedPaths) { - allSelectedPaths = [primaryCellPath]; - } - - this.updateEntitiesHelper(entities, entities => { - let newReduxFormSelectedCells; - if (selectedPaths) { - newReduxFormSelectedCells = { - ...reduxFormSelectedCells - }; - } else { - newReduxFormSelectedCells = { - [primaryCellId]: PRIMARY_SELECTED_VAL - }; - } - - const newCellValidate = { - ...reduxFormCellValidation - }; - const entityMap = getEntityIdToEntity(entities); - const { e: selectedEnt } = entityMap[primaryRowId]; - const firstCellToSelectRowIndex = - entityMap[cellsToSelect[0]?.split(":")[0]]?.i; - const pathToIndex = getFieldPathToIndex(schema); - - allSelectedPaths.forEach(selectedPath => { - const column = pathToField[selectedPath]; - - const selectedCellVal = getCellVal(selectedEnt, selectedPath, column); - const cellIndexOfSelectedPath = pathToIndex[selectedPath]; - let incrementStart; - let incrementPrefix; - let incrementPad = 0; - if (column.type === "string" || column.type === "number") { - const cellNumStr = getNumberStrAtEnd(selectedCellVal); - const cellNum = Number(cellNumStr); - const entityAbovePrimaryCell = - entities[entityMap[primaryRowId].i - 1]; - if (cellNumStr !== null && !isNaN(cellNum)) { - if ( - entityAbovePrimaryCell && - (!selectionGrid || selectionGrid.length <= 1) - ) { - const cellAboveVal = get( - entityAbovePrimaryCell, - selectedPath, - "" - ); - const cellAboveNumStr = getNumberStrAtEnd(cellAboveVal); - const cellAboveNum = Number(cellAboveNumStr); - if (!isNaN(cellAboveNum)) { - const isIncremental = cellNum - cellAboveNum === 1; - if (isIncremental) { - const cellTextNoNum = stripNumberAtEnd(selectedCellVal); - const sameText = - stripNumberAtEnd(cellAboveVal) === cellTextNoNum; - if (sameText) { - incrementStart = cellNum + 1; - incrementPrefix = cellTextNoNum || ""; - if (cellNumStr && cellNumStr.startsWith("0")) { - incrementPad = cellNumStr.length; - } - } - } - } - } - if (incrementStart === undefined) { - const draggingDown = - firstCellToSelectRowIndex > selectionGrid?.[0][0].rowIndex; - if (selectedPaths && draggingDown) { - let checkIncrement; - let prefix; - let maybePad; - // determine if all the cells in this column of the selectionGrid are incrementing - const allAreIncrementing = selectionGrid.every(row => { - // see if cell is selected - const cellInfo = row[cellIndexOfSelectedPath]; - if (!cellInfo) return false; - const { cellId } = cellInfo; - const [rowId] = cellId.split(":"); - const cellVal = getCellVal( - entityMap[rowId].e, - selectedPath, - pathToField[selectedPath] - ); - const cellNumStr = getNumberStrAtEnd(cellVal); - const cellNum = Number(cellNumStr); - const cellTextNoNum = stripNumberAtEnd(cellVal); - if (cellNumStr?.startsWith("0")) { - maybePad = cellNumStr.length; - } - if (cellTextNoNum && !prefix) { - prefix = cellTextNoNum; - } - if (cellTextNoNum && prefix !== cellTextNoNum) { - return false; - } - if (!isNaN(cellNum)) { - if (!checkIncrement) { - checkIncrement = cellNum; - return true; - } else { - return ++checkIncrement === cellNum; - } - } else { - return false; - } - }); + { + const checked = e.target.checked; + finishCellEdit(cellId, checked); + }} + /> + ); + noEllipsis = true; + } else if (editingCell === cellId) { + if (column.type === "genericSelect") { + const GenericSelectComp = column.GenericSelectComp; - if (allAreIncrementing) { - incrementStart = checkIncrement + 1; - incrementPrefix = prefix || ""; - incrementPad = maybePad; - } - } - } + return ( + { + finishCellEdit(cellId, newVal, doNotStopEditing); + }} + dataTest={dataTest} + cancelEdit={cancelCellEdit} + /> + ); + } + if (column.type === "dropdown" || column.type === "dropdownMulti") { + return ( + { + finishCellEdit(cellId, newVal, doNotStopEditing); + }} + dataTest={dataTest} + cancelEdit={cancelCellEdit} + /> + ); + } else { + return ( + setEditingCellSelectAll(false)} + dataTest={dataTest} + shouldSelectAll={editingCellSelectAll} + cancelEdit={cancelCellEdit} + isNumeric={column.type === "number"} + initialValue={text} + finishEdit={newVal => { + finishCellEdit(cellId, newVal); + }} + /> + ); } } - let firstSelectedCellRowIndex; - if (selectionGrid) { - selectionGrid[0].some(cell => { - if (cell) { - firstSelectedCellRowIndex = cell.rowIndex; - return true; - } - return false; - }); - } + //wrap the original tableColumn.Cell function in another div in order to add a title attribute + let title = text; + if (getCellHoverText) title = getCellHoverText(...args); + else if (column.getTitleAttr) title = column.getTitleAttr(...args); + const isSelectedCell = selectedCells?.[cellId]; + const { + isRect, + selectionGrid, + lastRowIndex, + lastCellIndex, + entityMap, + pathToIndex + } = isSelectionARectangle(); - cellsToSelect.forEach(cellId => { - const [rowId, cellPath] = cellId.split(":"); - if (cellPath !== selectedPath) return; - newReduxFormSelectedCells[cellId] = true; - const { e: entityToUpdate, i: rowIndex } = entityMap[rowId] || {}; - if (entityToUpdate) { - delete entityToUpdate._isClean; - let newVal; - if (incrementStart !== undefined) { - const num = incrementStart++; - newVal = incrementPrefix + padStart(num, incrementPad, "0"); - } else { - if (selectionGrid && selectionGrid.length > 1) { - // if there are multiple cells selected then we want to copy them repeating - // ex: if we have 1,2,3 selected and we drag for 5 more rows we want it to - // be 1,2,3,1,2 for the new row cells in this column - const draggingDown = rowIndex > firstSelectedCellRowIndex; - const cellIndex = pathToIndex[cellPath]; - let cellIdToCopy; - if (draggingDown) { - const { cellId } = selectionGrid[ - (rowIndex - firstSelectedCellRowIndex) % - selectionGrid.length - ].find(g => g && g.cellIndex === cellIndex); - cellIdToCopy = cellId; - } else { - const lastIndexInGrid = - selectionGrid[selectionGrid.length - 1][0].rowIndex; - const { cellId } = selectionGrid[ - (rowIndex + lastIndexInGrid + 1) % selectionGrid.length - ].find(g => g.cellIndex === cellIndex); - cellIdToCopy = cellId; - } + return ( + <> +
+ {val} +
+ {isCellEditable && + (column.type === "dropdown" || + column.type === "dropdownMulti" || + column.type === "genericSelect") && ( + { + startCellEdit(cellId); + }} + /> + )} - const [rowIdToCopy, cellPathToCopy] = cellIdToCopy.split(":"); - newVal = getCellVal( - entityMap[rowIdToCopy].e, - cellPathToCopy, - pathToField[cellPathToCopy] - ); - } else { - newVal = selectedCellVal; - } - } - const { error } = editCellHelper({ - entity: entityToUpdate, - path: cellPath, - schema, - newVal - }); - newCellValidate[cellId] = error; - } - }); - }); + {isSelectedCell && + (isRect + ? isBottomRightCornerOfRectangle({ + cellId, + selectionGrid, + lastRowIndex, + lastCellIndex, + entityMap, + pathToIndex + }) + : isSelectedCell === PRIMARY_SELECTED_VAL) && ( + + )} + + ); + }; - // select the new cells - this.updateValidation(entities, newCellValidate); - change("reduxFormSelectedCells", newReduxFormSelectedCells); + columnsToRender.push(tableColumn); }); + return columnsToRender; }; - getCopyTextForCell = (val, row = {}, column = {}) => { - const { cellRenderer } = computePresets(this.props); - // TODOCOPY we need a way to potentially omit certain columns from being added as a \t element (talk to taoh about this) - let text = typeof val !== "string" ? row.value : val; + const handleCellClick = ({ event, cellId }) => { + if (!cellId) return; + const [rowId, cellPath] = cellId.split(":"); + const entityMap = getEntityIdToEntity(entities); + const { e: entity, i: rowIndex } = entityMap[rowId]; + const pathToIndex = getFieldPathToIndex(schema); + const columnIndex = pathToIndex[cellPath]; + const rowDisabled = isEntityDisabled(entity); - const record = row.original; - if (column.getClipboardData) { - text = column.getClipboardData(row.value, record, row, this.props); - } else if (column.getValueToFilterOn) { - text = column.getValueToFilterOn(record, this.props); - } else if (column.render) { - text = column.render(row.value, record, row, this.props); - } else if (cellRenderer && cellRenderer[column.path]) { - text = cellRenderer[column.path]( - row.value, - row.original, - row, - this.props - ); - } else if (text) { - text = React.isValidElement(text) ? text : String(text); - } - const getTextFromElementOrLink = text => { - if (React.isValidElement(text)) { - if (text.props?.to) { - // this will convert Link elements to url strings - return joinUrl( - window.location.origin, - window.frontEndConfig?.clientBasePath || "", - text.props.to - ); - } else { - return getTextFromEl(text); - } + if (rowDisabled) return; + let newSelectedCells = { + ...selectedCells + }; + if (newSelectedCells[cellId] && !event.shiftKey) { + // don't deselect if editing + if (editingCell === cellId) return; + if (event.metaKey) { + delete newSelectedCells[cellId]; } else { - return text; + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } - }; - text = getTextFromElementOrLink(text); - - if (Array.isArray(text)) { - let arrText = text.map(getTextFromElementOrLink).join(", "); - // because we sometimes insert commas after links when mapping over an array of elements we will have double ,'s - arrText = arrText.replace(/, ,/g, ","); - text = arrText; - } - - const stringText = toString(text); - if (stringText === "[object Object]") return ""; - return stringText; - }; - - addEditableTableEntities = incomingEnts => { - const { entities = [], reduxFormCellValidation } = computePresets( - this.props - ); - - this.updateEntitiesHelper(entities, entities => { - const newEntities = incomingEnts.map(e => ({ - ...e, - id: e.id || nanoid(), - _isClean: false - })); + } else { + if (event.metaKey) { + if (isEmpty(newSelectedCells)) { + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + } else { + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + if (primarySelectedCellId) + newSelectedCells[primarySelectedCellId] = true; + } + } else if (event.shiftKey) { + if (primarySelectedCellId) { + const [rowId, colPath] = primarySelectedCellId.split(":"); + const primaryRowIndex = entities.findIndex((e, i) => { + return getIdOrCodeOrIndex(e, i) === rowId; + }); + const fieldToIndex = getFieldPathToIndex(schema); + const primaryColIndex = fieldToIndex[colPath]; - const { newEnts, validationErrors } = this.formatAndValidateEntities( - newEntities, - { - useDefaultValues: true, - indexToStartAt: entities.length + if (primaryRowIndex === -1 || primaryColIndex === -1) { + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + } else { + const minRowIndex = min([primaryRowIndex, rowIndex]); + const minColIndex = min([primaryColIndex, columnIndex]); + const maxRowIndex = max([primaryRowIndex, rowIndex]); + const maxColIndex = max([primaryColIndex, columnIndex]); + const entitiesBetweenRows = entities.slice( + minRowIndex, + maxRowIndex + 1 + ); + const fieldsBetweenCols = schema.fields.slice( + minColIndex, + maxColIndex + 1 + ); + newSelectedCells = { + [primarySelectedCellId]: PRIMARY_SELECTED_VAL + }; + entitiesBetweenRows.forEach(e => { + const rowId = getIdOrCodeOrIndex(e, entities.indexOf(e)); + fieldsBetweenCols.forEach(f => { + const cellId = `${rowId}:${f.path}`; + if (!newSelectedCells[cellId]) newSelectedCells[cellId] = true; + }); + }); + // newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + // newSelectedCells[primarySelectedCellId] = true; + } + } else { + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } - ); - if (every(entities, "_isClean")) { - forEach(newEnts, (e, i) => { - entities[i] = e; - }); } else { - entities.splice(entities.length, 0, ...newEnts); + newSelectedCells = {}; + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; } - - this.updateValidation(entities, { - ...reduxFormCellValidation, - ...validationErrors - }); - }); - }; - - getEditableTableInfoAndThrowFormError = () => { - const { schema, reduxFormEntities, reduxFormCellValidation } = - computePresets(this.props); - const { entsToUse, validationToUse } = removeCleanRows( - reduxFormEntities, - reduxFormCellValidation - ); - const validationWTableErrs = validateTableWideErrors({ - entities: entsToUse, - schema, - newCellValidate: validationToUse - }); - - if (!entsToUse?.length) { - throwFormError( - "Please add at least one row to the table before submitting." - ); - } - const invalid = - isEmpty(validationWTableErrs) || !some(validationWTableErrs, v => v) - ? undefined - : validationWTableErrs; - - if (invalid) { - throwFormError("Please fix the errors in the table before submitting."); } - return entsToUse; + setSelectedCells(newSelectedCells); }; - insertRows = ({ above, numRows = 1, appendToBottom } = {}) => { - const { entities = [], reduxFormCellValidation } = computePresets( - this.props - ); - - const primaryCellId = this.getPrimarySelectedCellId(); - const [rowId] = primaryCellId?.split(":") || []; - this.updateEntitiesHelper(entities, entities => { + const insertRows = ({ above, numRows = 1, appendToBottom } = {}) => { + const [rowId] = primarySelectedCellId?.split(":") || []; + updateEntitiesHelper(entities, entities => { const newEntities = times(numRows).map(() => ({ id: nanoid() })); const indexToInsert = entities.findIndex((e, i) => { @@ -3045,7 +2682,7 @@ class DataTable extends React.Component { }); const insertIndex = above ? indexToInsert : indexToInsert + 1; const insertIndexToUse = appendToBottom ? entities.length : insertIndex; - let { newEnts, validationErrors } = this.formatAndValidateEntities( + let { newEnts, validationErrors } = formatAndValidateEntities( newEntities, { useDefaultValues: true, @@ -3057,33 +2694,25 @@ class DataTable extends React.Component { ...e, _isClean: true })); - this.updateValidation(entities, { + updateValidation(entities, { ...reduxFormCellValidation, ...validationErrors }); entities.splice(insertIndexToUse, 0, ...newEnts); }); - this.refocusTable(); + refocusTable(); }; - showContextMenu = (e, idMap) => { - const { - history, - contextMenu, - isCopyable, - isCellEditable, - entities = [], - reduxFormSelectedCells = {} - } = computePresets(this.props); + const showContextMenu = (e, { idMap, selectedCells } = {}) => { let selectedRecords; if (isCellEditable) { const rowIds = {}; - Object.keys(reduxFormSelectedCells).forEach(cellKey => { + Object.keys(selectedCells).forEach(cellKey => { const [rowId] = cellKey.split(":"); rowIds[rowId] = true; }); - selectedRecords = entities.filter(e => rowIds[getIdOrCodeOrIndex(e)]); + selectedRecords = entities.filter(ent => rowIds[getIdOrCodeOrIndex(ent)]); } else { selectedRecords = getRecordsFromIdMap(idMap); } @@ -3163,7 +2792,7 @@ class DataTable extends React.Component { { - this.handleCopySelectedRows(selectedRecords, e); + handleCopySelectedRows(selectedRecords, e); // loop through each cell in the row }} text="Rows" @@ -3174,14 +2803,14 @@ class DataTable extends React.Component { { - this.handleCopyTable(e); + handleCopyTable(e); // loop through each cell in the row }} text="Table" /> ); } - const selectedRowIds = Object.keys(reduxFormSelectedCells).map(cellId => { + const selectedRowIds = Object.keys(selectedCells).map(cellId => { const [rowId] = cellId.split(":"); return rowId; }); @@ -3201,7 +2830,7 @@ class DataTable extends React.Component { text="Add Row Above" key="addRowAbove" onClick={() => { - this.insertRows({ above: true }); + insertRows({ above: true }); }} /> { - this.insertRows({}); + insertRows({}); }} /> 1 ? "s" : ""}`} key="removeRow" onClick={() => { - const { - entities = [], - reduxFormCellValidation, - reduxFormSelectedCells = {} - } = computePresets(this.props); - const selectedRowIds = Object.keys(reduxFormSelectedCells).map( + const selectedRowIds = Object.keys(selectedCells).map( cellId => { const [rowId] = cellId.split(":"); return rowId; } ); - this.updateEntitiesHelper(entities, entities => { + updateEntitiesHelper(entities, entities => { const ents = entities.filter( (e, i) => !selectedRowIds.includes(getIdOrCodeOrIndex(e, i)) ); - this.updateValidation( + updateValidation( ents, omitBy(reduxFormCellValidation, (v, cellId) => selectedRowIds.includes(cellId.split(":")[0]) @@ -3240,9 +2864,9 @@ class DataTable extends React.Component { ); return ents; }); - this.refocusTable(); + refocusTable(); }} - > + /> )} @@ -3250,215 +2874,822 @@ class DataTable extends React.Component { ContextMenu.show(menu, { left: e.clientX, top: e.clientY }); }; - renderColumnHeader = column => { - const { - addFilters, - setOrder, - order, - withFilter, - withSort, - filters, - removeSingleFilter, - currentParams, - isLocalCall, - setNewParams, - compact, - isCellEditable, - extraCompact, - entities - } = computePresets(this.props); - const { - displayName, - description, - isUnique, - sortDisabled, - filterDisabled, - columnFilterDisabled, - renderTitleInner, - filterIsActive = noop, - noTitle, - isNotEditable, - type, - path - } = column; - const columnDataType = column.type; - const isActionColumn = columnDataType === "action"; - const disableSorting = - sortDisabled || - isActionColumn || - (!isLocalCall && typeof path === "string" && path.includes(".")) || - columnDataType === "color"; - const disableFiltering = - filterDisabled || - columnDataType === "color" || - isActionColumn || - columnFilterDisabled; - const ccDisplayName = camelCase(displayName || path); - let columnTitle = displayName || startCase(camelCase(path)); - if (isActionColumn) columnTitle = ""; + const getTableRowProps = (state, rowInfo) => { + if (!rowInfo) { + return { + className: "no-row-data" + }; + } + const entity = rowInfo.original; + const rowId = getIdOrCodeOrIndex(entity, rowInfo.index); + const rowSelected = reduxFormSelectedEntityIdMap[rowId]; + const isExpanded = expandedEntityIdMap[rowId]; + const rowDisabled = isEntityDisabled(entity); + const dataId = entity.id || entity.code; + return { + onClick: e => { + if (isCellEditable) return; + // if checkboxes are activated or row expander is clicked don't select row + if (e.target.matches(".tg-expander, .tg-expander *")) { + setExpandedEntityIdMap(prev => ({ ...prev, [rowId]: !isExpanded })); + return; + } else if ( + e.target.closest(".tg-react-table-checkbox-cell-container") + ) { + return; + } else if (mustClickCheckboxToSelect) { + return; + } + if (e.detail > 1) { + return; //cancel multiple quick clicks + } + rowClick(e, rowInfo, entities, { + reduxFormSelectedEntityIdMap, + isSingleSelect, + noSelect, + onRowClick, + isEntityDisabled, + withCheckboxes, + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + change + }); + }, + //row right click + onContextMenu: e => { + e.preventDefault(); + if (rowId === undefined || rowDisabled || isCellEditable) return; + const oldIdMap = cloneDeep(reduxFormSelectedEntityIdMap) || {}; + let newIdMap; + if (withCheckboxes) { + newIdMap = oldIdMap; + } else { + // if we are not using checkboxes we need to make sure + // that the id of the record gets added to the id map + newIdMap = oldIdMap[rowId] ? oldIdMap : { [rowId]: { entity } }; - const currentFilter = - filters && - !!filters.length && - filters.filter(({ filterOn }) => { - return filterOn === ccDisplayName; - })[0]; - const filterActiveForColumn = - !!currentFilter || filterIsActive(currentParams); - let ordering; - if (order && order.length) { - order.forEach(order => { - const orderField = order.replace("-", ""); - if (orderField === ccDisplayName) { - if (orderField === order) { - ordering = "asc"; - } else { - ordering = "desc"; - } + // tgreen: this will refresh the selection with fresh data. The entities in redux might not be up to date + const keyedEntities = keyBy(entities, getIdOrCodeOrIndex); + forEach(newIdMap, (val, key) => { + const freshEntity = keyedEntities[key]; + if (freshEntity) { + newIdMap[key] = { ...newIdMap[key], entity: freshEntity }; + } + }); + finalizeSelection({ + idMap: newIdMap, + entities, + props: { + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + noSelect, + change + } + }); } - }); - } + showContextMenu(e, { idMap: newIdMap, selectedCells }); + }, + className: classNames( + "with-row-data", + getRowClassName && getRowClassName(rowInfo, state, props), + { + disabled: rowDisabled, + selected: rowSelected && !withCheckboxes, + "rt-tr-last-row": rowInfo.index === entities.length - 1 + } + ), + "data-test-id": dataId === undefined ? rowInfo.index : dataId, + "data-index": rowInfo.index, + onDoubleClick: e => { + if (rowDisabled) return; + onDoubleClick && + onDoubleClick(rowInfo.original, rowInfo.index, history, e); + } + }; + }; - const sortDown = ordering && ordering === "asc"; - const sortUp = ordering && !sortDown; - const sortComponent = - withSort && !disableSorting ? ( -
- { - setOrder("-" + ccDisplayName, sortUp, e.shiftKey); - }} - /> - { - setOrder(ccDisplayName, sortDown, e.shiftKey); - }} - /> -
- ) : null; - const FilterMenu = column.FilterMenu || FilterAndSortMenu; + const getTableCellProps = (state, rowInfo, column) => { + if (!isCellEditable) return {}; //only allow cell selection to do stuff here + if (!rowInfo) return {}; + if (!reduxFormCellValidation) return {}; + const entity = rowInfo.original; + const rowIndex = rowInfo.index; + const rowId = getIdOrCodeOrIndex(entity, rowIndex); + const { + cellId, + cellIdAbove, + cellIdToRight, + cellIdBelow, + cellIdToLeft, + rowDisabled, + columnIndex + } = getCellInfo({ + columnIndex: column.index, + columnPath: column.path, + rowId, + schema, + entities, + rowIndex, + isEntityDisabled, + entity + }); + + const _isClean = + (entity._isClean && doNotValidateUntouchedRows) || isEntityClean(entity); + + const err = !_isClean && reduxFormCellValidation[cellId]; + let selectedTopBorder, + selectedRightBorder, + selectedBottomBorder, + selectedLeftBorder; + if (selectedCells[cellId]) { + selectedTopBorder = !selectedCells[cellIdAbove]; + selectedRightBorder = !selectedCells[cellIdToRight]; + selectedBottomBorder = !selectedCells[cellIdBelow]; + selectedLeftBorder = !selectedCells[cellIdToLeft]; + } + const isPrimarySelected = selectedCells[cellId] === PRIMARY_SELECTED_VAL; + const className = classNames({ + isSelectedCell: selectedCells[cellId], + isPrimarySelected, + isSecondarySelected: selectedCells[cellId] === true, + noSelectedTopBorder: !selectedTopBorder, + isCleanRow: _isClean, + noSelectedRightBorder: !selectedRightBorder, + noSelectedBottomBorder: !selectedBottomBorder, + noSelectedLeftBorder: !selectedLeftBorder, + isDropdownCell: column.type === "dropdown", + isEditingCell: editingCell === cellId, + hasCellError: !!err, + "no-data-tip": selectedCells[cellId] + }); + return { + onDoubleClick: () => { + // cell double click + if (rowDisabled) return; + startCellEdit(cellId); + }, + ...(err && { + "data-tip": err?.message || err, + "data-no-child-data-tip": true + }), + onContextMenu: e => { + const newSelectedCells = { ...selectedCells }; + if (!isPrimarySelected) { + if (primarySelectedCellId) { + newSelectedCells[primarySelectedCellId] = true; + } + newSelectedCells[cellId] = PRIMARY_SELECTED_VAL; + setSelectedCells(newSelectedCells); + } + showContextMenu(e, { selectedCells: newSelectedCells }); + }, + onClick: event => { + handleCellClick({ + event, + cellId, + rowDisabled, + rowIndex, + columnIndex + }); + }, + className + }; + }; - const filterMenu = - withFilter && !disableFiltering ? ( - - ) : null; - let maybeCheckbox; - if (isCellEditable && !isNotEditable && type === "boolean") { - let isIndeterminate = false; - let isChecked = !!entities.length; - let hasFalse; - let hasTrue; - entities.some(e => { - if (!get(e, path)) { - isChecked = false; - hasFalse = true; + if (withSelectAll && !safeQuery) { + throw new Error("safeQuery is needed for selecting all table records"); + } + + let compactClassName = ""; + if (compactPaging) { + compactClassName += " tg-compact-paging"; + } + compactClassName += extraCompact + ? " tg-extra-compact-table" + : compact + ? " tg-compact-table" + : ""; + + const hasFilters = + filters.length || + searchTerm || + schema.fields.some( + field => field.filterIsActive && field.filterIsActive(currentParams) + ); + + const additionalFilterKeys = schema.fields.reduce((acc, field) => { + if (field.filterKey) acc.push(field.filterKey); + return acc; + }, []); + + const filtersOnNonDisplayedFields = []; + if (filters && filters.length) { + schema.fields.forEach(({ isHidden, displayName, path }) => { + const ccDisplayName = camelCase(displayName || path); + if (isHidden) { + filters.forEach(filter => { + if (filter.filterOn === ccDisplayName) { + filtersOnNonDisplayedFields.push({ + ...filter, + displayName + }); + } + }); + } + }); + } + const numRows = isInfinite ? entities.length : pageSize; + const idMap = reduxFormSelectedEntityIdMap || {}; + const selectedRowCount = Object.keys(idMap).filter(key => idMap[key]).length; + + let rowsToShow = doNotShowEmptyRows + ? Math.min(numRows, entities.length) + : numRows; + // if there are no entities then provide enough space to show + // no rows found message + if (entities.length === 0 && rowsToShow < 3) rowsToShow = 3; + const expandedRows = entities.reduce((acc, row, index) => { + const rowId = getIdOrCodeOrIndex(row, index); + acc[index] = expandedEntityIdMap[rowId]; + return acc; + }, {}); + let children = maybeChildren; + if (children && typeof children === "function") { + children = children(props); + } + const showHeader = (withTitle || withSearch || children) && !noHeader; + const toggleFullscreenButton = ( + ; + }} + columns={renderColumns()} + pageSize={rowsToShow} + expanded={expandedRows} + showPagination={false} + sortable={false} + loading={isLoading || disabled} + defaultResized={resized} + onResizedChange={(newResized = []) => { + const resizedToUse = newResized.map(column => { + // have a min width of 50 so that columns don't disappear + if (column.value < 50) { + return { + ...column, + value: 50 + }; + } else { + return column; + } + }); + resizePersist(resizedToUse); + }} + TheadComponent={TheadComponent} + ThComponent={ThComponent} + getTrGroupProps={getTableRowProps} + getTdProps={getTableCellProps} + NoDataComponent={({ children }) => + isLoading ? null : ( +
+ {noRowsFoundMessage || children} +
+ ) + } + LoadingComponent={props => ( + + )} + style={{ + maxHeight, + minHeight: 150, + ...style + }} + SubComponent={SubComponentToUse} + {...ReactTableProps} + /> + {isCellEditable && ( +
+
+ {!onlyShowRowsWErrors && ( + + )} +
+
+ )} + {!noFooter && ( +
+ {selectedAndTotalMessage} +
+ {additionalFooterButtons} + {!noFullscreenButton && toggleFullscreenButton} + {withDisplayOptions && ( + + )} + {shouldShowPaging && ( + { + change("reduxFormSelectedEntityIdMap", newIdMap); + }} + /> + )} +
+
+ )} - ); - }; -} + + ); +}; -// const CompToExport = dataTableEnhancer(HotkeysTarget(DataTable)); -// // CompToExport.selectRecords = (form, value) => { -// // return change(form, "reduxFormSelectedEntityIdMap", value) -// // } -// export default CompToExport const WrappedDT = dataTableEnhancer(DataTable); export default WrappedDT; const ConnectedPagingTool = dataTableEnhancer(PagingTool); diff --git a/packages/ui/src/DataTable/utils/computePresets.js b/packages/ui/src/DataTable/utils/computePresets.js deleted file mode 100644 index aaa72fe9..00000000 --- a/packages/ui/src/DataTable/utils/computePresets.js +++ /dev/null @@ -1,42 +0,0 @@ -import { omitBy, isNil } from "lodash-es"; -//we use this to make adding preset prop groups simpler -export default function computePresets(props = {}) { - const { isSimple } = props; - let toReturn = omitBy(props, isNil); - toReturn.pageSize = toReturn.controlled_pageSize || toReturn.pageSize; - if (isSimple) { - //isSimplePreset - toReturn = { - noHeader: true, - noFooter: !props.withPaging, - noPadding: true, - noFullscreenButton: true, - hidePageSizeWhenPossible: !props.withPaging, - isInfinite: !props.withPaging, - hideSelectedCount: true, - withTitle: false, - withSearch: false, - compact: true, - withPaging: false, - withFilter: false, - ...toReturn - }; - } else { - toReturn = { - // the usual defaults: - noFooter: false, - noPadding: false, - compact: true, - noFullscreenButton: false, - hidePageSizeWhenPossible: false, - isInfinite: false, - hideSelectedCount: false, - withTitle: true, - withSearch: true, - withPaging: true, - withFilter: true, - ...toReturn - }; - } - return toReturn || {}; -} diff --git a/packages/ui/src/DataTable/utils/convertSchema.js b/packages/ui/src/DataTable/utils/convertSchema.js index 8b240aab..a3acec42 100644 --- a/packages/ui/src/DataTable/utils/convertSchema.js +++ b/packages/ui/src/DataTable/utils/convertSchema.js @@ -1,7 +1,7 @@ import { camelCase } from "lodash-es"; import { startCase, keyBy, map } from "lodash-es"; -function convertSchema(schema) { +const convertSchema = schema => { let schemaToUse = schema; if (!schemaToUse.fields && Array.isArray(schema)) { schemaToUse = { @@ -43,7 +43,7 @@ function convertSchema(schema) { return fieldToUse; }); return schemaToUse; -} +}; export function mergeSchemas(_originalSchema, _overrideSchema) { const originalSchema = convertSchema(_originalSchema); diff --git a/packages/ui/src/DataTable/utils/handleCopyTable.js b/packages/ui/src/DataTable/utils/handleCopyTable.js new file mode 100644 index 00000000..a6cbe059 --- /dev/null +++ b/packages/ui/src/DataTable/utils/handleCopyTable.js @@ -0,0 +1,16 @@ +import { getAllRows } from "./getAllRows"; +import { handleCopyRows } from "./handleCopyRows"; + +export const handleCopyTable = (e, opts) => { + try { + const allRowEls = getAllRows(e); + if (!allRowEls) return; + handleCopyRows(allRowEls, { + ...opts, + onFinishMsg: "Table Copied" + }); + } catch (error) { + console.error(`error:`, error); + window.toastr.error("Error copying rows."); + } +}; diff --git a/packages/ui/src/DataTable/utils/index.js b/packages/ui/src/DataTable/utils/index.js index 2d85f6cb..7aa6dbfe 100644 --- a/packages/ui/src/DataTable/utils/index.js +++ b/packages/ui/src/DataTable/utils/index.js @@ -2,7 +2,6 @@ import { isEntityClean } from "./isEntityClean"; import { getSelectedRowsFromEntities } from "./selection"; import { removeCleanRows } from "./removeCleanRows"; import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; -import computePresets from "./computePresets"; import { getRecordsFromIdMap } from "./withSelectedEntities"; import { formatPasteData } from "./formatPasteData"; import { getFieldPathToField } from "./getFieldPathToField"; @@ -23,9 +22,9 @@ import { handleCopyHelper } from "./handleCopyHelper"; import { handleCopyRows } from "./handleCopyRows"; import { handleCopyColumn } from "./handleCopyColumn"; import { isBottomRightCornerOfRectangle } from "./isBottomRightCornerOfRectangle"; +import { handleCopyTable } from "./handleCopyTable"; export { - computePresets, defaultParsePaste, formatPasteData, getAllRows, @@ -44,6 +43,7 @@ export { handleCopyColumn, handleCopyHelper, handleCopyRows, + handleCopyTable, isBottomRightCornerOfRectangle, isEntityClean, removeCleanRows, diff --git a/packages/ui/src/DataTable/utils/rowClick.js b/packages/ui/src/DataTable/utils/rowClick.js index e4ee123a..f02c3010 100644 --- a/packages/ui/src/DataTable/utils/rowClick.js +++ b/packages/ui/src/DataTable/utils/rowClick.js @@ -3,15 +3,25 @@ import { getSelectedRowsFromEntities } from "./selection"; import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex"; import { getRecordsFromIdMap } from "./withSelectedEntities"; -export default function rowClick(e, rowInfo, entities, props) { - const { +export default function rowClick( + e, + rowInfo, + entities, + { reduxFormSelectedEntityIdMap, isSingleSelect, noSelect, onRowClick, isEntityDisabled, - withCheckboxes - } = props; + withCheckboxes, + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + change + } +) { const entity = rowInfo.original; onRowClick(e, entity, rowInfo); if (noSelect || isEntityDisabled(entity)) return; @@ -101,7 +111,19 @@ export default function rowClick(e, rowInfo, entities, props) { } } - finalizeSelection({ idMap: newIdMap, entities, props }); + finalizeSelection({ + idMap: newIdMap, + entities, + props: { + onDeselect, + onSingleRowSelect, + onMultiRowSelect, + noDeselectAll, + onRowSelect, + noSelect, + change + } + }); } export function changeSelectedEntities({ idMap, entities = [], change }) { diff --git a/packages/ui/src/DataTable/utils/useTableParams.js b/packages/ui/src/DataTable/utils/useTableParams.js new file mode 100644 index 00000000..9c8b50c8 --- /dev/null +++ b/packages/ui/src/DataTable/utils/useTableParams.js @@ -0,0 +1,368 @@ +import { useContext, useEffect, useMemo, useState } from "react"; +import { change } from "redux-form"; +import { useDispatch, useSelector } from "react-redux"; +import { camelCase, isFunction, keyBy, get } from "lodash-es"; +import TableFormTrackerContext from "../TableFormTrackerContext"; +import { viewColumn, openColumn } from "./viewColumn"; +import convertSchema from "./convertSchema"; +import { getRecordsFromIdMap } from "./withSelectedEntities"; +import { + makeDataTableHandlers, + getQueryParams, + setCurrentParamsOnUrl, + getCurrentParamsFromUrl +} from "./queryParams"; +import getTableConfigFromStorage from "./getTableConfigFromStorage"; + +const useSelectorOptions = { + devModeChecks: { stabilityCheck: "never" } +}; + +/* +NOTE: +This haven't been tested yet. It is the first version of what we should replace withTableParams +and also the first bit of the DataTable. +*/ + +/** + * Note all these options can be passed at Design Time or at Runtime (like reduxForm()) + * + * @export + * + * @param {compOrOpts} compOrOpts + * @typedef {object} compOrOpts + * @property {*string} formName - required unique identifier for the table + * @property {Object | Function} schema - The data table schema or a function returning it. The function wll be called with props as the argument. + * @property {boolean} urlConnected - whether the table should connect to/update the URL + * @property {boolean} withSelectedEntities - whether or not to pass the selected entities + * @property {boolean} isCodeModel - whether the model is keyed by code instead of id in the db + * @property {object} defaults - tableParam defaults such as pageSize, filter, etc + * @property {boolean} noOrderError - won't console an error if an order is not found on schema + */ +export default function useTableParams( + props // This should be the same as the spread above +) { + const { + formName, + isTableParamsConnected, + urlConnected, + onlyOneFilter, + defaults = {}, + // WE NEED THIS HOOK TO BE WRAPPED IN A WITHROUTER OR MOVE TO REACT-ROUTER-DOM 5 + // BEST SOLUTION IS TO ASSUME IT IS GOING TO BE RECEIVED + history, + withSelectedEntities, + tableParams: _tableParams, + schema: __schema, + noForm, + orderByFirstColumn, + withDisplayOptions, + syncDisplayOptionsToDb, + tableConfigurations, + isViewable, + isOpenable, + showEmptyColumnsByDefault, + isSimple, + entities: _origEntities = [], + cellRenderer, + additionalFilter, + additionalOrFilter, + doNotCoercePageSize, + isLocalCall + } = props; + const isInfinite = props.isInfinite || isSimple || !props.withPaging; + const additionalFilterToUse = + typeof additionalFilter === "function" + ? additionalFilter.bind(this, props) + : () => additionalFilter; + + const additionalOrFilterToUse = + typeof additionalOrFilter === "function" + ? additionalOrFilter.bind(this, props) + : () => additionalOrFilter; + + let _schema; + if (isFunction(__schema)) _schema = __schema(props); + else _schema = __schema; + const convertedSchema = convertSchema(_schema); + + if (isLocalCall) { + if (!noForm && (!formName || formName === "tgDataTable")) { + throw new Error( + "Please pass a unique 'formName' prop to the locally connected component with schema: ", + _schema + ); + } + if (orderByFirstColumn && !defaults?.order?.length) { + const r = [ + camelCase( + convertedSchema.fields[0].displayName || + convertedSchema.fields[0].path + ) + ]; + defaults.order = r; + } + } else { + //in user instantiated withTableParams() call + if (!formName || formName === "tgDataTable") { + throw new Error( + "Please pass a unique 'formName' prop to the withTableParams() with schema: ", + _schema + ); + } + } + + const [showForcedHiddenColumns, setShowForcedHidden] = useState(() => { + if (showEmptyColumnsByDefault) { + return true; + } + return false; + }); + + const [tableConfig, setTableConfig] = useState({ fieldOptions: [] }); + + useEffect(() => { + let newTableConfig = {}; + if (withDisplayOptions) { + if (syncDisplayOptionsToDb) { + newTableConfig = tableConfigurations && tableConfigurations[0]; + } else { + newTableConfig = getTableConfigFromStorage(formName); + } + if (!newTableConfig) { + newTableConfig = { + fieldOptions: [] + }; + } + } + setTableConfig(newTableConfig); + }, [ + formName, + syncDisplayOptionsToDb, + tableConfigurations, + withDisplayOptions + ]); + + // make user set page size persist + const userSetPageSize = + tableConfig?.userSetPageSize && parseInt(tableConfig.userSetPageSize, 10); + if (!syncDisplayOptionsToDb && userSetPageSize) { + defaults.pageSize = userSetPageSize; + } + + const { + reduxFormSearchInput = "", + onlyShowRowsWErrors, + reduxFormCellValidation, + reduxFormEntities, + reduxFormSelectedCells = {}, + reduxFormSelectedEntityIdMap = {}, + reduxFormQueryParams = {} + } = useSelector(state => { + if (!state.form[formName]) return {}; + return state.form[formName].values || {}; + }, useSelectorOptions); + + const entities = reduxFormEntities || _origEntities; + + const { schema } = useMemo(() => { + const schema = convertSchema(_schema); + if (isViewable) { + schema.fields = [viewColumn, ...schema.fields]; + } + if (isOpenable) { + schema.fields = [openColumn, ...schema.fields]; + } + // this must come before handling orderings. + schema.fields = schema.fields.map(field => { + if (field.placementPath) { + return { + ...field, + sortDisabled: + field.sortDisabled || + (typeof field.path === "string" && field.path.includes(".")), + path: field.placementPath + }; + } else { + return field; + } + }); + + if (withDisplayOptions) { + const fieldOptsByPath = keyBy(tableConfig.fieldOptions, "path"); + schema.fields = schema.fields.map(field => { + const fieldOpt = fieldOptsByPath[field.path]; + let noValsForField = false; + // only add this hidden column ability if no paging + if ( + !showForcedHiddenColumns && + withDisplayOptions && + (isSimple || isInfinite) + ) { + noValsForField = entities.every(e => { + const val = get(e, field.path); + return field.render + ? !field.render(val, e) + : cellRenderer[field.path] + ? !cellRenderer[field.path](val, e) + : !val; + }); + } + if (noValsForField) { + return { + ...field, + isHidden: true, + isForcedHidden: true + }; + } else if (fieldOpt) { + return { + ...field, + isHidden: fieldOpt.isHidden + }; + } else { + return field; + } + }); + + const columnOrderings = tableConfig.columnOrderings; + if (columnOrderings) { + const fieldsWithOrders = []; + const fieldsWithoutOrder = []; + // if a new field has been added since the orderings were set then we want + // it to be at the end instead of the beginning + schema.fields.forEach(field => { + if (columnOrderings.indexOf(field.path) > -1) { + fieldsWithOrders.push(field); + } else { + fieldsWithoutOrder.push(field); + } + }); + schema.fields = fieldsWithOrders + .sort(({ path: path1 }, { path: path2 }) => { + return ( + columnOrderings.indexOf(path1) - columnOrderings.indexOf(path2) + ); + }) + .concat(fieldsWithoutOrder); + setTableConfig(prev => ({ + ...prev, + columnOrderings: schema.fields.map(f => f.path) + })); + } + } + return { schema }; + }, [ + _schema, + cellRenderer, + entities, + isInfinite, + isOpenable, + isSimple, + isViewable, + showForcedHiddenColumns, + tableConfig, + withDisplayOptions + ]); + + const selectedEntities = withSelectedEntities + ? getRecordsFromIdMap(reduxFormSelectedEntityIdMap) + : undefined; + + const currentParams = urlConnected + ? getCurrentParamsFromUrl(history.location) //important to use history location and not ownProps.location because for some reason the location path lags one render behind!! + : reduxFormQueryParams; + + props = { + ...props, + ...getQueryParams({ + doNotCoercePageSize, + currentParams, + entities: props.entities, // for local table + urlConnected, + defaults: props.defaults, + schema: convertedSchema, + isInfinite, + isLocalCall, + additionalFilter: additionalFilterToUse, + additionalOrFilter: additionalOrFilterToUse, + noOrderError: props.noOrderError, + isCodeModel: props.isCodeModel, + ownProps: props + }) + }; + + const dispatch = useDispatch(); + let tableParams; + if (!isTableParamsConnected) { + const updateSearch = val => { + setTimeout(() => { + dispatch(change(formName, "reduxFormSearchInput", val || "")); + }); + }; + + let setNewParams; + if (urlConnected) { + setNewParams = newParams => { + setCurrentParamsOnUrl(newParams, history.replace); + dispatch(change(formName, "reduxFormQueryParams", newParams)); //we always will update the redux params as a workaround for withRouter not always working if inside a redux-connected container https://github.com/ReactTraining/react-router/issues/5037 + }; + } else { + setNewParams = function (newParams) { + dispatch(change(formName, "reduxFormQueryParams", newParams)); + }; + } + + const bindThese = makeDataTableHandlers({ + setNewParams, + updateSearch, + defaults, + onlyOneFilter + }); + + const boundDispatchProps = {}; + //bind currentParams to actions + Object.keys(bindThese).forEach(function (key) { + const action = bindThese[key]; + boundDispatchProps[key] = function (...args) { + action(...args, currentParams); + }; + }); + + const changeFormValue = (...args) => dispatch(change(formName, ...args)); + + tableParams = { + changeFormValue, + selectedEntities, + ..._tableParams, + ...props, + ...boundDispatchProps, + form: formName, //this will override the default redux form name + isTableParamsConnected: true //let the table know not to do local sorting/filtering etc. + }; + } + + const formTracker = useContext(TableFormTrackerContext); + useEffect(() => { + if (formTracker.isActive && !formTracker.formNames.includes(formName)) { + formTracker.pushFormName(formName); + } + }, [formTracker, formName]); + + return { + ...props, + selectedEntities, + tableParams, + currentParams, + schema, + entities, + reduxFormSearchInput, + onlyShowRowsWErrors, + reduxFormCellValidation, + reduxFormEntities, + reduxFormSelectedCells, + reduxFormSelectedEntityIdMap, + reduxFormQueryParams, + showForcedHiddenColumns, + setShowForcedHidden + }; +} diff --git a/packages/ui/src/DataTable/utils/withTableParams.js b/packages/ui/src/DataTable/utils/withTableParams.js index 7d25fede..1e00aece 100644 --- a/packages/ui/src/DataTable/utils/withTableParams.js +++ b/packages/ui/src/DataTable/utils/withTableParams.js @@ -1,4 +1,3 @@ -import React, { useContext, useEffect } from "react"; import { change, formValueSelector } from "redux-form"; import { connect } from "react-redux"; import { camelCase, isFunction, set } from "lodash-es"; @@ -6,7 +5,6 @@ import { withRouter } from "react-router-dom"; import { branch, compose } from "recompose"; import pureNoFunc from "../../utils/pureNoFunc"; -import TableFormTrackerContext from "../TableFormTrackerContext"; import convertSchema from "./convertSchema"; import { getRecordsFromReduxForm } from "./withSelectedEntities"; import { @@ -185,10 +183,6 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { ownProps: mergedOpts }), formNameFromWithTPCall: formNameFromWithTableParamsCall, - randomVarToForceLocalStorageUpdate: formSelector( - state, - "localStorageForceUpdate" - ), currentParams, selectedEntities, ...(withSelectedEntities && @@ -280,20 +274,6 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { return allMergedProps; } - function addFormTracking(Component) { - return props => { - const formTracker = useContext(TableFormTrackerContext); - const { formName } = props; - useEffect(() => { - if (formTracker.isActive && !formTracker.formNames.includes(formName)) { - formTracker.pushFormName(formName); - } - }, [formTracker, formName]); - - return ; - }; - } - const toReturn = compose( connect((state, ownProps) => { if (ownProps.isTableParamsConnected) { @@ -305,13 +285,10 @@ export default function withTableParams(compOrOpts, pTopLevelOpts) { formValueSelector(formName)(state, "reduxFormQueryParams") || {} //tnr: we need this to trigger withRouter and force it to update if it is nested in a redux-connected container.. very ugly but necessary }; }), - branch(props => { - //don't use withRouter if noRouter is passed! - return !props.noRouter; - }, withRouter), + //don't use withRouter if noRouter is passed! + branch(props => !props.noRouter, withRouter), connect(mapStateToProps, mapDispatchToProps, mergeProps), - pureNoFunc, - addFormTracking + pureNoFunc ); if (Component) { return toReturn(Component); diff --git a/packages/ui/src/InfoHelper/index.js b/packages/ui/src/InfoHelper/index.js index 11342212..8e72c1bd 100644 --- a/packages/ui/src/InfoHelper/index.js +++ b/packages/ui/src/InfoHelper/index.js @@ -43,12 +43,12 @@ export default ({ }; if (displayToSide) { toReturn = ( - + <> {IconInner} {content || children} - + ); } else if (isPopover) { toReturn = ; diff --git a/packages/ui/src/UploadCsvWizard.js b/packages/ui/src/UploadCsvWizard.js index bf9a7918..d91e723b 100644 --- a/packages/ui/src/UploadCsvWizard.js +++ b/packages/ui/src/UploadCsvWizard.js @@ -334,7 +334,7 @@ const UploadCsvWizardDialogInner = reduxForm()( { datatableFormName, validateAgainstSchema, userSchema = exampleData, - initialEntities + entities } = props; const rerenderKey = useRef(0); rerenderKey.current = rerenderKey.current + 1; @@ -539,8 +539,7 @@ export const PreviewCsvData = observer(props => { isSimple keepDirtyOnReinitialize isCellEditable - initialEntities={(initialEntities ? initialEntities : data) || []} - entities={(initialEntities ? initialEntities : data) || []} + entities={(entities ? entities : data) || []} schema={validateAgainstSchema} /> @@ -572,7 +571,7 @@ export const SimpleInsertDataDialog = compose( headerMessage, handleSubmit, userSchema, - initialEntities + entities }) { const dispatch = useDispatch(); const { entsToUse, validationToUse } = removeCleanRows( @@ -602,7 +601,7 @@ export const SimpleInsertDataDialog = compose( headerMessage={headerMessage} validateAgainstSchema={validateAgainstSchema} userSchema={userSchema} - initialEntities={initialEntities} + entities={entities} datatableFormName={"simpleInsertEditableTable"} /> diff --git a/packages/ui/src/utils/hotkeyUtils.js b/packages/ui/src/utils/hotkeyUtils.js index d725f1c5..cbd02108 100644 --- a/packages/ui/src/utils/hotkeyUtils.js +++ b/packages/ui/src/utils/hotkeyUtils.js @@ -96,7 +96,7 @@ export const withHotkeys = (hotkeys, handlers) => { React.cloneElement(children, newProps) ) : ( //if not, then we'll return a div that can be used -
+
); }; }; diff --git a/packages/ui/vite.config.ts.timestamp-1720444595414-4387a609f20aa.mjs b/packages/ui/vite.config.ts.timestamp-1720444595414-4387a609f20aa.mjs new file mode 100644 index 00000000..cc8ed117 --- /dev/null +++ b/packages/ui/vite.config.ts.timestamp-1720444595414-4387a609f20aa.mjs @@ -0,0 +1,196 @@ +// ../../vite.react.config.ts +import { defineConfig } from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/vite/dist/node/index.js"; +import fs from "node:fs"; +import react from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import viteTsConfigPaths from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import * as esbuild from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/esbuild/lib/main.js"; +import libCss from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/vite-plugin-libcss/index.js"; +import { camelCase } from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/lodash-es/lodash.js"; + +// ../../getPort.ts +import * as crypto from "crypto"; +function getPort(inputString) { + const hash = crypto.createHash("sha256"); + hash.update(inputString); + let hashValue = parseInt(hash.digest("hex").substring(0, 15), 16); + hashValue = Math.abs(hashValue); + return 2e3 + hashValue % (9e3 - 2e3 + 1); +} + +// ../../vite.react.config.ts +import { viteStaticCopy } from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/vite-plugin-static-copy/dist/index.js"; +import path from "node:path"; +import dts from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/vite-plugin-dts/dist/index.mjs"; +import { joinPathFragments } from "file:///Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/node_modules/nx/src/devkit-exports.js"; +var __vite_injected_original_dirname = "/Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss"; +var justSrc = [ + /\/src\/.*\.js$/, + /\/src\/.*\.jsx$/, + /\/src\/.*\.ts$/, + /\/src\/.*\.tsx$/ +]; +var sourceJSPattern = [ + ...justSrc, + /\/demo\/.*\.js$/, + /\/helperUtils\/.*\.js$/ +]; +var rollupPlugin = (matchers) => ({ + name: "js-in-jsx", + load(id) { + if (matchers.some((matcher) => matcher.test(id))) { + const file = fs.readFileSync(id, { encoding: "utf-8" }); + return esbuild.transformSync(file, { loader: "jsx" }).code; + } + return void 0; + } +}); +var vite_react_config_default = ({ name, dir }) => defineConfig(({ command, mode = "production" }) => { + const isDemo = mode === "demo"; + const isUmd = mode === "umd"; + if (!isDemo) { + mode = "production"; + } + const port = getPort(name); + return { + ...command === "build" ? { + define: { + "process.env.NODE_ENV": JSON.stringify("production") + } + } : {}, + cacheDir: `../../node_modules/.vite/${name}`, + server: { + port + }, + base: "./", + plugins: [ + //tnw - maybe add this back later after adding performance metrics https://twitter.com/aidenybai/status/1689773623827943424 + // million.vite({ + // auto: true + // }), + dts({ + entryRoot: "src", + tsconfigPath: joinPathFragments(dir, "tsconfig.json") + // skipDiagnostics: true, + }), + react(), + libCss(), + viteTsConfigPaths({ + root: "../../" + }), + ...command === "build" && !isDemo ? [ + // noBundlePlugin({ copy: "**/*.css" }), + viteStaticCopy({ + targets: [ + { + src: "./src", + dest: "." + }, + { + src: "./README.md", + dest: "." + } + ] + }) + ] : [] + ], + esbuild: { + loader: "jsx", + include: sourceJSPattern, + exclude: [], + keepNames: true, + minifyIdentifiers: false, + minifySyntax: false + }, + optimizeDeps: { + esbuildOptions: { + loader: { + ".js": "jsx", + ".ts": "tsx" + } + } + }, + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../', + // }), + // ], + // }, + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + minify: false, + target: "es2015", + outDir: `../../${isDemo ? "demo-dist" : "dist"}/${name}`, + ...isDemo ? {} : { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: "src/index.js", + name, + fileName: (format) => `index.${format}.js`, + formats: isUmd ? ["umd"] : ["es", "cjs"] + // Change this to the formats you want to support. + // Don't forgot to update your package.json as well. + } + }, + rollupOptions: { + plugins: [rollupPlugin(justSrc)], + output: { + name: camelCase(name) + }, + // External packages that should not be bundled into your library. + external: mode === "demo" || isUmd ? [] : [ + "react", + "react-dom", + "react/jsx-runtime", + "redux", + "react-redux", + "redux-form", + "@blueprintjs/core", + "@blueprintjs/select", + "@blueprintjs/datetime" + ] + } + }, + resolve: { + alias: { + react: path.join(__vite_injected_original_dirname, "node_modules/react"), + "@blueprintjs/core": path.join( + __vite_injected_original_dirname, + "node_modules/@blueprintjs/core" + ), + "@blueprintjs/datetime": path.join( + __vite_injected_original_dirname, + "node_modules/@blueprintjs/datetime" + ), + // "@teselagen/react-table/react-table.css": `/Users/tnrich/Sites/react-table/react-table.css`, + // "@teselagen/react-table": `/Users/tnrich/Sites/react-table/src`, + "react-dom": path.join(__vite_injected_original_dirname, "node_modules/react-dom"), + "react-redux": path.join(__vite_injected_original_dirname, "node_modules/react-redux"), + "redux-form": path.join(__vite_injected_original_dirname, "node_modules/redux-form"), + redux: path.join(__vite_injected_original_dirname, "node_modules/redux") + } + }, + test: { + globals: true, + cache: { + dir: "../../node_modules/.vitest" + }, + setupFiles: ["../../vitest.setup.ts"], + environment: "jsdom", + include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"] + } + }; +}); + +// vite.config.ts +var __vite_injected_original_dirname2 = "/Users/guillermoespinosa/Documents/Teselagen/repos/tg-oss/packages/ui"; +var vite_config_default = vite_react_config_default({ + name: "ui", + dir: __vite_injected_original_dirname2 +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vdml0ZS5yZWFjdC5jb25maWcudHMiLCAiLi4vLi4vZ2V0UG9ydC50cyIsICJ2aXRlLmNvbmZpZy50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9ndWlsbGVybW9lc3Bpbm9zYS9Eb2N1bWVudHMvVGVzZWxhZ2VuL3JlcG9zL3RnLW9zc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL2d1aWxsZXJtb2VzcGlub3NhL0RvY3VtZW50cy9UZXNlbGFnZW4vcmVwb3MvdGctb3NzL3ZpdGUucmVhY3QuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ndWlsbGVybW9lc3Bpbm9zYS9Eb2N1bWVudHMvVGVzZWxhZ2VuL3JlcG9zL3RnLW9zcy92aXRlLnJlYWN0LmNvbmZpZy50c1wiOy8vLyA8cmVmZXJlbmNlIHR5cGVzPVwidml0ZXN0XCIgLz5cbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCI7XG5pbXBvcnQgZnMgZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCByZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIjtcbmltcG9ydCB2aXRlVHNDb25maWdQYXRocyBmcm9tIFwidml0ZS10c2NvbmZpZy1wYXRoc1wiO1xuaW1wb3J0ICogYXMgZXNidWlsZCBmcm9tIFwiZXNidWlsZFwiO1xuaW1wb3J0IGxpYkNzcyBmcm9tIFwidml0ZS1wbHVnaW4tbGliY3NzXCI7XG5pbXBvcnQgeyBjYW1lbENhc2UgfSBmcm9tIFwibG9kYXNoLWVzXCI7XG5pbXBvcnQgeyBnZXRQb3J0IH0gZnJvbSBcIi4vZ2V0UG9ydFwiO1xuaW1wb3J0IHsgdml0ZVN0YXRpY0NvcHkgfSBmcm9tIFwidml0ZS1wbHVnaW4tc3RhdGljLWNvcHlcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJub2RlOnBhdGhcIjtcbmltcG9ydCBkdHMgZnJvbSBcInZpdGUtcGx1Z2luLWR0c1wiO1xuaW1wb3J0IHsgam9pblBhdGhGcmFnbWVudHMgfSBmcm9tIFwibngvc3JjL2RldmtpdC1leHBvcnRzXCI7XG5cbi8vIGltcG9ydCBtaWxsaW9uIGZyb20gXCJtaWxsaW9uL2NvbXBpbGVyXCI7XG4vL3ZpdGUgY29uZmlnIGZvciByZWFjdCBwYWNrYWdlc1xuXG5jb25zdCBqdXN0U3JjID0gW1xuICAvXFwvc3JjXFwvLipcXC5qcyQvLFxuICAvXFwvc3JjXFwvLipcXC5qc3gkLyxcbiAgL1xcL3NyY1xcLy4qXFwudHMkLyxcbiAgL1xcL3NyY1xcLy4qXFwudHN4JC9cbl07XG5jb25zdCBzb3VyY2VKU1BhdHRlcm4gPSBbXG4gIC4uLmp1c3RTcmMsXG4gIC9cXC9kZW1vXFwvLipcXC5qcyQvLFxuICAvXFwvaGVscGVyVXRpbHNcXC8uKlxcLmpzJC9cbl07XG5jb25zdCByb2xsdXBQbHVnaW4gPSAobWF0Y2hlcnM6IFJlZ0V4cFtdKSA9PiAoe1xuICBuYW1lOiBcImpzLWluLWpzeFwiLFxuICBsb2FkKGlkOiBzdHJpbmcpIHtcbiAgICBpZiAobWF0Y2hlcnMuc29tZShtYXRjaGVyID0+IG1hdGNoZXIudGVzdChpZCkpKSB7XG4gICAgICBjb25zdCBmaWxlID0gZnMucmVhZEZpbGVTeW5jKGlkLCB7IGVuY29kaW5nOiBcInV0Zi04XCIgfSk7XG4gICAgICByZXR1cm4gZXNidWlsZC50cmFuc2Zvcm1TeW5jKGZpbGUsIHsgbG9hZGVyOiBcImpzeFwiIH0pLmNvZGU7XG4gICAgfVxuICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbn0pO1xuXG5leHBvcnQgZGVmYXVsdCAoeyBuYW1lLCBkaXIgfTogeyBuYW1lOiBzdHJpbmc7IGRpcjogc3RyaW5nIH0pID0+XG4gIGRlZmluZUNvbmZpZygoeyBjb21tYW5kLCBtb2RlID0gXCJwcm9kdWN0aW9uXCIgfSkgPT4ge1xuICAgIGNvbnN0IGlzRGVtbyA9IG1vZGUgPT09IFwiZGVtb1wiO1xuICAgIGNvbnN0IGlzVW1kID0gbW9kZSA9PT0gXCJ1bWRcIjtcbiAgICBpZiAoIWlzRGVtbykge1xuICAgICAgbW9kZSA9IFwicHJvZHVjdGlvblwiO1xuICAgIH1cbiAgICBjb25zdCBwb3J0ID0gZ2V0UG9ydChuYW1lKTtcbiAgICByZXR1cm4ge1xuICAgICAgLi4uKGNvbW1hbmQgPT09IFwiYnVpbGRcIlxuICAgICAgICA/IHtcbiAgICAgICAgICAgIGRlZmluZToge1xuICAgICAgICAgICAgICBcInByb2Nlc3MuZW52Lk5PREVfRU5WXCI6IEpTT04uc3RyaW5naWZ5KFwicHJvZHVjdGlvblwiKVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgOiB7fSksXG4gICAgICBjYWNoZURpcjogYC4uLy4uL25vZGVfbW9kdWxlcy8udml0ZS8ke25hbWV9YCxcbiAgICAgIHNlcnZlcjoge1xuICAgICAgICBwb3J0XG4gICAgICB9LFxuICAgICAgYmFzZTogXCIuL1wiLFxuICAgICAgcGx1Z2luczogW1xuICAgICAgICAvL3RudyAtIG1heWJlIGFkZCB0aGlzIGJhY2sgbGF0ZXIgYWZ0ZXIgYWRkaW5nIHBlcmZvcm1hbmNlIG1ldHJpY3MgaHR0cHM6Ly90d2l0dGVyLmNvbS9haWRlbnliYWkvc3RhdHVzLzE2ODk3NzM2MjM4Mjc5NDM0MjRcbiAgICAgICAgLy8gbWlsbGlvbi52aXRlKHtcbiAgICAgICAgLy8gICBhdXRvOiB0cnVlXG4gICAgICAgIC8vIH0pLFxuICAgICAgICBkdHMoe1xuICAgICAgICAgIGVudHJ5Um9vdDogXCJzcmNcIixcbiAgICAgICAgICB0c2NvbmZpZ1BhdGg6IGpvaW5QYXRoRnJhZ21lbnRzKGRpciwgXCJ0c2NvbmZpZy5qc29uXCIpXG4gICAgICAgICAgLy8gc2tpcERpYWdub3N0aWNzOiB0cnVlLFxuICAgICAgICB9KSxcbiAgICAgICAgcmVhY3QoKSxcbiAgICAgICAgbGliQ3NzKCksXG4gICAgICAgIHZpdGVUc0NvbmZpZ1BhdGhzKHtcbiAgICAgICAgICByb290OiBcIi4uLy4uL1wiXG4gICAgICAgIH0pLFxuICAgICAgICAuLi4oY29tbWFuZCA9PT0gXCJidWlsZFwiICYmICFpc0RlbW9cbiAgICAgICAgICA/IFtcbiAgICAgICAgICAgICAgLy8gbm9CdW5kbGVQbHVnaW4oeyBjb3B5OiBcIioqLyouY3NzXCIgfSksXG4gICAgICAgICAgICAgIHZpdGVTdGF0aWNDb3B5KHtcbiAgICAgICAgICAgICAgICB0YXJnZXRzOiBbXG4gICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgIHNyYzogXCIuL3NyY1wiLFxuICAgICAgICAgICAgICAgICAgICBkZXN0OiBcIi5cIlxuICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgICAgc3JjOiBcIi4vUkVBRE1FLm1kXCIsXG4gICAgICAgICAgICAgICAgICAgIGRlc3Q6IFwiLlwiXG4gICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgXVxuICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgXVxuICAgICAgICAgIDogW10pXG4gICAgICBdLFxuICAgICAgZXNidWlsZDoge1xuICAgICAgICBsb2FkZXI6IFwianN4XCIsXG4gICAgICAgIGluY2x1ZGU6IHNvdXJjZUpTUGF0dGVybixcbiAgICAgICAgZXhjbHVkZTogW10sXG4gICAgICAgIGtlZXBOYW1lczogdHJ1ZSxcbiAgICAgICAgbWluaWZ5SWRlbnRpZmllcnM6IGZhbHNlLFxuICAgICAgICBtaW5pZnlTeW50YXg6IGZhbHNlXG4gICAgICB9LFxuICAgICAgb3B0aW1pemVEZXBzOiB7XG4gICAgICAgIGVzYnVpbGRPcHRpb25zOiB7XG4gICAgICAgICAgbG9hZGVyOiB7XG4gICAgICAgICAgICBcIi5qc1wiOiBcImpzeFwiLFxuICAgICAgICAgICAgXCIudHNcIjogXCJ0c3hcIlxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfSxcblxuICAgICAgLy8gVW5jb21tZW50IHRoaXMgaWYgeW91IGFyZSB1c2luZyB3b3JrZXJzLlxuICAgICAgLy8gd29ya2VyOiB7XG4gICAgICAvLyAgcGx1Z2luczogW1xuICAgICAgLy8gICAgdml0ZVRzQ29uZmlnUGF0aHMoe1xuICAgICAgLy8gICAgICByb290OiAnLi4vLi4vJyxcbiAgICAgIC8vICAgIH0pLFxuICAgICAgLy8gIF0sXG4gICAgICAvLyB9LFxuICAgICAgLy8gQ29uZmlndXJhdGlvbiBmb3IgYnVpbGRpbmcgeW91ciBsaWJyYXJ5LlxuICAgICAgLy8gU2VlOiBodHRwczovL3ZpdGVqcy5kZXYvZ3VpZGUvYnVpbGQuaHRtbCNsaWJyYXJ5LW1vZGVcbiAgICAgIGJ1aWxkOiB7XG4gICAgICAgIG1pbmlmeTogZmFsc2UsXG4gICAgICAgIHRhcmdldDogXCJlczIwMTVcIixcbiAgICAgICAgb3V0RGlyOiBgLi4vLi4vJHtpc0RlbW8gPyBcImRlbW8tZGlzdFwiIDogXCJkaXN0XCJ9LyR7bmFtZX1gLFxuICAgICAgICAuLi4oaXNEZW1vXG4gICAgICAgICAgPyB7fVxuICAgICAgICAgIDoge1xuICAgICAgICAgICAgICBsaWI6IHtcbiAgICAgICAgICAgICAgICAvLyBDb3VsZCBhbHNvIGJlIGEgZGljdGlvbmFyeSBvciBhcnJheSBvZiBtdWx0aXBsZSBlbnRyeSBwb2ludHMuXG4gICAgICAgICAgICAgICAgZW50cnk6IFwic3JjL2luZGV4LmpzXCIsXG4gICAgICAgICAgICAgICAgbmFtZSxcbiAgICAgICAgICAgICAgICBmaWxlTmFtZTogZm9ybWF0ID0+IGBpbmRleC4ke2Zvcm1hdH0uanNgLFxuICAgICAgICAgICAgICAgIGZvcm1hdHM6IGlzVW1kID8gW1widW1kXCJdIDogW1wiZXNcIiwgXCJjanNcIl1cbiAgICAgICAgICAgICAgICAvLyBDaGFuZ2UgdGhpcyB0byB0aGUgZm9ybWF0cyB5b3Ugd2FudCB0byBzdXBwb3J0LlxuICAgICAgICAgICAgICAgIC8vIERvbid0IGZvcmdvdCB0byB1cGRhdGUgeW91ciBwYWNrYWdlLmpzb24gYXMgd2VsbC5cbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfSksXG4gICAgICAgIHJvbGx1cE9wdGlvbnM6IHtcbiAgICAgICAgICBwbHVnaW5zOiBbcm9sbHVwUGx1Z2luKGp1c3RTcmMpXSxcbiAgICAgICAgICBvdXRwdXQ6IHtcbiAgICAgICAgICAgIG5hbWU6IGNhbWVsQ2FzZShuYW1lKVxuICAgICAgICAgIH0sXG4gICAgICAgICAgLy8gRXh0ZXJuYWwgcGFja2FnZXMgdGhhdCBzaG91bGQgbm90IGJlIGJ1bmRsZWQgaW50byB5b3VyIGxpYnJhcnkuXG4gICAgICAgICAgZXh0ZXJuYWw6XG4gICAgICAgICAgICBtb2RlID09PSBcImRlbW9cIiB8fCBpc1VtZFxuICAgICAgICAgICAgICA/IFtdXG4gICAgICAgICAgICAgIDogW1xuICAgICAgICAgICAgICAgICAgXCJyZWFjdFwiLFxuICAgICAgICAgICAgICAgICAgXCJyZWFjdC1kb21cIixcbiAgICAgICAgICAgICAgICAgIFwicmVhY3QvanN4LXJ1bnRpbWVcIixcbiAgICAgICAgICAgICAgICAgIFwicmVkdXhcIixcbiAgICAgICAgICAgICAgICAgIFwicmVhY3QtcmVkdXhcIixcbiAgICAgICAgICAgICAgICAgIFwicmVkdXgtZm9ybVwiLFxuICAgICAgICAgICAgICAgICAgXCJAYmx1ZXByaW50anMvY29yZVwiLFxuICAgICAgICAgICAgICAgICAgXCJAYmx1ZXByaW50anMvc2VsZWN0XCIsXG4gICAgICAgICAgICAgICAgICBcIkBibHVlcHJpbnRqcy9kYXRldGltZVwiXG4gICAgICAgICAgICAgICAgXVxuICAgICAgICB9XG4gICAgICB9LFxuICAgICAgcmVzb2x2ZToge1xuICAgICAgICBhbGlhczoge1xuICAgICAgICAgIHJlYWN0OiBwYXRoLmpvaW4oX19kaXJuYW1lLCBcIm5vZGVfbW9kdWxlcy9yZWFjdFwiKSxcbiAgICAgICAgICBcIkBibHVlcHJpbnRqcy9jb3JlXCI6IHBhdGguam9pbihcbiAgICAgICAgICAgIF9fZGlybmFtZSxcbiAgICAgICAgICAgIFwibm9kZV9tb2R1bGVzL0BibHVlcHJpbnRqcy9jb3JlXCJcbiAgICAgICAgICApLFxuICAgICAgICAgIFwiQGJsdWVwcmludGpzL2RhdGV0aW1lXCI6IHBhdGguam9pbihcbiAgICAgICAgICAgIF9fZGlybmFtZSxcbiAgICAgICAgICAgIFwibm9kZV9tb2R1bGVzL0BibHVlcHJpbnRqcy9kYXRldGltZVwiXG4gICAgICAgICAgKSxcblxuICAgICAgICAgIC8vIFwiQHRlc2VsYWdlbi9yZWFjdC10YWJsZS9yZWFjdC10YWJsZS5jc3NcIjogYC9Vc2Vycy90bnJpY2gvU2l0ZXMvcmVhY3QtdGFibGUvcmVhY3QtdGFibGUuY3NzYCxcbiAgICAgICAgICAvLyBcIkB0ZXNlbGFnZW4vcmVhY3QtdGFibGVcIjogYC9Vc2Vycy90bnJpY2gvU2l0ZXMvcmVhY3QtdGFibGUvc3JjYCxcbiAgICAgICAgICBcInJlYWN0LWRvbVwiOiBwYXRoLmpvaW4oX19kaXJuYW1lLCBcIm5vZGVfbW9kdWxlcy9yZWFjdC1kb21cIiksXG4gICAgICAgICAgXCJyZWFjdC1yZWR1eFwiOiBwYXRoLmpvaW4oX19kaXJuYW1lLCBcIm5vZGVfbW9kdWxlcy9yZWFjdC1yZWR1eFwiKSxcbiAgICAgICAgICBcInJlZHV4LWZvcm1cIjogcGF0aC5qb2luKF9fZGlybmFtZSwgXCJub2RlX21vZHVsZXMvcmVkdXgtZm9ybVwiKSxcbiAgICAgICAgICByZWR1eDogcGF0aC5qb2luKF9fZGlybmFtZSwgXCJub2RlX21vZHVsZXMvcmVkdXhcIilcbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIHRlc3Q6IHtcbiAgICAgICAgZ2xvYmFsczogdHJ1ZSxcbiAgICAgICAgY2FjaGU6IHtcbiAgICAgICAgICBkaXI6IFwiLi4vLi4vbm9kZV9tb2R1bGVzLy52aXRlc3RcIlxuICAgICAgICB9LFxuICAgICAgICBzZXR1cEZpbGVzOiBbXCIuLi8uLi92aXRlc3Quc2V0dXAudHNcIl0sXG4gICAgICAgIGVudmlyb25tZW50OiBcImpzZG9tXCIsXG4gICAgICAgIGluY2x1ZGU6IFtcInNyYy8qKi8qLnt0ZXN0LHNwZWN9LntqcyxtanMsY2pzLHRzLG10cyxjdHMsanN4LHRzeH1cIl1cbiAgICAgIH1cbiAgICB9O1xuICB9KTtcbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL2d1aWxsZXJtb2VzcGlub3NhL0RvY3VtZW50cy9UZXNlbGFnZW4vcmVwb3MvdGctb3NzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvZ3VpbGxlcm1vZXNwaW5vc2EvRG9jdW1lbnRzL1Rlc2VsYWdlbi9yZXBvcy90Zy1vc3MvZ2V0UG9ydC50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvZ3VpbGxlcm1vZXNwaW5vc2EvRG9jdW1lbnRzL1Rlc2VsYWdlbi9yZXBvcy90Zy1vc3MvZ2V0UG9ydC50c1wiO2ltcG9ydCAqIGFzIGNyeXB0byBmcm9tIFwiY3J5cHRvXCI7XG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRQb3J0KGlucHV0U3RyaW5nOiBzdHJpbmcpIHtcbiAgY29uc3QgaGFzaCA9IGNyeXB0by5jcmVhdGVIYXNoKFwic2hhMjU2XCIpO1xuICBoYXNoLnVwZGF0ZShpbnB1dFN0cmluZyk7XG4gIGxldCBoYXNoVmFsdWUgPSBwYXJzZUludChoYXNoLmRpZ2VzdChcImhleFwiKS5zdWJzdHJpbmcoMCwgMTUpLCAxNik7IC8vIEdldCBhIGxhcmdlIG51bWJlciBmcm9tIHRoZSBoYXNoLlxuICBoYXNoVmFsdWUgPSBNYXRoLmFicyhoYXNoVmFsdWUpOyAvLyBFbnN1cmUgaXQgaXMgbm9uLW5lZ2F0aXZlLlxuICByZXR1cm4gMjAwMCArIChoYXNoVmFsdWUgJSAoOTAwMCAtIDIwMDAgKyAxKSk7IC8vIFNjYWxlIGl0IHRvIGJlIGluIFsyMDAwLCA2NTUzNV0uXG59XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9Vc2Vycy9ndWlsbGVybW9lc3Bpbm9zYS9Eb2N1bWVudHMvVGVzZWxhZ2VuL3JlcG9zL3RnLW9zcy9wYWNrYWdlcy91aVwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL2d1aWxsZXJtb2VzcGlub3NhL0RvY3VtZW50cy9UZXNlbGFnZW4vcmVwb3MvdGctb3NzL3BhY2thZ2VzL3VpL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9ndWlsbGVybW9lc3Bpbm9zYS9Eb2N1bWVudHMvVGVzZWxhZ2VuL3JlcG9zL3RnLW9zcy9wYWNrYWdlcy91aS92aXRlLmNvbmZpZy50c1wiOy8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAbngvZW5mb3JjZS1tb2R1bGUtYm91bmRhcmllc1xuaW1wb3J0IHZpdGVSZWFjdENvbmZpZyBmcm9tIFwiLi4vLi4vdml0ZS5yZWFjdC5jb25maWdcIjtcblxuZXhwb3J0IGRlZmF1bHQgdml0ZVJlYWN0Q29uZmlnKHtcbiAgbmFtZTogXCJ1aVwiLFxuICBkaXI6IF9fZGlybmFtZVxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQ0EsU0FBUyxvQkFBb0I7QUFDN0IsT0FBTyxRQUFRO0FBQ2YsT0FBTyxXQUFXO0FBQ2xCLE9BQU8sdUJBQXVCO0FBQzlCLFlBQVksYUFBYTtBQUN6QixPQUFPLFlBQVk7QUFDbkIsU0FBUyxpQkFBaUI7OztBQ1AyVCxZQUFZLFlBQVk7QUFFdFcsU0FBUyxRQUFRLGFBQXFCO0FBQzNDLFFBQU0sT0FBYyxrQkFBVyxRQUFRO0FBQ3ZDLE9BQUssT0FBTyxXQUFXO0FBQ3ZCLE1BQUksWUFBWSxTQUFTLEtBQUssT0FBTyxLQUFLLEVBQUUsVUFBVSxHQUFHLEVBQUUsR0FBRyxFQUFFO0FBQ2hFLGNBQVksS0FBSyxJQUFJLFNBQVM7QUFDOUIsU0FBTyxNQUFRLGFBQWEsTUFBTyxNQUFPO0FBQzVDOzs7QURDQSxTQUFTLHNCQUFzQjtBQUMvQixPQUFPLFVBQVU7QUFDakIsT0FBTyxTQUFTO0FBQ2hCLFNBQVMseUJBQXlCO0FBWmxDLElBQU0sbUNBQW1DO0FBaUJ6QyxJQUFNLFVBQVU7QUFBQSxFQUNkO0FBQUEsRUFDQTtBQUFBLEVBQ0E7QUFBQSxFQUNBO0FBQ0Y7QUFDQSxJQUFNLGtCQUFrQjtBQUFBLEVBQ3RCLEdBQUc7QUFBQSxFQUNIO0FBQUEsRUFDQTtBQUNGO0FBQ0EsSUFBTSxlQUFlLENBQUMsY0FBd0I7QUFBQSxFQUM1QyxNQUFNO0FBQUEsRUFDTixLQUFLLElBQVk7QUFDZixRQUFJLFNBQVMsS0FBSyxhQUFXLFFBQVEsS0FBSyxFQUFFLENBQUMsR0FBRztBQUM5QyxZQUFNLE9BQU8sR0FBRyxhQUFhLElBQUksRUFBRSxVQUFVLFFBQVEsQ0FBQztBQUN0RCxhQUFlLHNCQUFjLE1BQU0sRUFBRSxRQUFRLE1BQU0sQ0FBQyxFQUFFO0FBQUEsSUFDeEQ7QUFDQSxXQUFPO0FBQUEsRUFDVDtBQUNGO0FBRUEsSUFBTyw0QkFBUSxDQUFDLEVBQUUsTUFBTSxJQUFJLE1BQzFCLGFBQWEsQ0FBQyxFQUFFLFNBQVMsT0FBTyxhQUFhLE1BQU07QUFDakQsUUFBTSxTQUFTLFNBQVM7QUFDeEIsUUFBTSxRQUFRLFNBQVM7QUFDdkIsTUFBSSxDQUFDLFFBQVE7QUFDWCxXQUFPO0FBQUEsRUFDVDtBQUNBLFFBQU0sT0FBTyxRQUFRLElBQUk7QUFDekIsU0FBTztBQUFBLElBQ0wsR0FBSSxZQUFZLFVBQ1o7QUFBQSxNQUNFLFFBQVE7QUFBQSxRQUNOLHdCQUF3QixLQUFLLFVBQVUsWUFBWTtBQUFBLE1BQ3JEO0FBQUEsSUFDRixJQUNBLENBQUM7QUFBQSxJQUNMLFVBQVUsNEJBQTRCLElBQUk7QUFBQSxJQUMxQyxRQUFRO0FBQUEsTUFDTjtBQUFBLElBQ0Y7QUFBQSxJQUNBLE1BQU07QUFBQSxJQUNOLFNBQVM7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBLE1BS1AsSUFBSTtBQUFBLFFBQ0YsV0FBVztBQUFBLFFBQ1gsY0FBYyxrQkFBa0IsS0FBSyxlQUFlO0FBQUE7QUFBQSxNQUV0RCxDQUFDO0FBQUEsTUFDRCxNQUFNO0FBQUEsTUFDTixPQUFPO0FBQUEsTUFDUCxrQkFBa0I7QUFBQSxRQUNoQixNQUFNO0FBQUEsTUFDUixDQUFDO0FBQUEsTUFDRCxHQUFJLFlBQVksV0FBVyxDQUFDLFNBQ3hCO0FBQUE7QUFBQSxRQUVFLGVBQWU7QUFBQSxVQUNiLFNBQVM7QUFBQSxZQUNQO0FBQUEsY0FDRSxLQUFLO0FBQUEsY0FDTCxNQUFNO0FBQUEsWUFDUjtBQUFBLFlBQ0E7QUFBQSxjQUNFLEtBQUs7QUFBQSxjQUNMLE1BQU07QUFBQSxZQUNSO0FBQUEsVUFDRjtBQUFBLFFBQ0YsQ0FBQztBQUFBLE1BQ0gsSUFDQSxDQUFDO0FBQUEsSUFDUDtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ1AsUUFBUTtBQUFBLE1BQ1IsU0FBUztBQUFBLE1BQ1QsU0FBUyxDQUFDO0FBQUEsTUFDVixXQUFXO0FBQUEsTUFDWCxtQkFBbUI7QUFBQSxNQUNuQixjQUFjO0FBQUEsSUFDaEI7QUFBQSxJQUNBLGNBQWM7QUFBQSxNQUNaLGdCQUFnQjtBQUFBLFFBQ2QsUUFBUTtBQUFBLFVBQ04sT0FBTztBQUFBLFVBQ1AsT0FBTztBQUFBLFFBQ1Q7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUEsSUFZQSxPQUFPO0FBQUEsTUFDTCxRQUFRO0FBQUEsTUFDUixRQUFRO0FBQUEsTUFDUixRQUFRLFNBQVMsU0FBUyxjQUFjLE1BQU0sSUFBSSxJQUFJO0FBQUEsTUFDdEQsR0FBSSxTQUNBLENBQUMsSUFDRDtBQUFBLFFBQ0UsS0FBSztBQUFBO0FBQUEsVUFFSCxPQUFPO0FBQUEsVUFDUDtBQUFBLFVBQ0EsVUFBVSxZQUFVLFNBQVMsTUFBTTtBQUFBLFVBQ25DLFNBQVMsUUFBUSxDQUFDLEtBQUssSUFBSSxDQUFDLE1BQU0sS0FBSztBQUFBO0FBQUE7QUFBQSxRQUd6QztBQUFBLE1BQ0Y7QUFBQSxNQUNKLGVBQWU7QUFBQSxRQUNiLFNBQVMsQ0FBQyxhQUFhLE9BQU8sQ0FBQztBQUFBLFFBQy9CLFFBQVE7QUFBQSxVQUNOLE1BQU0sVUFBVSxJQUFJO0FBQUEsUUFDdEI7QUFBQTtBQUFBLFFBRUEsVUFDRSxTQUFTLFVBQVUsUUFDZixDQUFDLElBQ0Q7QUFBQSxVQUNFO0FBQUEsVUFDQTtBQUFBLFVBQ0E7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFVBQ0E7QUFBQSxVQUNBO0FBQUEsVUFDQTtBQUFBLFVBQ0E7QUFBQSxRQUNGO0FBQUEsTUFDUjtBQUFBLElBQ0Y7QUFBQSxJQUNBLFNBQVM7QUFBQSxNQUNQLE9BQU87QUFBQSxRQUNMLE9BQU8sS0FBSyxLQUFLLGtDQUFXLG9CQUFvQjtBQUFBLFFBQ2hELHFCQUFxQixLQUFLO0FBQUEsVUFDeEI7QUFBQSxVQUNBO0FBQUEsUUFDRjtBQUFBLFFBQ0EseUJBQXlCLEtBQUs7QUFBQSxVQUM1QjtBQUFBLFVBQ0E7QUFBQSxRQUNGO0FBQUE7QUFBQTtBQUFBLFFBSUEsYUFBYSxLQUFLLEtBQUssa0NBQVcsd0JBQXdCO0FBQUEsUUFDMUQsZUFBZSxLQUFLLEtBQUssa0NBQVcsMEJBQTBCO0FBQUEsUUFDOUQsY0FBYyxLQUFLLEtBQUssa0NBQVcseUJBQXlCO0FBQUEsUUFDNUQsT0FBTyxLQUFLLEtBQUssa0NBQVcsb0JBQW9CO0FBQUEsTUFDbEQ7QUFBQSxJQUNGO0FBQUEsSUFDQSxNQUFNO0FBQUEsTUFDSixTQUFTO0FBQUEsTUFDVCxPQUFPO0FBQUEsUUFDTCxLQUFLO0FBQUEsTUFDUDtBQUFBLE1BQ0EsWUFBWSxDQUFDLHVCQUF1QjtBQUFBLE1BQ3BDLGFBQWE7QUFBQSxNQUNiLFNBQVMsQ0FBQyxzREFBc0Q7QUFBQSxJQUNsRTtBQUFBLEVBQ0Y7QUFDRixDQUFDOzs7QUU3TEgsSUFBTUEsb0NBQW1DO0FBR3pDLElBQU8sc0JBQVEsMEJBQWdCO0FBQUEsRUFDN0IsTUFBTTtBQUFBLEVBQ04sS0FBS0M7QUFDUCxDQUFDOyIsCiAgIm5hbWVzIjogWyJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSIsICJfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSJdCn0K