From f98649b56631a2867c5ad9e34143aebb96b40aae Mon Sep 17 00:00:00 2001 From: heswell Date: Mon, 20 Nov 2023 22:18:19 +0000 Subject: [PATCH] row based keyboard nav in tables, showcase basket test data (#972) * add data update to test tables, remove redundant generators * table supports row highlightingh * fix scroll on instrument search * controlled row highlighting for table * handle focus in NewBasket prompt * keyboard navigation row mode tables * improve UX arouns basket selector * type fixes * format notional values * drag drop across components * WIP * use columnMap in BackGround cell rensderer * remove console.log --- vuu-ui/packages/vuu-data-test/src/Table.ts | 27 +- .../src/TickingArrayDataSource.ts | 14 + .../vuu-data-test/src/basket/basket-module.ts | 131 +- .../data-generators/basket-generator.ts | 10 - .../basketConstituent-generator.ts | 13 - .../basketTrading-generator.ts | 13 - .../basketTradingConstituent-generator.ts | 13 - .../src/basket/data-generators/index.ts | 18 - .../basket/reference-data/basketTrading.ts | 45 - .../basketTradingConstituent.ts | 16 - .../src/basket/reference-data/index.ts | 8 - vuu-ui/packages/vuu-data-test/src/index.ts | 1 + .../data-generators/child-order-generator.ts | 91 - .../data-generators/generate-data-utils.ts | 60 - .../simul/data-generators/generatedData.ts | 12 - .../src/simul/data-generators/index.ts | 6 - .../data-generators/instrument-generator.ts | 62 - .../instrument-prices-generator.ts | 62 - .../simul/data-generators/order-generator.ts | 92 - .../data-generators/parent-order-generator.ts | 83 - .../simul/data-generators/prices-generator.ts | 36 - .../vuu-data-test/src/vuu-row-generator.ts | 2 - .../array-data-source/array-data-source.ts | 22 +- vuu-ui/packages/vuu-data/src/data-source.ts | 12 +- .../packages/vuu-data/src/inlined-worker.js | 2552 ++++++++++++++++- .../vuu-data/src/remote-data-source.ts | 31 + vuu-ui/packages/vuu-data/src/worker.ts | 2 +- vuu-ui/packages/vuu-datagrid-types/index.d.ts | 15 +- .../packages/vuu-popups/src/popup/Popup.tsx | 1 + .../src/popup/useAnchoredPosition.ts | 2 + vuu-ui/packages/vuu-protocol-types/index.d.ts | 13 + .../background-cell/BackgroundCell.tsx | 9 +- vuu-ui/packages/vuu-table/src/index.ts | 6 +- .../packages/vuu-table/src/table-next/Row.css | 13 +- .../packages/vuu-table/src/table-next/Row.tsx | 5 +- .../vuu-table/src/table-next/TableNext.css | 5 +- .../vuu-table/src/table-next/TableNext.tsx | 43 +- .../dropdown-cell/DropdownCell.tsx | 5 +- .../cell-renderers/toggle-cell/ToggleCell.tsx | 9 +- .../vuu-table/src/table-next/index.ts | 1 + .../src/table-next/table-cell/TableCell.tsx | 4 - .../src/table-next/table-dom-utils.ts | 15 + .../useControlledTableNavigation.ts | 48 + .../vuu-table/src/table-next/useDataSource.ts | 2 +- .../src/table-next/useKeyboardNavigation.ts | 190 +- .../vuu-table/src/table-next/useSelection.ts | 100 + .../vuu-table/src/table-next/useTableNext.ts | 128 +- .../packages/vuu-table/src/table/TableRow.tsx | 6 +- .../vuu-table/src/table/dataTableTypes.ts | 21 +- .../vuu-table/src/table/useSelection.ts | 2 +- .../vuu-ui-controls/src/common-hooks/index.ts | 5 +- .../src/common-hooks/selectionTypes.ts | 2 +- .../src/common-hooks/useSelection.ts | 21 +- .../src/common-hooks/useStateRef.ts | 31 + .../cycle-state-button/CycleStateButton.tsx | 1 - .../src/drag-drop/DragDropProvider.tsx | 40 +- .../src/drag-drop/Draggable.tsx | 108 +- .../src/drag-drop/dragDropTypesNext.ts | 12 +- .../src/drag-drop/drop-target-utils.ts | 32 +- .../src/drag-drop/useDragDropCopy.ts | 37 + .../useDragDropNaturalMovementNext.tsx | 10 +- .../src/drag-drop/useDragDropNext.tsx | 89 +- .../src/drag-drop/useGlobalDragDrop.ts | 33 +- .../src/editable/useEditableText.ts | 12 +- .../instrument-picker/InstrumentPicker.tsx | 8 +- .../instrument-picker/useInstrumentPicker.ts | 16 +- .../instrument-search/InstrumentSearch.css | 2 + .../instrument-search/InstrumentSearch.tsx | 32 +- .../common-hooks/useKeyboardNavigation.ts | 1 + .../vuu-ui-controls/src/list/useList.ts | 2 +- .../src/vuu-input/VuuInput.tsx | 7 +- vuu-ui/packages/vuu-utils/src/html-utils.ts | 10 + .../sample-apps/app-vuu-example/src/App.tsx | 65 +- .../src/VuuBasketTradingFeature.tsx | 2 + .../src/basket-selector/BasketSelector.css | 5 +- .../src/basket-selector/BasketSelector.tsx | 7 +- .../src/basket-selector/BasketSelectorRow.css | 16 + .../src/basket-selector/BasketSelectorRow.tsx | 11 +- .../src/basket-selector/useBasketSelector.ts | 17 +- .../src/basket-table-edit/BasketTableEdit.tsx | 4 + .../src/basket-toolbar/BasketToolbar.tsx | 15 +- .../src/new-basket-panel/NewBasketPanel.tsx | 18 +- .../src/new-basket-panel/useNewBasketPanel.ts | 7 +- .../src/useBasketContextMenus.ts | 9 +- .../src/useBasketTrading.tsx | 57 +- .../src/examples/Apps/NewTheme.examples.tsx | 58 +- .../src/examples/Shell/AppHeader.examples.tsx | 8 +- .../examples/Shell/LoginPanel.examples.tsx | 2 +- .../src/examples/Table/TableNext.examples.tsx | 115 +- .../examples/UiControls/DragDrop.examples.tsx | 8 +- .../UiControls/InstrumentPicker.examples.tsx | 47 +- .../UiControls/InstrumentSearch.examples.tsx | 106 +- .../VuuFeatures/BasketSelector.examples.tsx | 95 +- .../html/html-table-components/Row.tsx | 6 +- .../html-table-components/vuu-table/Row.tsx | 6 +- 95 files changed, 4155 insertions(+), 1117 deletions(-) delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts delete mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts create mode 100644 vuu-ui/packages/vuu-table/src/table-next/useControlledTableNavigation.ts create mode 100644 vuu-ui/packages/vuu-table/src/table-next/useSelection.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/common-hooks/useStateRef.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts diff --git a/vuu-ui/packages/vuu-data-test/src/Table.ts b/vuu-ui/packages/vuu-data-test/src/Table.ts index 0c76ffdcd..9b645b45f 100644 --- a/vuu-ui/packages/vuu-data-test/src/Table.ts +++ b/vuu-ui/packages/vuu-data-test/src/Table.ts @@ -1,19 +1,28 @@ import { TableSchema } from "@finos/vuu-data"; import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; -import { EventEmitter } from "@finos/vuu-utils"; +import { ColumnMap, EventEmitter } from "@finos/vuu-utils"; export type TableEvents = { delete: (row: VuuRowDataItemType[]) => void; insert: (row: VuuRowDataItemType[]) => void; + update: (row: VuuRowDataItemType[], columnName: string) => void; }; export class Table extends EventEmitter { #data: VuuRowDataItemType[][]; + #dataMap: ColumnMap; + #indexOfKey: number; #schema: TableSchema; - constructor(schema: TableSchema, data: VuuRowDataItemType[][]) { + constructor( + schema: TableSchema, + data: VuuRowDataItemType[][], + dataMap: ColumnMap + ) { super(); this.#data = data; + this.#dataMap = dataMap; this.#schema = schema; + this.#indexOfKey = dataMap[schema.key]; } get data() { @@ -24,4 +33,18 @@ export class Table extends EventEmitter { this.#data.push(row); this.emit("insert", row); } + + update(key: string, columnName: string, value: VuuRowDataItemType) { + const rowIndex = this.#data.findIndex( + (row) => row[this.#indexOfKey] === key + ); + const colIndex = this.#dataMap[columnName]; + if (rowIndex !== -1) { + const row = this.#data[rowIndex]; + const newRow = row.slice(); + newRow[colIndex] = value; + this.#data[rowIndex] = newRow; + this.emit("update", newRow, columnName); + } + } } diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index cd47eda97..e939ed9ae 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -15,6 +15,7 @@ import { VuuRange, VuuRowDataItemType, } from "@finos/vuu-protocol-types"; +import { metadataKeys } from "@finos/vuu-utils"; import { UpdateGenerator, UpdateHandler } from "./rowUpdates"; import { Table } from "./Table"; @@ -35,6 +36,7 @@ export interface TickingArrayDataSourceConstructorProps export class TickingArrayDataSource extends ArrayDataSource { #rpcServices: RpcService[] | undefined; #updateGenerator: UpdateGenerator | undefined; + #table?: Table; constructor({ data, @@ -54,11 +56,13 @@ export class TickingArrayDataSource extends ArrayDataSource { this._menu = menu; this.#rpcServices = rpcServices; this.#updateGenerator = updateGenerator; + this.#table = table; updateGenerator?.setDataSource(this); updateGenerator?.setUpdateHandler(this.processUpdates); if (table) { table.on("insert", this.insert); + table.on("update", this.update); } } @@ -139,6 +143,16 @@ export class TickingArrayDataSource extends ArrayDataSource { }, []); } + applyEdit( + row: DataSourceRow, + columnName: string, + value: VuuRowDataItemType + ): Promise { + const key = row[metadataKeys.KEY]; + this.#table?.update(key, columnName, value); + return Promise.resolve(true); + } + async menuRpcCall( rpcRequest: Omit | ClientToServerEditRpc ): Promise< diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts index 4c06d6bb5..d92ce20ce 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts @@ -20,6 +20,18 @@ const buildDataColumnMap = (tableName: BasketsTableName) => {} ); +const tableMaps: Record = { + algoType: buildDataColumnMap("algoType"), + basket: buildDataColumnMap("basket"), + basketTrading: buildDataColumnMap("basketTrading"), + basketTradingConstituent: buildDataColumnMap("basketTradingConstituent"), + basketConstituent: buildDataColumnMap("basketConstituent"), + basketTradingConstituentJoin: buildDataColumnMap( + "basketTradingConstituentJoin" + ), + priceStrategyType: buildDataColumnMap("priceStrategyType"), +}; + //--------------- const { KEY } = metadataKeys; @@ -72,13 +84,18 @@ for (const row of sp500) { const basketConstituent = new Table( schemas.basketConstituent, - basketConstituentData + basketConstituentData, + tableMaps.basketConstituent ); /** * BasketTrading */ -const basketTrading = new Table(schemas.basketTrading, []); +const basketTrading = new Table( + schemas.basketTrading, + [], + tableMaps.basketTrading +); let basketIncrement = 1; /** @@ -86,26 +103,36 @@ let basketIncrement = 1; */ const basketTradingConstituent = new Table( schemas.basketTradingConstituent, - [] + [], + tableMaps.basketTradingConstituent ); const basketTradingConstituentJoin = new Table( schemas.basketTradingConstituentJoin, - [] + [], + tableMaps.basketTradingConstituentJoin ); +// export as convenience for showcase examples +export const createBasketTradingRow = ( + basketId: string, + basketName: string, + side = "BUY", + status = "OFF MARKET" +) => [ + basketId, + basketName, + 0, + 1.25, + `steve-${basketIncrement++}`, + side, + status, + 1_000_000, + 1_250_000, + 100, +]; + function createTradingBasket(basketId: string, basketName: string) { - const instanceId = `steve-${basketIncrement++}`; - const basketTradingRow = [ - basketId, - basketName, - 0, - 1.25, - instanceId, - "OFF MARKET", - 1_000_000, - 1_250_000, - 100, - ]; + const basketTradingRow = createBasketTradingRow(basketId, basketName); basketTrading.insert(basketTradingRow); @@ -126,13 +153,15 @@ function createTradingBasket(basketId: string, basketName: string) { const side = "BUY"; const venue = "venue"; + const { instanceId } = tableMaps.basketTrading; + const basketInstanceId = basketTradingRow[instanceId]; const basketTradingConstituentRow: VuuRowDataItemType[] = [ algo, algoParams, basketId, description, - instanceId, - `${instanceId}-${ric}`, + basketInstanceId, + `${basketInstanceId}-${ric}`, limitPrice, notionalLocal, notionalUsd, @@ -168,8 +197,8 @@ function createTradingBasket(basketId: string, basketName: string) { bidSize, close, description, - instanceId, - `${instanceId}-${ric}`, + basketInstanceId, + `${basketInstanceId}-${ric}`, last, limitPrice, notionalLocal, @@ -202,42 +231,42 @@ async function createNewBasket(rpcRequest: any) { //------------------- -const tableMaps: Record = { - algoType: buildDataColumnMap("algoType"), - basket: buildDataColumnMap("basket"), - basketTrading: buildDataColumnMap("basketTrading"), - basketTradingConstituent: buildDataColumnMap("basketTradingConstituent"), - basketConstituent: buildDataColumnMap("basketConstituent"), - basketTradingConstituentJoin: buildDataColumnMap( - "basketTradingConstituentJoin" - ), - priceStrategyType: buildDataColumnMap("priceStrategyType"), -}; - export const tables: Record = { - algoType: new Table(schemas.algoType, [ - ["Sniper", 0], - ["Dark Liquidity", 1], - ["VWAP", 2], - ["POV", 3], - ["Dynamic Close", 4], - ]), - basket: new Table(schemas.basket, [ - [".NASDAQ100", ".NASDAQ100", 0, 0], - [".HSI", ".HSI", 0, 0], - [".FTSE100", ".FTSE100", 0, 0], - [".SP500", ".SP500", 0, 0], - ]), + algoType: new Table( + schemas.algoType, + [ + ["Sniper", 0], + ["Dark Liquidity", 1], + ["VWAP", 2], + ["POV", 3], + ["Dynamic Close", 4], + ], + tableMaps.algoType + ), + basket: new Table( + schemas.basket, + [ + [".NASDAQ100", ".NASDAQ100", 0, 0], + [".HSI", ".HSI", 0, 0], + [".FTSE100", ".FTSE100", 0, 0], + [".SP500", ".SP500", 0, 0], + ], + tableMaps.basket + ), basketConstituent, basketTrading, basketTradingConstituent, basketTradingConstituentJoin, - priceStrategyType: new Table(schemas.priceStrategyType, [ - ["Peg to Near Touch", 0], - ["Far Touch", 1], - ["Limit", 2], - ["Algo", 3], - ]), + priceStrategyType: new Table( + schemas.priceStrategyType, + [ + ["Peg to Near Touch", 0], + ["Far Touch", 1], + ["Limit", 2], + ["Algo", 3], + ], + tableMaps.priceStrategyType + ), }; const menus: Record = { diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts deleted file mode 100644 index 34aa03b1b..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BasketColumnMap, BasketReferenceData } from "../reference-data"; -import { getGenerators } from "../../generatorTemplate"; - -const [rowGenerator] = getGenerators( - "basket", - BasketColumnMap, - BasketReferenceData -); - -export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts deleted file mode 100644 index 279313978..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - BasketConstituentColumnMap, - BasketConstituentReferenceData, -} from "../reference-data"; -import { getGenerators } from "../../generatorTemplate"; - -const [rowGenerator] = getGenerators( - "basketConstituent", - BasketConstituentColumnMap, - BasketConstituentReferenceData -); - -export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts deleted file mode 100644 index cbd01998e..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - BasketTradingColumnMap, - BasketTradingReferenceData, -} from "../reference-data"; -import { getGenerators } from "../../generatorTemplate"; - -const [rowGenerator] = getGenerators( - "basketTrading", - BasketTradingColumnMap, - BasketTradingReferenceData -); - -export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts deleted file mode 100644 index 3c8391fd3..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - BasketTradingConstituentColumnMap, - BasketTradingConstituentReferenceData, -} from "../reference-data"; -import { getGenerators } from "../../generatorTemplate"; - -const [rowGenerator] = getGenerators( - "basketTradingConstituent", - BasketTradingConstituentColumnMap, - BasketTradingConstituentReferenceData -); - -export default rowGenerator; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts deleted file mode 100644 index 5933c46ae..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RowGeneratorFactory } from "../.."; -import { BasketsTableName } from "../basket-schemas"; -import basketGenerators from "./basket-generator"; -import basketConstituentGenerators from "./basketConstituent-generator"; -import basketTradingGenerators from "./basketTrading-generator"; -import basketTradingConstituentGenerators from "./basketTradingConstituent-generator"; - -const generators: Record = { - algoType: undefined, - basket: basketGenerators, - basketConstituent: basketConstituentGenerators, - basketTrading: basketTradingGenerators, - basketTradingConstituent: basketTradingConstituentGenerators, - basketTradingConstituentJoin: basketTradingConstituentGenerators, - priceStrategyType: undefined, -}; - -export default generators; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts deleted file mode 100644 index a3a14b0bf..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { ColumnMap } from "@finos/vuu-utils"; -import { getSchema } from "../../schemas"; - -import baskets, { BasketColumnMap } from "./basket"; -import basketConstituents from "./basketConstituent"; - -const schema = getSchema("basketTrading"); - -export const BasketTradingColumnMap = Object.values( - schema.columns -).reduce((map, col, index) => { - map[col.name] = index; - return map; -}, {}); - -let instance = 1; - -const data: VuuDataRow[] = []; - -const createBasket = (basketId: string, basketName: string) => { - const key = BasketColumnMap.basketId; - const basketRow = baskets.find((basket) => basket[key] === basketId); - const basketTradingRow = [ - basketId, - basketName, - 0, - 0, - `steve-${instance++}`, - "OFF-MARKET", - 0, - 0, - 0, - ]; - data.push(basketTradingRow); -}; - -// createBasket(".FTSE", "Steve FTSE 1"); -// createBasket(".FTSE", "Steve FTSE 2"); -// createBasket(".FTSE", "Steve FTSE 3"); -// createBasket(".FTSE", "Steve FTSE 4"); -// createBasket(".FTSE", "Steve FTSE 5"); -// createBasket(".FTSE", "Steve FTSE 6"); - -export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts deleted file mode 100644 index 01e3471ba..000000000 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { ColumnMap } from "@finos/vuu-utils"; -import { getSchema } from "../../schemas"; - -const schema = getSchema("basketTradingConstituent"); - -export const BasketTradingConstituentColumnMap = Object.values( - schema.columns -).reduce((map, col, index) => { - map[col.name] = index; - return map; -}, {}); - -const data: VuuDataRow[] = []; - -export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts index 4ece82ad2..b430b4240 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts @@ -3,11 +3,3 @@ export { default as BasketConstituentReferenceData, BasketConstituentColumnMap, } from "./basketConstituent"; -export { - default as BasketTradingReferenceData, - BasketTradingColumnMap, -} from "./basketTrading"; -export { - default as BasketTradingConstituentReferenceData, - BasketTradingConstituentColumnMap, -} from "./basketTradingConstituent"; diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts index a702e7142..5acf00475 100644 --- a/vuu-ui/packages/vuu-data-test/src/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -4,4 +4,5 @@ export * from "./TickingArrayDataSource"; export * from "./vuu-row-generator"; export * from "./vuu-modules"; export { type BasketsTableName } from "./basket/basket-schemas"; +export { createBasketTradingRow } from "./basket/basket-module"; export { type SimulTableName } from "./simul/simul-schemas"; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts deleted file mode 100644 index d1af767b5..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; -import { getSchema } from "../../index"; -import { currencies, locations, suffixes } from "./generatedData"; - -function random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -const accounts = [ - "Account 1", - "Account 2", - "Account 3", - "Account 4", - "Account 5", -]; - -const strategies = [ - "Strategy 1", - "Strategy 2", - "Strategy 3", - "Strategy 4", - "Strategy 5", -]; -const algos = ["Algo 1", "Algo 2", "Algo 3", "Algo 4", "Algo 5"]; - -const maxIndex = 20 * 20 * 20 * 20 * 8; - -export const RowGenerator: RowGeneratorFactory = () => (index: number) => { - if (index > maxIndex) { - throw Error("generateRow index val is too high"); - } - - const suffix = suffixes[random(0, suffixes.length - 1)]; - - const account = accounts[random(0, 4)]; - const averagePrice = 0; - const ccy = currencies[random(0, 4)]; - const exchange = locations[suffix][1]; - const filledQty = 0; - const id = `${index}`; - const idAsInt = index; - const lastUpdate = Date.now(); - const openQty = 0; - const parentOrderId = 0; - const price = 0; - const quantity = 0; - const ric = "AAA.L"; - const side = "buy"; - const status = "active"; - const strategy = strategies[random(0, strategies.length - 1)]; - const volLimit = 10_000; - - return [ - account, - averagePrice, - ccy, - exchange, - filledQty, - id, - idAsInt, - lastUpdate, - openQty, - parentOrderId, - price, - quantity, - ric, - side, - status, - strategy, - volLimit, - ]; -}; - -export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { - const schema = getSchema("childOrders"); - const schemaColumns: ColumnDescriptor[] = schema.columns; - if (typeof columns === "number") { - throw Error("ChildOrderColumnGenerator must be passed columns (strings)"); - } else if (columns.length === 0) { - return schemaColumns; - } else { - // TODO return just requested columns and apply extended config - return schemaColumns; - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts deleted file mode 100644 index 0cf067d08..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { faker } from "@faker-js/faker"; -import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; - -export function createArray(numofrows: number): VuuRowDataItemType[][] { - const result = []; - - for (let i = 0; i < numofrows; i++) { - const FakerDataGenerator = [ - faker.company.name(), - faker.finance.currencyCode(), - Number(faker.finance.amount({ min: 5, max: 10, dec: 2 })), - faker.finance.amount({ min: 100, max: 2000, dec: 0 }), - faker.finance.transactionType(), - faker.finance.transactionDescription(), - faker.date.anytime().getMilliseconds(), - faker.finance.accountName(), - faker.finance.accountNumber(), - faker.commerce.department(), - faker.commerce.product(), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - faker.finance.amount({ min: 5, max: 10, dec: 2 }), - ]; - result.push([ - i + 1, - FakerDataGenerator[0], - FakerDataGenerator[1], - Number(FakerDataGenerator[2]), - FakerDataGenerator[3] as number, - Number( - Math.floor( - Number(FakerDataGenerator[2]) * Number(FakerDataGenerator[3]) - ) - ), - FakerDataGenerator[4], - FakerDataGenerator[5], - FakerDataGenerator[6], - FakerDataGenerator[7], - FakerDataGenerator[8], - FakerDataGenerator[9], - FakerDataGenerator[10], - FakerDataGenerator[11], - FakerDataGenerator[12], - FakerDataGenerator[13], - FakerDataGenerator[14], - FakerDataGenerator[15], - FakerDataGenerator[16], - FakerDataGenerator[17], - Number(FakerDataGenerator[18]), - ]); - } - - return result; -} diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts deleted file mode 100644 index 78809fc23..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const locations: { [key: string]: string[] } = { - L: ["London PLC", "XLON/LSE-SETS"], - N: ["Corporation", "XNGS/NAS-GSM"], - AS: ["B.V.", "XAMS/ENA-MAIN"], - OQ: ["Co.", "XNYS/NYS-MAIN"], - PA: ["Paris", "PAR/EUR_FR"], - MI: ["Milan", "MIL/EUR_IT"], - FR: ["Frankfurt", "FR/EUR_DE"], - AT: ["Athens", "AT/EUR_GR"], -}; -export const suffixes = ["L", "N", "OQ", "AS", "PA", "MI", "FR", "AT"]; -export const currencies = ["CAD", "GBX", "USD", "EUR", "GBP"]; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts deleted file mode 100644 index b378012d4..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * as childOrders from "./child-order-generator"; -export * as instruments from "./instrument-generator"; -export * as instrumentPrices from "./instrument-prices-generator"; -export * as orders from "./order-generator"; -export * as parentOrders from "./parent-order-generator"; -export * as prices from "./prices-generator"; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts deleted file mode 100644 index 609c7eda5..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; -import { getSchema } from "../../index"; -import { - InstrumentReferenceData, - InstrumentColumnMap, -} from "../reference-data"; -import { getCalculatedColumnType, isCalculatedColumn } from "@finos/vuu-utils"; - -export type ExtendedColumnConfig = { [key: string]: Partial }; - -export const RowGenerator: RowGeneratorFactory = - (columnNames?: string[]) => (index: number) => { - if (index >= InstrumentReferenceData.length) { - throw Error("generateRow index val is too high"); - } - if (columnNames) { - return columnNames.map( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (name) => InstrumentReferenceData[index][InstrumentColumnMap[name]] - ); - } else { - return InstrumentReferenceData[index].slice(0, 7); - } - }; - -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - const schema = getSchema("instruments"); - const instrumentColumns: ColumnDescriptor[] = schema.columns; - if (typeof columns === "number") { - throw Error("InstrumentColumnGenerator must be passed columns (strings)"); - } else if (columns.length === 0) { - return instrumentColumns.map((column) => ({ - ...column, - ...columnConfig[column.name], - })); - } else { - return columns.map((name) => { - const column = instrumentColumns.find((col) => col.name === name); - if (column) { - return { - ...column, - ...columnConfig[column.name], - }; - } else if (isCalculatedColumn(name)) { - return { - name, - serverDataType: getCalculatedColumnType({ name }), - } as ColumnDescriptor; - } else { - throw Error(`InstrumentColumnGenerator no column ${name}`); - } - }); - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts deleted file mode 100644 index 3059b73cc..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { buildColumnMap } from "@finos/vuu-utils/src"; -import { - InstrumentPricesColumnMap, - InstrumentPricesReferenceData, -} from "../reference-data"; -import { BaseUpdateGenerator } from "../../UpdateGenerator"; -import { getSchema } from "../../index"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; - -const instrumentPriceSchema = getSchema("instrumentPrices"); - -export const RowGenerator: RowGeneratorFactory = - (columnNames?: string[]) => (index: number) => { - if (index >= InstrumentPricesReferenceData.length) { - throw Error("generateRow index val is too high"); - } - if (columnNames) { - return columnNames.map( - (name) => - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - InstrumentPricesReferenceData[index][InstrumentPricesColumnMap[name]] - ); - } else { - return InstrumentPricesReferenceData[index].slice(0, 7); - } - }; - -const { bid, bidSize, ask, askSize } = buildColumnMap( - instrumentPriceSchema.columns -); - -export const createUpdateGenerator = () => - new BaseUpdateGenerator([bid, bidSize, ask, askSize]); - -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [] - //columnConfig: ExtendedColumnConfig = {} -) => { - const instrumentPriceColumns: ColumnDescriptor[] = - instrumentPriceSchema.columns; - if (typeof columns === "number") { - throw Error( - "InstrumentPricesColumnGenerator must be passed columns (strings)" - ); - } else if (columns.length === 0) { - return instrumentPriceColumns; - } else { - return columns.map((name) => { - const column = instrumentPriceColumns.find((col) => col.name === name); - if (column) { - return column; - } else { - throw Error(`InstrumentPricesColumnGenerator no column ${name}`); - } - }); - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts deleted file mode 100644 index df7389829..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; -import { getSchema } from "../../index"; - -export type ExtendedColumnConfig = { [key: string]: Partial }; - -function random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -const chars = Array.from("ABCDEFGHIJKLMNOPQRST"); -const suffixes = ["L", "N", "OQ", "AS", "PA", "MI", "FR", "AT"]; -const currencies = ["CAD", "GBX", "USD", "EUR", "GBP"]; -const sides = ["buy", "sell", "buy", "sell", "short"]; -const traders = ["Arkwright", "Enfield", "Bailey", "Cui", "Kohl"]; - -/* - each top level loop (20 x 'A...') has 64,000 iterations of nested loops, - so divide index by 64000 to get index of first character - - remainder is our index into next level of loops - each second level loop ( 20 x 'A...') has, 3,200 iterations, so divide remainder by - 3,200 to get index of second character - - each third level loop (20 x 'A...') has 160 iterations - -*/ - -const maxIndex = 20 * 20 * 20 * 20 * 8; - -export const RowGenerator: RowGeneratorFactory = () => (index: number) => { - if (index > maxIndex) { - throw Error("generateRow index val is too high"); - } - const index1 = Math.floor(index / 64000); - const remainder1 = index % 64000; - - const index2 = Math.floor(remainder1 / 3200); - const remainder2 = remainder1 % 3200; - - const index3 = Math.floor(remainder2 / 160); - const remainder3 = remainder2 % 160; - - const index4 = Math.floor(remainder3 / 8); - const remainder4 = remainder3 % 8; - - const suffix = suffixes[remainder4]; - - const ccy = currencies[random(0, 4)]; - const created = 0; - const filledQuantity = 0; - const lastUpdate = 0; - const orderId = "1"; - const quantity = 0; - const ric = `${chars[index1]}${chars[index2]}${chars[index3]}${chars[index4]}.${suffix}`; - const side = sides[random(0, 4)]; - const trader = traders[random(0, 4)]; - - return [ - ccy, - created, - filledQuantity, - lastUpdate, - orderId, - quantity, - ric, - side, - trader, - ]; -}; - -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); - const schema = getSchema("orders"); - const instrumentColumns: ColumnDescriptor[] = schema.columns; - if (typeof columns === "number") { - throw Error("OrderColumnGenerator must be passed columns (strings)"); - } else if (columns.length === 0) { - return instrumentColumns; - } else { - // TODO return just requested columns and apply extended config - return instrumentColumns; - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts deleted file mode 100644 index 78c773c1c..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; -import { getSchema } from "../../index"; -import { currencies, locations, suffixes } from "./generatedData"; - -function random(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -const accounts = [ - "Account 1", - "Account 2", - "Account 3", - "Account 4", - "Account 5", -]; -const algos = ["Algo 1", "Algo 2", "Algo 3", "Algo 4", "Algo 5"]; - -const maxIndex = 20 * 20 * 20 * 20 * 8; - -export const RowGenerator: RowGeneratorFactory = () => (index: number) => { - if (index > maxIndex) { - throw Error("generateRow index val is too high"); - } - - const suffix = suffixes[random(0, suffixes.length - 1)]; - - const account = accounts[random(0, 4)]; - const algo = algos[random(0, 4)]; - const avgPrice = 0; - const ccy = currencies[random(0, 4)]; - const childCount = 0; - const exchange = locations[suffix][1]; - const filledQty = 0; - const id = `${index}`; - const idAsInt = index; - const lastUpdate = Date.now(); - const openQty = 0; - const price = 0; - const quantity = 0; - const ric = "AAA.L"; - const side = "buy"; - const status = "active"; - const volLimit = 10_000; - - return [ - account, - algo, - avgPrice, - ccy, - childCount, - exchange, - filledQty, - id, - idAsInt, - lastUpdate, - openQty, - price, - quantity, - ric, - side, - status, - volLimit, - ]; -}; - -export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { - const schema = getSchema("parentOrders"); - const schemaColumns: ColumnDescriptor[] = schema.columns; - if (typeof columns === "number") { - throw Error("ParentOrderColumnGenerator must be passed columns (strings)"); - } else if (columns.length === 0) { - return schemaColumns; - } else { - // TODO return just requested columns and apply extended config - return schemaColumns; - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts deleted file mode 100644 index 223cbf01e..000000000 --- a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { buildColumnMap } from "@finos/vuu-utils"; -import { PriceReferenceData } from "../reference-data"; -import { - ColumnGeneratorFn, - RowGeneratorFactory, -} from "../../vuu-row-generator"; -import { BaseUpdateGenerator } from "../../UpdateGenerator"; -import { getAllSchemas } from "../../index"; - -export const RowGenerator: RowGeneratorFactory = () => (index: number) => { - if (index >= PriceReferenceData.length) { - throw Error("generateRow index val is too high"); - } - - return PriceReferenceData[index]; -}; - -const schemas = getAllSchemas(); -const { prices: pricesSchema } = schemas; -const { bid, bidSize, ask, askSize } = buildColumnMap(pricesSchema.columns); -const tickingColumns = [bid, bidSize, ask, askSize]; -export const createUpdateGenerator = () => - new BaseUpdateGenerator(tickingColumns); - -export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { - const schemaColumns: ColumnDescriptor[] = pricesSchema.columns; - if (typeof columns === "number") { - throw Error("PricesColumnGenerator must be passed columns (strings)"); - } else if (columns.length === 0) { - return schemaColumns; - } else { - // TODO return just requested columns and apply extended config - return schemaColumns; - } -}; diff --git a/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts index 3468f64c1..91615e1d0 100644 --- a/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts @@ -1,7 +1,5 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; import { VuuRowDataItemType, VuuTable } from "@finos/vuu-protocol-types"; -import * as simulDataGenerators from "./simul/data-generators"; -import * as basketDataGenerators from "./basket/data-generators"; import { UpdateGenerator } from "./rowUpdates"; type RowAtIndexFunc = (index: number) => T | undefined; diff --git a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts index 839ea1821..4bf28e993 100644 --- a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts @@ -20,6 +20,7 @@ import { getMissingItems, KeySet, logger, + metadataKeys, NULL_RANGE, rangeNewItems, resetRange, @@ -97,12 +98,9 @@ export class ArrayDataSource private dataIndices: number[] | undefined; /** Map reflecting positions of data items in raw data */ private dataMap: ColumnMap | undefined; - private disabled = false; - private groupedData: undefined | DataSourceRow[]; private groupMap: undefined | GroupMap; /** the index of key field within raw data row */ private key: number; - private suspended = false; private tableSchema: TableSchema; private lastRangeServed: VuuRange = { from: 0, to: 0 }; private rangeChangeRowset: "delta" | "full"; @@ -440,6 +438,24 @@ export class ArrayDataSource } }; + protected update = (row: VuuRowDataItemType[], columnName: string) => { + // TODO take sorting, filtering. grouping into account + const keyValue = row[this.key]; + const { KEY } = metadataKeys; + const colIndex = this.#columnMap[columnName]; + const dataColIndex = this.dataMap?.[columnName]; + const dataIndex = this.#data.findIndex((row) => row[KEY] === keyValue); + if (dataIndex !== -1 && dataColIndex !== undefined) { + const dataSourceRow = this.#data[dataIndex]; + dataSourceRow[colIndex] = row[dataColIndex]; + const { from, to } = this.#range; + const [rowIdx] = dataSourceRow; + if (rowIdx >= from && rowIdx < to) { + this.sendRowsToClient(true); + } + } + }; + private setRange(range: VuuRange, forceFullRefresh = false) { this.#range = range; this.keys.reset(range); diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index f04af2e0f..d026a1f87 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -8,8 +8,8 @@ import { ClientToServerMenuRPC, LinkDescriptorWithLabel, VuuAggregation, - VuuColumnDataType, VuuColumns, + VuuDataRowDto, VuuFilter, VuuGroupBy, VuuLinkDescriptor, @@ -485,6 +485,12 @@ export type DataSourceEditHandler = ( value: VuuRowDataItemType ) => Promise; +export type DataSourceDeleteHandler = (key: string) => Promise; +export type DataSourceInsertHandler = ( + key: string, + data: VuuDataRowDto +) => Promise; + export type RpcResponse = | MenuRpcResponse | VuuUIMessageInRPCEditReject @@ -525,6 +531,9 @@ export interface DataSource extends EventEmitter { */ suspend?: () => void; resume?: () => void; + + deleteRow?: DataSourceDeleteHandler; + /** * For a dataSource that has been previously disabled and is currently in disabled state , this will restore * the subscription to active status. Fresh data will be dispatched to client. The enable call optionally @@ -540,6 +549,7 @@ export interface DataSource extends EventEmitter { disable?: () => void; filter: DataSourceFilter; groupBy: VuuGroupBy; + insertRow?: DataSourceInsertHandler; links?: LinkDescriptorWithLabel[]; menu?: VuuMenu; menuRpcCall: ( diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index 749733516..b0715dd93 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1,8 +1,2554 @@ export const workerSourceCode = ` -var de=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var d=(s,e,t)=>(de(s,e,"read from private field"),t?t.call(s):e.get(s)),k=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},ge=(s,e,t,n)=>(de(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function fe(s,e,t=[],n=[]){for(let r=0,o=s.length;r{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${s}=\`)))==null?void 0:t.split("=")[1]};function z({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ttypeof s=="string"&&at.includes(s),lt="error",A=()=>{},ct="error",{loggingLevel:U=ct}=pt(),R=s=>{let e=U==="debug",t=e||U==="info",n=t||U==="warn",r=n||U==="error",o=t?p=>console.info(\`[\${s}] \${p}\`):A,i=n?p=>console.warn(\`[\${s}] \${p}\`):A,u=e?p=>console.debug(\`[\${s}] \${p}\`):A;return{errorEnabled:r,error:r?p=>console.error(\`[\${s}] \${p}\`):A}};function pt(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:dt()}}function dt(){let s=me("vuu-logging-level");return ut(s)?s:lt}var{debug:gt,debugEnabled:ft}=R("range-monitor"),F=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:n}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),n)ft&>(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function he(s){return Array.isArray(s)}function mt(s){return!Array.isArray(s)}var y,Ce=class{constructor(){k(this,y,new Map)}addListener(e,t){let n=d(this,y).get(e);n?he(n)?n.push(t):mt(n)&&d(this,y).set(e,[n,t]):d(this,y).set(e,t)}removeListener(e,t){if(!d(this,y).has(e))return;let n=d(this,y).get(e),r=-1;if(n===t)d(this,y).delete(e);else if(Array.isArray(n)){for(let o=length;o-- >0;)if(n[o]===t){r=o;break}if(r<0)return;n.length===1?(n.length=0,d(this,y).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&d(this,y).has(e)?d(this,y).delete(e):e===void 0&&d(this,y).clear()}emit(e,...t){if(d(this,y)){let n=d(this,y).get(e);n&&this.invokeHandler(n,t)}}once(e,t){let n=(...r)=>{this.removeListener(e,n),t(...r)};this.on(e,n)}on(e,t){this.addListener(e,t)}hasListener(e,t){let n=d(this,y).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(he(e))e.slice().forEach(n=>this.invokeHandler(n,t));else switch(t.length){case 0:e();break;case 1:e(t[0]);break;case 2:e(t[0],t[1]);break;default:e.call(null,...t)}}};y=new WeakMap;var N=String.fromCharCode(8200),f=String.fromCharCode(8199);var En={DIGIT:f,TWO_DIGITS:f+f,THREE_DIGITS:f+f+f,FULL_PADDING:[null,N+f,N+f+f,N+f+f+f,N+f+f+f+f]};var Vn=f+f+f+f+f+f+f+f+f;var{COUNT:Gn}=V;var W=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((r,o)=>{(o=t)&&(this.free.push(r),this.keys.delete(o))});let n=t-e;this.keys.size+this.free.length>n&&(this.free.length=Math.max(0,n-this.keys.size));for(let r=e;rthis.keys.size&&(this.nextKeyValue=this.keys.size)}keyFor(e){let t=this.keys.get(e);if(t===void 0)throw console.log(\`key not found +var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); +}; +var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); +}; +var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); +}; +var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; +}; + +// ../vuu-utils/src/array-utils.ts +function partition(array, test, pass = [], fail = []) { + for (let i = 0, len = array.length; i < len; i++) { + (test(array[i], i) ? pass : fail).push(array[i]); + } + return [pass, fail]; +} + +// ../vuu-utils/src/column-utils.ts +var metadataKeys = { + IDX: 0, + RENDER_IDX: 1, + IS_LEAF: 2, + IS_EXPANDED: 3, + DEPTH: 4, + COUNT: 5, + KEY: 6, + SELECTED: 7, + count: 8, + // TODO following only used in datamodel + PARENT_IDX: "parent_idx", + IDX_POINTER: "idx_pointer", + FILTER_COUNT: "filter_count", + NEXT_FILTER_IDX: "next_filter_idx" +}; +var { DEPTH, IS_LEAF } = metadataKeys; + +// ../vuu-utils/src/cookie-utils.ts +var getCookieValue = (name) => { + var _a, _b; + if (((_a = globalThis.document) == null ? void 0 : _a.cookie) !== void 0) { + return (_b = globalThis.document.cookie.split("; ").find((row) => row.startsWith(\`\${name}=\`))) == null ? void 0 : _b.split("=")[1]; + } +}; + +// ../vuu-utils/src/range-utils.ts +function getFullRange({ from, to }, bufferSize = 0, rowCount = Number.MAX_SAFE_INTEGER) { + if (bufferSize === 0) { + if (rowCount < from) { + return { from: 0, to: 0 }; + } else { + return { from, to: Math.min(to, rowCount) }; + } + } else if (from === 0) { + return { from, to: Math.min(to + bufferSize, rowCount) }; + } else { + const rangeSize = to - from; + const buff = Math.round(bufferSize / 2); + const shortfallBefore = from - buff < 0; + const shortFallAfter = rowCount - (to + buff) < 0; + if (shortfallBefore && shortFallAfter) { + return { from: 0, to: rowCount }; + } else if (shortfallBefore) { + return { from: 0, to: rangeSize + bufferSize }; + } else if (shortFallAfter) { + return { + from: Math.max(0, rowCount - (rangeSize + bufferSize)), + to: rowCount + }; + } else { + return { from: from - buff, to: to + buff }; + } + } +} +var withinRange = (value, { from, to }) => value >= from && value < to; +var WindowRange = class { + constructor(from, to) { + this.from = from; + this.to = to; + } + isWithin(index) { + return withinRange(index, this); + } + //find the overlap of this range and a new one + overlap(from, to) { + return from >= this.to || to < this.from ? [0, 0] : [Math.max(from, this.from), Math.min(to, this.to)]; + } + copy() { + return new WindowRange(this.from, this.to); + } +}; + +// ../vuu-utils/src/DataWindow.ts +var { KEY } = metadataKeys; + +// ../vuu-utils/src/logging-utils.ts +var logLevels = ["error", "warn", "info", "debug"]; +var isValidLogLevel = (value) => typeof value === "string" && logLevels.includes(value); +var DEFAULT_LOG_LEVEL = "error"; +var NO_OP = () => void 0; +var DEFAULT_DEBUG_LEVEL = false ? "error" : "info"; +var { loggingLevel = DEFAULT_DEBUG_LEVEL } = getLoggingSettings(); +var logger = (category) => { + const debugEnabled5 = loggingLevel === "debug"; + const infoEnabled5 = debugEnabled5 || loggingLevel === "info"; + const warnEnabled = infoEnabled5 || loggingLevel === "warn"; + const errorEnabled = warnEnabled || loggingLevel === "error"; + const info5 = infoEnabled5 ? (message) => console.info(\`[\${category}] \${message}\`) : NO_OP; + const warn4 = warnEnabled ? (message) => console.warn(\`[\${category}] \${message}\`) : NO_OP; + const debug5 = debugEnabled5 ? (message) => console.debug(\`[\${category}] \${message}\`) : NO_OP; + const error4 = errorEnabled ? (message) => console.error(\`[\${category}] \${message}\`) : NO_OP; + if (false) { + return { + errorEnabled, + error: error4 + }; + } else { + return { + debugEnabled: debugEnabled5, + infoEnabled: infoEnabled5, + warnEnabled, + errorEnabled, + info: info5, + warn: warn4, + debug: debug5, + error: error4 + }; + } +}; +function getLoggingSettings() { + if (typeof loggingSettings !== "undefined") { + return loggingSettings; + } else { + return { + loggingLevel: getLoggingLevelFromCookie() + }; + } +} +function getLoggingLevelFromCookie() { + const value = getCookieValue("vuu-logging-level"); + if (isValidLogLevel(value)) { + return value; + } else { + return DEFAULT_LOG_LEVEL; + } +} + +// ../vuu-utils/src/debug-utils.ts +var { debug, debugEnabled } = logger("range-monitor"); +var RangeMonitor = class { + constructor(source) { + this.source = source; + this.range = { from: 0, to: 0 }; + this.timestamp = 0; + } + isSet() { + return this.timestamp !== 0; + } + set({ from, to }) { + const { timestamp } = this; + this.range.from = from; + this.range.to = to; + this.timestamp = performance.now(); + if (timestamp) { + debugEnabled && debug( + \`<\${this.source}> [\${from}-\${to}], \${(this.timestamp - timestamp).toFixed(0)} ms elapsed\` + ); + } else { + return 0; + } + } +}; + +// ../vuu-utils/src/event-emitter.ts +function isArrayOfListeners(listeners) { + return Array.isArray(listeners); +} +function isOnlyListener(listeners) { + return !Array.isArray(listeners); +} +var _events; +var EventEmitter = class { + constructor() { + __privateAdd(this, _events, /* @__PURE__ */ new Map()); + } + addListener(event, listener) { + const listeners = __privateGet(this, _events).get(event); + if (!listeners) { + __privateGet(this, _events).set(event, listener); + } else if (isArrayOfListeners(listeners)) { + listeners.push(listener); + } else if (isOnlyListener(listeners)) { + __privateGet(this, _events).set(event, [listeners, listener]); + } + } + removeListener(event, listener) { + if (!__privateGet(this, _events).has(event)) { + return; + } + const listenerOrListeners = __privateGet(this, _events).get(event); + let position = -1; + if (listenerOrListeners === listener) { + __privateGet(this, _events).delete(event); + } else if (Array.isArray(listenerOrListeners)) { + for (let i = length; i-- > 0; ) { + if (listenerOrListeners[i] === listener) { + position = i; + break; + } + } + if (position < 0) { + return; + } + if (listenerOrListeners.length === 1) { + listenerOrListeners.length = 0; + __privateGet(this, _events).delete(event); + } else { + listenerOrListeners.splice(position, 1); + } + } + } + removeAllListeners(event) { + if (event && __privateGet(this, _events).has(event)) { + __privateGet(this, _events).delete(event); + } else if (event === void 0) { + __privateGet(this, _events).clear(); + } + } + emit(event, ...args) { + if (__privateGet(this, _events)) { + const handler = __privateGet(this, _events).get(event); + if (handler) { + this.invokeHandler(handler, args); + } + } + } + once(event, listener) { + const handler = (...args) => { + this.removeListener(event, handler); + listener(...args); + }; + this.on(event, handler); + } + on(event, listener) { + this.addListener(event, listener); + } + hasListener(event, listener) { + const listeners = __privateGet(this, _events).get(event); + if (Array.isArray(listeners)) { + return listeners.includes(listener); + } else { + return listeners === listener; + } + } + invokeHandler(handler, args) { + if (isArrayOfListeners(handler)) { + handler.slice().forEach((listener) => this.invokeHandler(listener, args)); + } else { + switch (args.length) { + case 0: + handler(); + break; + case 1: + handler(args[0]); + break; + case 2: + handler(args[0], args[1]); + break; + default: + handler.call(null, ...args); + } + } + } +}; +_events = new WeakMap(); + +// ../vuu-utils/src/round-decimal.ts +var PUNCTUATION_STR = String.fromCharCode(8200); +var DIGIT_STR = String.fromCharCode(8199); +var Space = { + DIGIT: DIGIT_STR, + TWO_DIGITS: DIGIT_STR + DIGIT_STR, + THREE_DIGITS: DIGIT_STR + DIGIT_STR + DIGIT_STR, + FULL_PADDING: [ + null, + PUNCTUATION_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR, + PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + ] +}; +var LEADING_FILL = DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR; + +// ../vuu-utils/src/json-utils.ts +var { COUNT } = metadataKeys; + +// ../vuu-utils/src/keyset.ts +var KeySet = class { + constructor(range) { + this.keys = /* @__PURE__ */ new Map(); + this.free = []; + this.nextKeyValue = 0; + this.reset(range); + } + next() { + if (this.free.length > 0) { + return this.free.pop(); + } else { + return this.nextKeyValue++; + } + } + reset({ from, to }) { + this.keys.forEach((keyValue, rowIndex) => { + if (rowIndex < from || rowIndex >= to) { + this.free.push(keyValue); + this.keys.delete(rowIndex); + } + }); + const size = to - from; + if (this.keys.size + this.free.length > size) { + this.free.length = Math.max(0, size - this.keys.size); + } + for (let rowIndex = from; rowIndex < to; rowIndex++) { + if (!this.keys.has(rowIndex)) { + const nextKeyValue = this.next(); + this.keys.set(rowIndex, nextKeyValue); + } + } + if (this.nextKeyValue > this.keys.size) { + this.nextKeyValue = this.keys.size; + } + } + keyFor(rowIndex) { + const key = this.keys.get(rowIndex); + if (key === void 0) { + console.log(\`key not found keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{IDX:Zn}=V;var{SELECTED:er}=V,M={False:0,True:1,First:2,Last:4};var ht=(s,e)=>e>=s[0]&&e<=s[1],Ct=M.True+M.First+M.Last,bt=M.True+M.First,yt=M.True+M.Last,J=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return Ct}else if(ht(t,e))return e===t[0]?bt:e===t[1]?yt:M.True;return M.False};var be=s=>{if(s.every(t=>typeof t=="number"))return s;let e=[];for(let t of s)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var Tt=(()=>{let s=0,e=()=>\`0000\${(Math.random()*36**4<<0).toString(36)}\`.slice(-4);return()=>(s+=1,\`u\${e()}\${s}\`)})();var{debug:ks,debugEnabled:As,error:Se,info:E,infoEnabled:Vt,warn:D}=R("websocket-connection"),Re="ws",Mt=s=>s.startsWith(Re+"://")||s.startsWith(Re+"s://"),Ve={},X=Symbol("setWebsocket"),\$=Symbol("connectionCallback");async function Me(s,e,t,n=10,r=5){return Ve[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},xe(s,e,t)}async function Z(s){throw Error("connection broken")}async function xe(s,e,t,n){let{status:r,connect:o,reconnect:i}=Ve[s],u=r==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",p=await vt(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[X](p);let a=n!=null?n:new Q(p,s,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),a.status=l,u.remaining=u.allowed,a}catch{let p=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:p}),p)return xt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var xt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(xe(s,e,t,n))},r)}),vt=(s,e)=>new Promise((t,n)=>{let r=Mt(s)?s:\`wss://\${s}\`;Vt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=i=>n(i)}),we=()=>{D==null||D("Connection cannot be closed, socket not yet opened")},Ee=s=>{D==null||D(\`Message cannot be sent, socket closed \${s.body.type}\`)},It=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},Q=class{constructor(e,t,n,r){this.close=we;this.requiresLogin=!0;this.send=Ee;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=It(e.data);this.messagesCount+=1,this[\$](t)};this.url=t,this.protocol=n,this[\$]=r,this[X](e)}reconnect(){Z(this)}[(\$,X)](e){let t=this[\$];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{Se("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?Se("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=r)},e.onclose=()=>{E==null||E("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Z(this),this.send=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=we,this.send=Ee,E==null||E("close websocket")}}};var Dt=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],ve=s=>Dt.includes(s.type),ee=({requestId:s,...e})=>[s,e],Ie=s=>{let e=s.at(0);if(e.updateType==="SIZE"){if(s.length===1)return s;e=s.at(1)}let t=s.at(-1);return[e,t]},De=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var te=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Pe=s=>s.type==="connection-status",Le=s=>s.type==="connection-metrics";var _e=s=>"viewport"in s,Oe=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&q(s.action.table),q=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var ke="CHANGE_VP_SUCCESS",Ae="CHANGE_VP_RANGE_SUCCESS",Ue="CLOSE_TREE_NODE",Fe="CLOSE_TREE_SUCCESS";var Ne="CREATE_VP",We="DISABLE_VP",\$e="DISABLE_VP_SUCCESS";var qe="ENABLE_VP",Ge="ENABLE_VP_SUCCESS";var ne="GET_VP_VISUAL_LINKS",Be="GET_VIEW_PORT_MENUS";var Ke="HB",He="HB_RESP",je="LOGIN",ze="OPEN_TREE_NODE",Je="OPEN_TREE_SUCCESS";var Ye="REMOVE_VP";var re="RPC_RESP";var Ze="SET_SELECTION_SUCCESS",Xe="TABLE_ROW";var et=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var tt=[],T=R("array-backed-moving-window");function Pt(s,e){if(!e||e.data.length!==s.data.length||e.sel!==s.sel)return!1;for(let t=0;t{var t;if((t=T.info)==null||t.call(T,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return d(this,h).to-t0&&e-d(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:r}=this.range;if(t=r)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-d(this,h).from;if(Pt(e,this.internalData[n]))return!1;let r=this.isWithinClientRange(t);return(r||this.isWithinRange(t))&&(!this.internalData[n]&&r&&(this.rowsWithinRange+=1),this.internalData[n]=e),r}getAtIndex(e){return d(this,h).isWithin(e)&&this.internalData[e-d(this,h).from]!=null?this.internalData[e-d(this,h).from]:void 0}isWithinRange(e){return d(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=T.debug)==null||p.call(T,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,r=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===r)return[!1,tt];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let a=e;ao.to){let a=Math.max(e,o.to);i=this.internalData.slice(a-u,t-u)}else{let a=Math.min(o.from,t);i=this.internalData.slice(e-u,a-u)}return[this.bufferBreakout(e,t),i]}setRange(e,t){var n,r;if(e!==d(this,h).from||t!==d(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,i]=d(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let c=o;c=0;o--)if(e[o]!==void 0){r=e[o];break}return n&&r?[n.rowIndex,r.rowIndex]:[-1,-1]}};h=new WeakMap;var Lt=[],{debug:C,debugEnabled:B,error:_t,info:g,infoEnabled:Ot,warn:P}=R("viewport"),kt=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),K=[void 0,void 0],At={count:0,mode:void 0,size:0,ts:0},H=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},m){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=At;this.updateThrottleTimer=void 0;this.rangeMonitor=new F("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:n}=this.lastUpdateStatus,r=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,r=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,r=0;return this.lastUpdateStatus.mode=e,r};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:r}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(r>=o&&r{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(g==null||g("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=a,this.columns=n,this.filter=r,this.groupBy=o,this.keys=new W(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,Ot&&(g==null||g(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new G(this.clientRange,u,this.bufferSize),this.postMessageToClient=m}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:Ne,table:this.table,range:z(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:n,filterSpec:r,range:o,sort:i,groupBy:u},c){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=n,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:n,filter:r,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:r}=this,o=r.get(e);if(!o){_t("no matching operation found to complete");return}let{type:i}=o;if(g==null||g(\`completeOperation \${i}\`),r.delete(e),i==="CHANGE_VP_RANGE"){let[c,p]=t;(u=this.dataWindow)==null||u.setRange(c,p);for(let a=this.pendingRangeRequests.length-1;a>=0;a--){let l=this.pendingRangeRequests[a];if(l.requestId===e){l.acked=!0;break}else P==null||P("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:m}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=m,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),C==null||C(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,config:o.data}}else{if(i==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,C==null||C(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,groupBy:o.data};if(i==="columns")return this.columns=o.data,{clientViewportId:n,type:i,columns:o.data};if(i==="filter")return this.filter=o.data,{clientViewportId:n,type:i,filter:o.data};if(i==="aggregate")return this.aggregations=o.data,{clientViewportId:n,type:"aggregate",aggregations:this.aggregations};if(i==="sort")return this.sort=o.data,{clientViewportId:n,type:i,sort:this.sort};if(i!=="selection"){if(i==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:n};if(i==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:n};if(i==="CREATE_VISUAL_LINK"){let[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){B&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...z(t,this.bufferSize,u)}:null;if(c){B&&(C==null||C(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});let a=this.pendingRangeRequests.at(-1);if(a)if(a.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:m}=a;this.dataWindow.outOfRange(l,m)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:P==null||P("Range Request before previous request is acked")}this.pendingRangeRequests.push({...c,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);this.keys.reset(this.dataWindow.clientRange);let p=this.isTree?oe:se;return o.length?[c,o.map(a=>p(a,this.keys,this.selectedRows))]:i?[c,void 0,i]:[c]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:ze,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ue,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,r){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:r,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,g==null||g("suspend")}resume(){return this.suspended=!1,B&&(C==null||C(\`resume: \${this.currentData()}\`)),this.currentData()}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?oe:se;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),g==null||g(\`enable: \${this.serverViewportId}\`),{type:qe,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),g==null||g(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:We,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),C==null||C(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:n}=t;return g==null||g(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...r}=t;return this.useBatchMode&&(this.batchMode=!0),B?C==null||C(\`setConfig \${JSON.stringify(t)}\`):g==null||g("setConfig"),this.createRequest({...r,filterSpec:typeof(n==null?void 0:n.filter)=="string"?{filter:n.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),g==null||g(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),g==null||g(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=Lt){var n;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(n=this.dataWindow)==null||n.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),g==null||g(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:be(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],i=!0;if(e>=r&&er&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,n,r));this.pendingUpdates.length=0}else{let i=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of i)e.push(o(u,n,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?K:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},se=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?J(o,s):0].concat(n),oe=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[i,u,,c,,p,...a]=n;return[s,r.keyFor(s),c,u,i,p,e,t?J(o,s):0].concat(a)};var nt=1;var{debug:x,debugEnabled:L,error:_,info:S,infoEnabled:Ut,warn:ie}=R("server-proxy"),b=()=>\`\${nt++}\`,Ft={},Nt=s=>s.disabled!==!0&&s.suspended!==!0,Wt={type:"NO_ACTION"},\$t=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function qt(s,e){return s.map(t=>{let{parentVpId:n}=t,r=e.get(n);if(r)return{...t,parentClientVpId:r.clientViewportId,label:r.title};throw Error("addLabelsToLinks viewport not found")})}var j=class{constructor(e,t){this.authToken="";this.user="user";this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableMetaRequests=new Map;this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=fe(Array.from(this.viewports.values()),Nt);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.forEach(o=>{let{clientViewportId:i}=o;this.viewports.set(i,o),this.sendMessageToServer(o.subscribe(),i)})};n(e),setTimeout(()=>{n(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((n,r)=>{this.sendMessageToServer({type:je,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&_("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))_(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),n=new H(e,this.postMessageToClient);this.viewports.set(e.viewport,n);let r=this.awaitResponseToMessage(n.subscribe(),e.viewport);Promise.all([r,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=n;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,n)),this.mapClientToServerViewport.set(e.viewport,c);let a=n.handleSubscribed(i,u);a&&(this.postMessageToClient(a),L&&x(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),n.disabled&&this.disableViewport(n),p==="subscribing"&&!q(n.table)&&(this.sendMessageToServer({type:ne,vpId:c}),this.sendMessageToServer({type:Be,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:m}])=>l!==c&&!m).forEach(([l])=>{this.sendMessageToServer({type:ne,vpId:l})}))})}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(S==null||S(\`Unsubscribe Message (Client to Server): - \${t}\`),this.sendMessageToServer({type:Ye,viewPortId:t})):_(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let r=this.viewports.get(n);if(r)return r;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let n=b(),[r,o,i]=e.rangeRequest(n,t.range);S==null||S(\`setViewRange \${t.range.from} - \${t.range.to}\`),r&&this.sendIfReady(r,n,e.status==="subscribed"),o?(S==null||S(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):i&&this.postMessageToClient(i)}setConfig(e,t){let n=b(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=b(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=b(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=b(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=b(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=b(),{columns:r}=t,o=e.columnRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=b(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=b(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=b(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{S==null||S("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(x==null||x("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let t=e.resume();this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,i=b(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(i,o,u,r);this.sendMessageToServer(c,i)}else _("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=b(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var r;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(r=o.links)!=null&&r.some(i=>i.parentVpId===t)){let[i]=o.setLinks(\$t(o.links,t,n));this.postMessageToClient(i)}}removeViewportFromVisualLinks(e){var t;for(let n of this.viewports.values())if((t=n.links)!=null&&t.some(({parentVpId:r})=>r===e)){let[r]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(r)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=ee(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}rpcCall(e){let[t,n]=ee(e),r=et(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){var t;if(_e(e))if(e.type==="disable"){let n=this.getViewportForClient(e.viewport,!1);return n!==null?this.disableViewport(n):void 0}else{let n=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(n,e);case"config":return this.setConfig(n,e);case"aggregate":return this.aggregate(n,e);case"sort":return this.sort(n,e);case"groupBy":return this.groupBy(n,e);case"filter":return this.filter(n,e);case"select":return this.select(n,e);case"suspend":return this.suspendViewport(n);case"resume":return this.resumeViewport(n);case"enable":return this.enableViewport(n);case"openTreeNode":return this.openTreeNode(n,e);case"closeTreeNode":return this.closeTreeNode(n,e);case"createLink":return this.createLink(n,e);case"removeLink":return this.removeLink(n);case"setColumns":return this.setColumns(n,e);case"setTitle":return this.setTitle(n,e);default:}}else{if(ve(e))return this.menuRpcCall(e);{let{type:n,requestId:r}=e;switch(n){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:n},r)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:r})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,r).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:r})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}_(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=b()){if(q(e))return Promise.resolve(void 0);let n=\`\${e.module}:\${e.table}\`,r=this.cachedTableMetaRequests.get(n);return r||(r=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(n,r)),r==null?void 0:r.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=b()){return new Promise((n,r)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:r,resolve:n})})}sendIfReady(e,t,n=!0){return n?this.sendMessageToServer(e,t):this.queuedRequests.push(e),n}sendMessageToServer(e,t=\`\${nt++}\`,n=Ft){let{module:r="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:r,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:r}=e,o=this.pendingRequests.get(n);if(o){let{resolve:a}=o;this.pendingRequests.delete(n),a(t);return}let{viewports:i}=this;switch(t.type){case Ke:this.sendMessageToServer({type:He,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(r)this.sessionId=r,(u=this.pendingLogin)==null||u.resolve(r),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case"REMOVE_VP_SUCCESS":{let a=i.get(t.viewPortId);a&&(this.mapClientToServerViewport.delete(a.clientViewportId),i.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case Ze:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(n)}break;case ke:case \$e:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),L&&x(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case Ge:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);if(l){this.postMessageToClient(l);let m=a.currentData();L&&x(\`Enable Response (ServerProxy to Client): \${JSON.stringify(l)}\`),a.size===0?L&&x("Viewport Enabled but size 0, resend to server"):(this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:m,size:a.size,type:"viewport-update"}),L&&x(\`Enable Response (ServerProxy to Client): send size \${a.size} \${m.length} rows from cache\`))}}}break;case Xe:{let a=De(t.rows);for(let[l,m]of Object.entries(a)){let w=i.get(l);w?w.updateRows(m):ie==null||ie(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case Ae:{let a=this.viewports.get(t.viewPortId);if(a){let{from:l,to:m}=t;a.completeOperation(n,l,m)}}break;case Je:case Fe:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:m,parentColumnName:w}=t,O=a.completeOperation(n,m,l.clientViewportId,w);O&&this.postMessageToClient(O)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(n);l&&this.postMessageToClient(l)}}break;case"VP_VISUAL_LINKS_RESP":{let a=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(a.length&&l){let m=qt(a,this.viewports),[w,O]=l.setLinks(m);if(this.postMessageToClient(w),O){let{link:le,parentClientVpId:rt}=O,ce=b(),pe=this.mapClientToServerViewport.get(rt);if(pe){let st=l.createLink(ce,le.fromColumn,pe,le.toColumn);this.sendMessageToServer(st,ce)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let a=this.viewports.get(t.vpId);if(a){let l=a.setMenu(t.menu);this.postMessageToClient(l)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:n,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:n,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_REJ":{console.log("send menu error back to client");let{error:a,rpcName:l,vpId:m}=t,w=this.viewports.get(m);w&&this.postMessageToClient({clientViewportId:w.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:n});break}case"VIEW_PORT_MENU_RESP":if(Oe(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(m=>{let w=te(m);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:w},tableAlreadyOpen:this.isTableOpen(a.table),requestId:n})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Wt,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:n})}break;case re:{let{method:a,result:l}=t;this.postMessageToClient({type:re,method:a,result:l,requestId:n})}break;case"ERROR":_(t.msg);break;default:Ut&&S(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=te(e),this.cachedTableSchemas.set(r,o)),o}isTableOpen(e){if(e){let t=e.table;for(let n of this.viewports.values())if(!n.suspended&&n.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let n=this.viewports.get(t.parentVpId);return n&&!n.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let n=e.getClientRows();if(n!==K){let[r,o]=n,i=e.getNewRowCount();(i!==void 0||r&&r.length>0)&&(L&&x(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:i,type:"viewport-update"}))}}})}};var I,{info:ae,infoEnabled:ue}=R("worker");async function Gt(s,e,t,n,r,o,i){let u=await Me(s,e,c=>{Le(c)?(console.log("post connection metrics"),postMessage({type:"connection-metrics",messages:c})):Pe(c)?(r(c),c.status==="reconnected"&&I.reconnect()):I.handleMessageFromServer(c)},o,i);I=new j(u,c=>Bt(c)),u.requiresLogin&&await I.login(t,n)}function Bt(s){postMessage(s)}var Kt=async({data:s})=>{switch(s.type){case"connect":await Gt(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":ue&&ae(\`client subscribe: \${JSON.stringify(s)}\`),I.subscribe(s);break;case"unsubscribe":ue&&ae(\`client unsubscribe: \${JSON.stringify(s)}\`),I.unsubscribe(s.viewport);break;default:ue&&ae(\`client message: \${JSON.stringify(s)}\`),I.handleMessageFromClient(s)}};self.addEventListener("message",Kt);postMessage({type:"ready"}); + \`); + throw Error(\`KeySet, no key found for rowIndex \${rowIndex}\`); + } + return key; + } + toDebugString() { + return Array.from(this.keys.entries()).map((k, v) => \`\${k}=>\${v}\`).join(","); + } +}; + +// ../vuu-utils/src/row-utils.ts +var { IDX } = metadataKeys; + +// ../vuu-utils/src/selection-utils.ts +var { SELECTED } = metadataKeys; +var RowSelected = { + False: 0, + True: 1, + First: 2, + Last: 4 +}; +var rangeIncludes = (range, index) => index >= range[0] && index <= range[1]; +var SINGLE_SELECTED_ROW = RowSelected.True + RowSelected.First + RowSelected.Last; +var FIRST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.First; +var LAST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.Last; +var getSelectionStatus = (selected, itemIndex) => { + for (const item of selected) { + if (typeof item === "number") { + if (item === itemIndex) { + return SINGLE_SELECTED_ROW; + } + } else if (rangeIncludes(item, itemIndex)) { + if (itemIndex === item[0]) { + return FIRST_SELECTED_ROW_OF_BLOCK; + } else if (itemIndex === item[1]) { + return LAST_SELECTED_ROW_OF_BLOCK; + } else { + return RowSelected.True; + } + } + } + return RowSelected.False; +}; +var expandSelection = (selected) => { + if (selected.every((selectedItem) => typeof selectedItem === "number")) { + return selected; + } + const expandedSelected = []; + for (const selectedItem of selected) { + if (typeof selectedItem === "number") { + expandedSelected.push(selectedItem); + } else { + for (let i = selectedItem[0]; i <= selectedItem[1]; i++) { + expandedSelected.push(i); + } + } + } + return expandedSelected; +}; + +// ../../node_modules/html-to-image/es/util.js +var uuid = (() => { + let counter = 0; + const random = () => ( + // eslint-disable-next-line no-bitwise + \`0000\${(Math.random() * 36 ** 4 << 0).toString(36)}\`.slice(-4) + ); + return () => { + counter += 1; + return \`u\${random()}\${counter}\`; + }; +})(); + +// src/websocket-connection.ts +var { debug: debug2, debugEnabled: debugEnabled2, error, info, infoEnabled, warn } = logger( + "websocket-connection" +); +var WS = "ws"; +var isWebsocketUrl = (url) => url.startsWith(WS + "://") || url.startsWith(WS + "s://"); +var connectionAttemptStatus = {}; +var setWebsocket = Symbol("setWebsocket"); +var connectionCallback = Symbol("connectionCallback"); +async function connect(connectionString, protocol, callback, retryLimitDisconnect = 10, retryLimitStartup = 5) { + connectionAttemptStatus[connectionString] = { + status: "connecting", + connect: { + allowed: retryLimitStartup, + remaining: retryLimitStartup + }, + reconnect: { + allowed: retryLimitDisconnect, + remaining: retryLimitDisconnect + } + }; + return makeConnection(connectionString, protocol, callback); +} +async function reconnect(connection) { + throw Error("connection broken"); +} +async function makeConnection(url, protocol, callback, connection) { + const { + status: currentStatus, + connect: connectStatus, + reconnect: reconnectStatus + } = connectionAttemptStatus[url]; + const trackedStatus = currentStatus === "connecting" ? connectStatus : reconnectStatus; + try { + callback({ type: "connection-status", status: "connecting" }); + const reconnecting = typeof connection !== "undefined"; + const ws = await createWebsocket(url, protocol); + console.info( + "%c\u26A1 %cconnected", + "font-size: 24px;color: green;font-weight: bold;", + "color:green; font-size: 14px;" + ); + if (connection !== void 0) { + connection[setWebsocket](ws); + } + const websocketConnection = connection != null ? connection : new WebsocketConnection(ws, url, protocol, callback); + const status = reconnecting ? "reconnected" : "connection-open-awaiting-session"; + callback({ type: "connection-status", status }); + websocketConnection.status = status; + trackedStatus.remaining = trackedStatus.allowed; + return websocketConnection; + } catch (err) { + const retry = --trackedStatus.remaining > 0; + callback({ + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry + }); + if (retry) { + return makeConnectionIn(url, protocol, callback, connection, 2e3); + } else { + throw Error("Failed to establish connection"); + } + } +} +var makeConnectionIn = (url, protocol, callback, connection, delay) => new Promise((resolve) => { + setTimeout(() => { + resolve(makeConnection(url, protocol, callback, connection)); + }, delay); +}); +var createWebsocket = (connectionString, protocol) => new Promise((resolve, reject) => { + const websocketUrl = isWebsocketUrl(connectionString) ? connectionString : \`wss://\${connectionString}\`; + if (infoEnabled && protocol !== void 0) { + info(\`WebSocket Protocol \${protocol == null ? void 0 : protocol.toString()}\`); + } + const ws = new WebSocket(websocketUrl, protocol); + ws.onopen = () => resolve(ws); + ws.onerror = (evt) => reject(evt); +}); +var closeWarn = () => { + warn == null ? void 0 : warn(\`Connection cannot be closed, socket not yet opened\`); +}; +var sendWarn = (msg) => { + warn == null ? void 0 : warn(\`Message cannot be sent, socket closed \${msg.body.type}\`); +}; +var parseMessage = (message) => { + try { + return JSON.parse(message); + } catch (e) { + throw Error(\`Error parsing JSON response from server \${message}\`); + } +}; +var WebsocketConnection = class { + constructor(ws, url, protocol, callback) { + this.close = closeWarn; + this.requiresLogin = true; + this.send = sendWarn; + this.status = "ready"; + this.messagesCount = 0; + this.connectionMetricsInterval = null; + this.handleWebsocketMessage = (evt) => { + const vuuMessageFromServer = parseMessage(evt.data); + this.messagesCount += 1; + if (true) { + if (debugEnabled2 && vuuMessageFromServer.body.type !== "HB") { + debug2 == null ? void 0 : debug2(\`<<< \${vuuMessageFromServer.body.type}\`); + } + } + this[connectionCallback](vuuMessageFromServer); + }; + this.url = url; + this.protocol = protocol; + this[connectionCallback] = callback; + this[setWebsocket](ws); + } + reconnect() { + reconnect(this); + } + [(connectionCallback, setWebsocket)](ws) { + const callback = this[connectionCallback]; + ws.onmessage = (evt) => { + this.status = "connected"; + ws.onmessage = this.handleWebsocketMessage; + this.handleWebsocketMessage(evt); + }; + this.connectionMetricsInterval = setInterval(() => { + callback({ + type: "connection-metrics", + messagesLength: this.messagesCount + }); + this.messagesCount = 0; + }, 2e3); + ws.onerror = () => { + error(\`\u26A1 connection error\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "error" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status === "connection-open-awaiting-session") { + error( + \`Websocket connection lost before Vuu session established, check websocket configuration\` + ); + } else if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + ws.onclose = () => { + info == null ? void 0 : info(\`\u26A1 connection close\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "close" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + const send = (msg) => { + if (true) { + if (debugEnabled2 && msg.body.type !== "HB_RESP") { + debug2 == null ? void 0 : debug2(\`>>> \${msg.body.type}\`); + } + } + ws.send(JSON.stringify(msg)); + }; + const queue = (msg) => { + info == null ? void 0 : info(\`TODO queue message until websocket reconnected \${msg.body.type}\`); + }; + this.send = send; + this.close = () => { + this.status = "closed"; + ws.close(); + this.close = closeWarn; + this.send = sendWarn; + info == null ? void 0 : info("close websocket"); + }; + } +}; + +// src/message-utils.ts +var MENU_RPC_TYPES = [ + "VIEW_PORT_MENUS_SELECT_RPC", + "VIEW_PORT_MENU_TABLE_RPC", + "VIEW_PORT_MENU_ROW_RPC", + "VIEW_PORT_MENU_CELL_RPC", + "VP_EDIT_CELL_RPC", + "VP_EDIT_ROW_RPC", + "VP_EDIT_ADD_ROW_RPC", + "VP_EDIT_DELETE_CELL_RPC", + "VP_EDIT_DELETE_ROW_RPC", + "VP_EDIT_SUBMIT_FORM_RPC" +]; +var isVuuMenuRpcRequest = (message) => MENU_RPC_TYPES.includes(message["type"]); +var stripRequestId = ({ + requestId, + ...rest +}) => [requestId, rest]; +var getFirstAndLastRows = (rows) => { + let firstRow = rows.at(0); + if (firstRow.updateType === "SIZE") { + if (rows.length === 1) { + return rows; + } else { + firstRow = rows.at(1); + } + } + const lastRow = rows.at(-1); + return [firstRow, lastRow]; +}; +var groupRowsByViewport = (rows) => { + const result = {}; + for (const row of rows) { + const rowsForViewport = result[row.viewPortId] || (result[row.viewPortId] = []); + rowsForViewport.push(row); + } + return result; +}; +var createSchemaFromTableMetadata = ({ + columns, + dataTypes, + key, + table +}) => { + return { + table, + columns: columns.map((col, idx) => ({ + name: col, + serverDataType: dataTypes[idx] + })), + key + }; +}; + +// src/vuuUIMessageTypes.ts +var isConnectionStatusMessage = (msg) => msg.type === "connection-status"; +var isConnectionQualityMetrics = (msg) => msg.type === "connection-metrics"; +var isViewporttMessage = (msg) => "viewport" in msg; +var isSessionTableActionMessage = (messageBody) => messageBody.type === "VIEW_PORT_MENU_RESP" && messageBody.action !== null && isSessionTable(messageBody.action.table); +var isSessionTable = (table) => { + if (table !== null && typeof table === "object" && "table" in table && "module" in table) { + return table.table.startsWith("session"); + } + return false; +}; + +// src/server-proxy/messages.ts +var CHANGE_VP_SUCCESS = "CHANGE_VP_SUCCESS"; +var CHANGE_VP_RANGE_SUCCESS = "CHANGE_VP_RANGE_SUCCESS"; +var CLOSE_TREE_NODE = "CLOSE_TREE_NODE"; +var CLOSE_TREE_SUCCESS = "CLOSE_TREE_SUCCESS"; +var CREATE_VP = "CREATE_VP"; +var DISABLE_VP = "DISABLE_VP"; +var DISABLE_VP_SUCCESS = "DISABLE_VP_SUCCESS"; +var ENABLE_VP = "ENABLE_VP"; +var ENABLE_VP_SUCCESS = "ENABLE_VP_SUCCESS"; +var GET_VP_VISUAL_LINKS = "GET_VP_VISUAL_LINKS"; +var GET_VIEW_PORT_MENUS = "GET_VIEW_PORT_MENUS"; +var HB = "HB"; +var HB_RESP = "HB_RESP"; +var LOGIN = "LOGIN"; +var OPEN_TREE_NODE = "OPEN_TREE_NODE"; +var OPEN_TREE_SUCCESS = "OPEN_TREE_SUCCESS"; +var REMOVE_VP = "REMOVE_VP"; +var RPC_RESP = "RPC_RESP"; +var SET_SELECTION_SUCCESS = "SET_SELECTION_SUCCESS"; +var TABLE_ROW = "TABLE_ROW"; + +// src/server-proxy/rpc-services.ts +var getRpcServiceModule = (service) => { + switch (service) { + case "TypeAheadRpcHandler": + return "TYPEAHEAD"; + default: + return "SIMUL"; + } +}; + +// src/server-proxy/array-backed-moving-window.ts +var EMPTY_ARRAY = []; +var log = logger("array-backed-moving-window"); +function dataIsUnchanged(newRow, existingRow) { + if (!existingRow) { + return false; + } + if (existingRow.data.length !== newRow.data.length) { + return false; + } + if (existingRow.sel !== newRow.sel) { + return false; + } + for (let i = 0; i < existingRow.data.length; i++) { + if (existingRow.data[i] !== newRow.data[i]) { + return false; + } + } + return true; +} +var _range; +var ArrayBackedMovingWindow = class { + // Note, the buffer is already accounted for in the range passed in here + constructor({ from: clientFrom, to: clientTo }, { from, to }, bufferSize) { + __privateAdd(this, _range, void 0); + this.setRowCount = (rowCount) => { + var _a; + (_a = log.info) == null ? void 0 : _a.call(log, \`setRowCount \${rowCount}\`); + if (rowCount < this.internalData.length) { + this.internalData.length = rowCount; + } + if (rowCount < this.rowCount) { + this.rowsWithinRange = 0; + const end = Math.min(rowCount, this.clientRange.to); + for (let i = this.clientRange.from; i < end; i++) { + const rowIndex = i - __privateGet(this, _range).from; + if (this.internalData[rowIndex] !== void 0) { + this.rowsWithinRange += 1; + } + } + } + this.rowCount = rowCount; + }; + this.bufferBreakout = (from, to) => { + const bufferPerimeter = this.bufferSize * 0.25; + if (__privateGet(this, _range).to - to < bufferPerimeter) { + return true; + } else if (__privateGet(this, _range).from > 0 && from - __privateGet(this, _range).from < bufferPerimeter) { + return true; + } else { + return false; + } + }; + this.bufferSize = bufferSize; + this.clientRange = new WindowRange(clientFrom, clientTo); + __privateSet(this, _range, new WindowRange(from, to)); + this.internalData = new Array(bufferSize); + this.rowsWithinRange = 0; + this.rowCount = 0; + } + get range() { + return __privateGet(this, _range); + } + // TODO we shpuld probably have a hasAllClientRowsWithinRange + get hasAllRowsWithinRange() { + return this.rowsWithinRange === this.clientRange.to - this.clientRange.from || // this.rowsWithinRange === this.range.to - this.range.from || + this.rowCount > 0 && this.clientRange.from + this.rowsWithinRange === this.rowCount; + } + // Check to see if set of rows is outside the current viewport range, indicating + // that veiwport is being scrolled quickly and server is not able to keep up. + outOfRange(firstIndex, lastIndex) { + const { from, to } = this.range; + if (lastIndex < from) { + return true; + } + if (firstIndex >= to) { + return true; + } + } + setAtIndex(row) { + const { rowIndex: index } = row; + const internalIndex = index - __privateGet(this, _range).from; + if (dataIsUnchanged(row, this.internalData[internalIndex])) { + return false; + } + const isWithinClientRange = this.isWithinClientRange(index); + if (isWithinClientRange || this.isWithinRange(index)) { + if (!this.internalData[internalIndex] && isWithinClientRange) { + this.rowsWithinRange += 1; + } + this.internalData[internalIndex] = row; + } + return isWithinClientRange; + } + getAtIndex(index) { + return __privateGet(this, _range).isWithin(index) && this.internalData[index - __privateGet(this, _range).from] != null ? this.internalData[index - __privateGet(this, _range).from] : void 0; + } + isWithinRange(index) { + return __privateGet(this, _range).isWithin(index); + } + isWithinClientRange(index) { + return this.clientRange.isWithin(index); + } + // Returns [false] or [serverDataRequired, clientRows, holdingRows] + setClientRange(from, to) { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, \`setClientRange \${from} - \${to}\`); + const currentFrom = this.clientRange.from; + const currentTo = Math.min(this.clientRange.to, this.rowCount); + if (from === currentFrom && to === currentTo) { + return [ + false, + EMPTY_ARRAY + /*, EMPTY_ARRAY*/ + ]; + } + const originalRange = this.clientRange.copy(); + this.clientRange.from = from; + this.clientRange.to = to; + this.rowsWithinRange = 0; + for (let i = from; i < to; i++) { + const internalIndex = i - __privateGet(this, _range).from; + if (this.internalData[internalIndex]) { + this.rowsWithinRange += 1; + } + } + let clientRows = EMPTY_ARRAY; + const offset = __privateGet(this, _range).from; + if (this.hasAllRowsWithinRange) { + if (to > originalRange.to) { + const start = Math.max(from, originalRange.to); + clientRows = this.internalData.slice(start - offset, to - offset); + } else { + const end = Math.min(originalRange.from, to); + clientRows = this.internalData.slice(from - offset, end - offset); + } + } + const serverDataRequired = this.bufferBreakout(from, to); + return [serverDataRequired, clientRows]; + } + setRange(from, to) { + var _a, _b; + if (from !== __privateGet(this, _range).from || to !== __privateGet(this, _range).to) { + (_a = log.debug) == null ? void 0 : _a.call(log, \`setRange \${from} - \${to}\`); + const [overlapFrom, overlapTo] = __privateGet(this, _range).overlap(from, to); + const newData = new Array(to - from); + this.rowsWithinRange = 0; + for (let i = overlapFrom; i < overlapTo; i++) { + const data = this.getAtIndex(i); + if (data) { + const index = i - from; + newData[index] = data; + if (this.isWithinClientRange(i)) { + this.rowsWithinRange += 1; + } + } + } + this.internalData = newData; + __privateGet(this, _range).from = from; + __privateGet(this, _range).to = to; + } else { + (_b = log.debug) == null ? void 0 : _b.call(log, \`setRange \${from} - \${to} IGNORED because not changed\`); + } + } + //TODO temp + get data() { + return this.internalData; + } + getData() { + var _a; + const { from, to } = __privateGet(this, _range); + const { from: clientFrom, to: clientTo } = this.clientRange; + const startOffset = Math.max(0, clientFrom - from); + const endOffset = Math.min( + to - from, + to, + clientTo - from, + (_a = this.rowCount) != null ? _a : to + ); + return this.internalData.slice(startOffset, endOffset); + } + clear() { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, "clear"); + this.internalData.length = 0; + this.rowsWithinRange = 0; + this.setRowCount(0); + } + // used only for debugging + getCurrentDataRange() { + const rows = this.internalData; + const len = rows.length; + let [firstRow] = this.internalData; + let lastRow = this.internalData[len - 1]; + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + for (let i = 0; i < len; i++) { + if (rows[i] !== void 0) { + firstRow = rows[i]; + break; + } + } + for (let i = len - 1; i >= 0; i--) { + if (rows[i] !== void 0) { + lastRow = rows[i]; + break; + } + } + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + return [-1, -1]; + } + } + } +}; +_range = new WeakMap(); + +// src/server-proxy/viewport.ts +var EMPTY_GROUPBY = []; +var { debug: debug3, debugEnabled: debugEnabled3, error: error2, info: info2, infoEnabled: infoEnabled2, warn: warn2 } = logger("viewport"); +var isLeafUpdate = ({ rowKey, updateType }) => updateType === "U" && !rowKey.startsWith("\$root"); +var NO_DATA_UPDATE = [ + void 0, + void 0 +]; +var NO_UPDATE_STATUS = { + count: 0, + mode: void 0, + size: 0, + ts: 0 +}; +var Viewport = class { + constructor({ + aggregations, + bufferSize = 50, + columns, + filter, + groupBy = [], + table, + range, + sort, + title, + viewport, + visualLink + }, postMessageToClient) { + /** batchMode is irrelevant for Vuu Table, it was introduced to try and improve rendering performance of AgGrid */ + this.batchMode = true; + this.hasUpdates = false; + this.pendingUpdates = []; + this.pendingOperations = /* @__PURE__ */ new Map(); + this.pendingRangeRequests = []; + this.rowCountChanged = false; + this.selectedRows = []; + this.useBatchMode = true; + this.lastUpdateStatus = NO_UPDATE_STATUS; + this.updateThrottleTimer = void 0; + this.rangeMonitor = new RangeMonitor("ViewPort"); + this.disabled = false; + this.isTree = false; + // TODO roll disabled/suspended into status + this.status = ""; + this.suspended = false; + this.suspendTimer = null; + // Records SIZE only updates + this.setLastSizeOnlyUpdateSize = (size) => { + this.lastUpdateStatus.size = size; + }; + this.setLastUpdate = (mode) => { + const { ts: lastTS, mode: lastMode } = this.lastUpdateStatus; + let elapsedTime = 0; + if (lastMode === mode) { + const ts = Date.now(); + this.lastUpdateStatus.count += 1; + this.lastUpdateStatus.ts = ts; + elapsedTime = lastTS === 0 ? 0 : ts - lastTS; + } else { + this.lastUpdateStatus.count = 1; + this.lastUpdateStatus.ts = 0; + elapsedTime = 0; + } + this.lastUpdateStatus.mode = mode; + return elapsedTime; + }; + this.rangeRequestAlreadyPending = (range) => { + const { bufferSize } = this; + const bufferThreshold = bufferSize * 0.25; + let { from: stillPendingFrom } = range; + for (const { from, to } of this.pendingRangeRequests) { + if (stillPendingFrom >= from && stillPendingFrom < to) { + if (range.to + bufferThreshold <= to) { + return true; + } else { + stillPendingFrom = to; + } + } + } + return false; + }; + this.sendThrottledSizeMessage = () => { + this.updateThrottleTimer = void 0; + this.lastUpdateStatus.count = 3; + this.postMessageToClient({ + clientViewportId: this.clientViewportId, + mode: "size-only", + size: this.lastUpdateStatus.size, + type: "viewport-update" + }); + }; + // If we are receiving multiple SIZE updates but no data, table is loading rows + // outside of our viewport. We can safely throttle these requests. Doing so will + // alleviate pressure on UI DataTable. + this.shouldThrottleMessage = (mode) => { + const elapsedTime = this.setLastUpdate(mode); + return mode === "size-only" && elapsedTime > 0 && elapsedTime < 500 && this.lastUpdateStatus.count > 3; + }; + this.throttleMessage = (mode) => { + if (this.shouldThrottleMessage(mode)) { + info2 == null ? void 0 : info2("throttling updates setTimeout to 2000"); + if (this.updateThrottleTimer === void 0) { + this.updateThrottleTimer = setTimeout( + this.sendThrottledSizeMessage, + 2e3 + ); + } + return true; + } else if (this.updateThrottleTimer !== void 0) { + clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + return false; + }; + this.getNewRowCount = () => { + if (this.rowCountChanged && this.dataWindow) { + this.rowCountChanged = false; + return this.dataWindow.rowCount; + } + }; + this.aggregations = aggregations; + this.bufferSize = bufferSize; + this.clientRange = range; + this.clientViewportId = viewport; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.keys = new KeySet(range); + this.pendingLinkedParent = visualLink; + this.table = table; + this.sort = sort; + this.title = title; + infoEnabled2 && (info2 == null ? void 0 : info2( + \`constructor #\${viewport} \${table.table} bufferSize=\${bufferSize}\` + )); + this.dataWindow = new ArrayBackedMovingWindow( + this.clientRange, + range, + this.bufferSize + ); + this.postMessageToClient = postMessageToClient; + } + get hasUpdatesToProcess() { + if (this.suspended) { + return false; + } + return this.rowCountChanged || this.hasUpdates; + } + get size() { + var _a; + return (_a = this.dataWindow.rowCount) != null ? _a : 0; + } + subscribe() { + const { filter } = this.filter; + this.status = this.status === "subscribed" ? "resubscribing" : "subscribing"; + return { + type: CREATE_VP, + table: this.table, + range: getFullRange(this.clientRange, this.bufferSize), + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { filter } + }; + } + handleSubscribed({ + viewPortId, + aggregations, + columns, + filterSpec: filter, + range, + sort, + groupBy + }, tableSchema) { + this.serverViewportId = viewPortId; + this.status = "subscribed"; + this.aggregations = aggregations; + this.columns = columns; + this.groupBy = groupBy; + this.isTree = groupBy && groupBy.length > 0; + this.dataWindow.setRange(range.from, range.to); + return { + aggregations, + type: "subscribed", + clientViewportId: this.clientViewportId, + columns, + filter, + groupBy, + range, + sort, + tableSchema + }; + } + awaitOperation(requestId, msg) { + this.pendingOperations.set(requestId, msg); + } + // Return a message if we need to communicate this to client UI + completeOperation(requestId, ...params) { + var _a; + const { clientViewportId, pendingOperations } = this; + const pendingOperation = pendingOperations.get(requestId); + if (!pendingOperation) { + error2("no matching operation found to complete"); + return; + } + const { type } = pendingOperation; + info2 == null ? void 0 : info2(\`completeOperation \${type}\`); + pendingOperations.delete(requestId); + if (type === "CHANGE_VP_RANGE") { + const [from, to] = params; + (_a = this.dataWindow) == null ? void 0 : _a.setRange(from, to); + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const pendingRangeRequest = this.pendingRangeRequests[i]; + if (pendingRangeRequest.requestId === requestId) { + pendingRangeRequest.acked = true; + break; + } else { + warn2 == null ? void 0 : warn2("range requests sent faster than they are being ACKed"); + } + } + } else if (type === "config") { + const { aggregations, columns, filter, groupBy, sort } = pendingOperation.data; + this.aggregations = aggregations; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.sort = sort; + if (groupBy.length > 0) { + this.isTree = true; + } else if (this.isTree) { + this.isTree = false; + } + debug3 == null ? void 0 : debug3(\`config change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + config: pendingOperation.data + }; + } else if (type === "groupBy") { + this.isTree = pendingOperation.data.length > 0; + this.groupBy = pendingOperation.data; + debug3 == null ? void 0 : debug3(\`groupBy change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + groupBy: pendingOperation.data + }; + } else if (type === "columns") { + this.columns = pendingOperation.data; + return { + clientViewportId, + type, + columns: pendingOperation.data + }; + } else if (type === "filter") { + this.filter = pendingOperation.data; + return { + clientViewportId, + type, + filter: pendingOperation.data + }; + } else if (type === "aggregate") { + this.aggregations = pendingOperation.data; + return { + clientViewportId, + type: "aggregate", + aggregations: this.aggregations + }; + } else if (type === "sort") { + this.sort = pendingOperation.data; + return { + clientViewportId, + type, + sort: this.sort + }; + } else if (type === "selection") { + } else if (type === "disable") { + this.disabled = true; + return { + type: "disabled", + clientViewportId + }; + } else if (type === "enable") { + this.disabled = false; + return { + type: "enabled", + clientViewportId + }; + } else if (type === "CREATE_VISUAL_LINK") { + const [colName, parentViewportId, parentColName] = params; + this.linkedParent = { + colName, + parentViewportId, + parentColName + }; + this.pendingLinkedParent = void 0; + return { + type: "vuu-link-created", + clientViewportId, + colName, + parentViewportId, + parentColName + }; + } else if (type === "REMOVE_VISUAL_LINK") { + this.linkedParent = void 0; + return { + type: "vuu-link-removed", + clientViewportId + }; + } + } + // TODO when a range request arrives, consider the viewport to be scrolling + // until data arrives and we have the full range. + // When not scrolling, any server data is an update + // When scrolling, we are in batch mode + rangeRequest(requestId, range) { + if (debugEnabled3) { + this.rangeMonitor.set(range); + } + const type = "CHANGE_VP_RANGE"; + if (this.dataWindow) { + const [serverDataRequired, clientRows] = this.dataWindow.setClientRange( + range.from, + range.to + ); + let debounceRequest; + const maxRange = this.dataWindow.rowCount || void 0; + const serverRequest = serverDataRequired && !this.rangeRequestAlreadyPending(range) ? { + type, + viewPortId: this.serverViewportId, + ...getFullRange(range, this.bufferSize, maxRange) + } : null; + if (serverRequest) { + debugEnabled3 && (debug3 == null ? void 0 : debug3( + \`create CHANGE_VP_RANGE: [\${serverRequest.from} - \${serverRequest.to}]\` + )); + this.awaitOperation(requestId, { type }); + const pendingRequest = this.pendingRangeRequests.at(-1); + if (pendingRequest) { + if (pendingRequest.acked) { + console.warn("Range Request before previous request is filled"); + } else { + const { from, to } = pendingRequest; + if (this.dataWindow.outOfRange(from, to)) { + debounceRequest = { + clientViewportId: this.clientViewportId, + type: "debounce-begin" + }; + } else { + warn2 == null ? void 0 : warn2("Range Request before previous request is acked"); + } + } + } + this.pendingRangeRequests.push({ ...serverRequest, requestId }); + if (this.useBatchMode) { + this.batchMode = true; + } + } else if (clientRows.length > 0) { + this.batchMode = false; + } + this.keys.reset(this.dataWindow.clientRange); + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (clientRows.length) { + return [ + serverRequest, + clientRows.map((row) => { + return toClient(row, this.keys, this.selectedRows); + }) + ]; + } else if (debounceRequest) { + return [serverRequest, void 0, debounceRequest]; + } else { + return [serverRequest]; + } + } else { + return [null]; + } + } + setLinks(links) { + this.links = links; + return [ + { + type: "vuu-links", + links, + clientViewportId: this.clientViewportId + }, + this.pendingLinkedParent + ]; + } + setMenu(menu) { + return { + type: "vuu-menu", + menu, + clientViewportId: this.clientViewportId + }; + } + openTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: OPEN_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + closeTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: CLOSE_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + createLink(requestId, colName, parentVpId, parentColumnName) { + const message = { + type: "CREATE_VISUAL_LINK", + parentVpId, + childVpId: this.serverViewportId, + parentColumnName, + childColumnName: colName + }; + this.awaitOperation(requestId, message); + if (this.useBatchMode) { + this.batchMode = true; + } + return message; + } + removeLink(requestId) { + const message = { + type: "REMOVE_VISUAL_LINK", + childVpId: this.serverViewportId + }; + this.awaitOperation(requestId, message); + return message; + } + suspend() { + this.suspended = true; + info2 == null ? void 0 : info2("suspend"); + } + resume() { + this.suspended = false; + if (debugEnabled3) { + debug3 == null ? void 0 : debug3(\`resume: \${this.currentData()}\`); + } + return this.currentData(); + } + currentData() { + const out = []; + if (this.dataWindow) { + const records = this.dataWindow.getData(); + const { keys } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + for (const row of records) { + if (row) { + out.push(toClient(row, keys, this.selectedRows)); + } + } + } + return out; + } + enable(requestId) { + this.awaitOperation(requestId, { type: "enable" }); + info2 == null ? void 0 : info2(\`enable: \${this.serverViewportId}\`); + return { + type: ENABLE_VP, + viewPortId: this.serverViewportId + }; + } + disable(requestId) { + this.awaitOperation(requestId, { type: "disable" }); + info2 == null ? void 0 : info2(\`disable: \${this.serverViewportId}\`); + this.suspended = false; + return { + type: DISABLE_VP, + viewPortId: this.serverViewportId + }; + } + columnRequest(requestId, columns) { + this.awaitOperation(requestId, { + type: "columns", + data: columns + }); + debug3 == null ? void 0 : debug3(\`columnRequest: \${columns}\`); + return this.createRequest({ columns }); + } + filterRequest(requestId, dataSourceFilter) { + this.awaitOperation(requestId, { + type: "filter", + data: dataSourceFilter + }); + if (this.useBatchMode) { + this.batchMode = true; + } + const { filter } = dataSourceFilter; + info2 == null ? void 0 : info2(\`filterRequest: \${filter}\`); + return this.createRequest({ filterSpec: { filter } }); + } + setConfig(requestId, config) { + this.awaitOperation(requestId, { type: "config", data: config }); + const { filter, ...remainingConfig } = config; + if (this.useBatchMode) { + this.batchMode = true; + } + debugEnabled3 ? debug3 == null ? void 0 : debug3(\`setConfig \${JSON.stringify(config)}\`) : info2 == null ? void 0 : info2(\`setConfig\`); + return this.createRequest( + { + ...remainingConfig, + filterSpec: typeof (filter == null ? void 0 : filter.filter) === "string" ? { + filter: filter.filter + } : { + filter: "" + } + }, + true + ); + } + aggregateRequest(requestId, aggregations) { + this.awaitOperation(requestId, { type: "aggregate", data: aggregations }); + info2 == null ? void 0 : info2(\`aggregateRequest: \${aggregations}\`); + return this.createRequest({ aggregations }); + } + sortRequest(requestId, sort) { + this.awaitOperation(requestId, { type: "sort", data: sort }); + info2 == null ? void 0 : info2(\`sortRequest: \${JSON.stringify(sort.sortDefs)}\`); + return this.createRequest({ sort }); + } + groupByRequest(requestId, groupBy = EMPTY_GROUPBY) { + var _a; + this.awaitOperation(requestId, { type: "groupBy", data: groupBy }); + if (this.useBatchMode) { + this.batchMode = true; + } + if (!this.isTree) { + (_a = this.dataWindow) == null ? void 0 : _a.clear(); + } + return this.createRequest({ groupBy }); + } + selectRequest(requestId, selected) { + this.selectedRows = selected; + this.awaitOperation(requestId, { type: "selection", data: selected }); + info2 == null ? void 0 : info2(\`selectRequest: \${selected}\`); + return { + type: "SET_SELECTION", + vpId: this.serverViewportId, + selection: expandSelection(selected) + }; + } + removePendingRangeRequest(firstIndex, lastIndex) { + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const { from, to } = this.pendingRangeRequests[i]; + let isLast = true; + if (firstIndex >= from && firstIndex < to || lastIndex > from && lastIndex < to) { + if (!isLast) { + console.warn( + "removePendingRangeRequest TABLE_ROWS are not for latest request" + ); + } + this.pendingRangeRequests.splice(i, 1); + break; + } else { + isLast = false; + } + } + } + updateRows(rows) { + var _a, _b, _c; + const [firstRow, lastRow] = getFirstAndLastRows(rows); + if (firstRow && lastRow) { + this.removePendingRangeRequest(firstRow.rowIndex, lastRow.rowIndex); + } + if (rows.length === 1) { + if (firstRow.vpSize === 0 && this.disabled) { + debug3 == null ? void 0 : debug3( + \`ignore a SIZE=0 message on disabled viewport (\${rows.length} rows)\` + ); + return; + } else if (firstRow.updateType === "SIZE") { + this.setLastSizeOnlyUpdateSize(firstRow.vpSize); + } + } + for (const row of rows) { + if (this.isTree && isLeafUpdate(row)) { + continue; + } else { + if (row.updateType === "SIZE" || ((_a = this.dataWindow) == null ? void 0 : _a.rowCount) !== row.vpSize) { + (_b = this.dataWindow) == null ? void 0 : _b.setRowCount(row.vpSize); + this.rowCountChanged = true; + } + if (row.updateType === "U") { + if ((_c = this.dataWindow) == null ? void 0 : _c.setAtIndex(row)) { + this.hasUpdates = true; + if (!this.batchMode) { + this.pendingUpdates.push(row); + } + } + } + } + } + } + // This is called only after new data has been received from server - data + // returned direcly from buffer does not use this. + getClientRows() { + let out = void 0; + let mode = "size-only"; + if (!this.hasUpdates && !this.rowCountChanged) { + return NO_DATA_UPDATE; + } + if (this.hasUpdates) { + const { keys, selectedRows } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (this.updateThrottleTimer) { + self.clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + if (this.pendingUpdates.length > 0) { + out = []; + mode = "update"; + for (const row of this.pendingUpdates) { + out.push(toClient(row, keys, selectedRows)); + } + this.pendingUpdates.length = 0; + } else { + const records = this.dataWindow.getData(); + if (this.dataWindow.hasAllRowsWithinRange) { + out = []; + mode = "batch"; + for (const row of records) { + out.push(toClient(row, keys, selectedRows)); + } + this.batchMode = false; + } + } + this.hasUpdates = false; + } + if (this.throttleMessage(mode)) { + return NO_DATA_UPDATE; + } else { + return [out, mode]; + } + } + createRequest(params, overWrite = false) { + if (overWrite) { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + ...params + }; + } else { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { + filter: this.filter.filter + }, + ...params + }; + } + } +}; +var toClientRow = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + return [ + rowIndex, + keys.keyFor(rowIndex), + true, + false, + 0, + 0, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(data); +}; +var toClientRowTree = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + const [depth, isExpanded, , isLeaf, , count, ...rest] = data; + return [ + rowIndex, + keys.keyFor(rowIndex), + isLeaf, + isExpanded, + depth, + count, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(rest); +}; + +// src/server-proxy/server-proxy.ts +var _requestId = 1; +var { debug: debug4, debugEnabled: debugEnabled4, error: error3, info: info3, infoEnabled: infoEnabled3, warn: warn3 } = logger("server-proxy"); +var nextRequestId = () => \`\${_requestId++}\`; +var DEFAULT_OPTIONS = {}; +var isActiveViewport = (viewPort) => viewPort.disabled !== true && viewPort.suspended !== true; +var NO_ACTION = { + type: "NO_ACTION" +}; +var addTitleToLinks = (links, serverViewportId, label) => links.map( + (link) => link.parentVpId === serverViewportId ? { ...link, label } : link +); +function addLabelsToLinks(links, viewports) { + return links.map((linkDescriptor) => { + const { parentVpId } = linkDescriptor; + const viewport = viewports.get(parentVpId); + if (viewport) { + return { + ...linkDescriptor, + parentClientVpId: viewport.clientViewportId, + label: viewport.title + }; + } else { + throw Error("addLabelsToLinks viewport not found"); + } + }); +} +var ServerProxy = class { + constructor(connection, callback) { + this.authToken = ""; + this.user = "user"; + this.pendingRequests = /* @__PURE__ */ new Map(); + this.queuedRequests = []; + this.cachedTableMetaRequests = /* @__PURE__ */ new Map(); + this.cachedTableSchemas = /* @__PURE__ */ new Map(); + this.connection = connection; + this.postMessageToClient = callback; + this.viewports = /* @__PURE__ */ new Map(); + this.mapClientToServerViewport = /* @__PURE__ */ new Map(); + } + async reconnect() { + await this.login(this.authToken); + const [activeViewports, inactiveViewports] = partition( + Array.from(this.viewports.values()), + isActiveViewport + ); + this.viewports.clear(); + this.mapClientToServerViewport.clear(); + const reconnectViewports = (viewports) => { + viewports.forEach((viewport) => { + const { clientViewportId } = viewport; + this.viewports.set(clientViewportId, viewport); + this.sendMessageToServer(viewport.subscribe(), clientViewportId); + }); + }; + reconnectViewports(activeViewports); + setTimeout(() => { + reconnectViewports(inactiveViewports); + }, 2e3); + } + async login(authToken, user = "user") { + if (authToken) { + this.authToken = authToken; + this.user = user; + return new Promise((resolve, reject) => { + this.sendMessageToServer( + { type: LOGIN, token: this.authToken, user }, + "" + ); + this.pendingLogin = { resolve, reject }; + }); + } else if (this.authToken === "") { + error3("login, cannot login until auth token has been obtained"); + } + } + subscribe(message) { + if (!this.mapClientToServerViewport.has(message.viewport)) { + const pendingTableSchema = this.getTableMeta(message.table); + const viewport = new Viewport(message, this.postMessageToClient); + this.viewports.set(message.viewport, viewport); + const pendingSubscription = this.awaitResponseToMessage( + viewport.subscribe(), + message.viewport + ); + const awaitPendingReponses = Promise.all([ + pendingSubscription, + pendingTableSchema + ]); + awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { + const { viewPortId: serverViewportId } = subscribeResponse; + const { status: viewportStatus } = viewport; + if (message.viewport !== serverViewportId) { + this.viewports.delete(message.viewport); + this.viewports.set(serverViewportId, viewport); + } + this.mapClientToServerViewport.set(message.viewport, serverViewportId); + const clientResponse = viewport.handleSubscribed( + subscribeResponse, + tableSchema + ); + if (clientResponse) { + this.postMessageToClient(clientResponse); + if (debugEnabled4) { + debug4( + \`post DataSourceSubscribedMessage to client: \${JSON.stringify( + clientResponse + )}\` + ); + } + } + if (viewport.disabled) { + this.disableViewport(viewport); + } + if (viewportStatus === "subscribing" && // A session table will never have Visual Links, nor Context Menus + !isSessionTable(viewport.table)) { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId: serverViewportId + }); + this.sendMessageToServer({ + type: GET_VIEW_PORT_MENUS, + vpId: serverViewportId + }); + Array.from(this.viewports.entries()).filter( + ([id, { disabled }]) => id !== serverViewportId && !disabled + ).forEach(([vpId]) => { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId + }); + }); + } + }); + } else { + error3(\`spurious subscribe call \${message.viewport}\`); + } + } + unsubscribe(clientViewportId) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + info3 == null ? void 0 : info3( + \`Unsubscribe Message (Client to Server): + \${serverViewportId}\` + ); + this.sendMessageToServer({ + type: REMOVE_VP, + viewPortId: serverViewportId + }); + } else { + error3( + \`failed to unsubscribe client viewport \${clientViewportId}, viewport not found\` + ); + } + } + getViewportForClient(clientViewportId, throws = true) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + const viewport = this.viewports.get(serverViewportId); + if (viewport) { + return viewport; + } else if (throws) { + throw Error( + \`Viewport not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } else if (this.viewports.has(clientViewportId)) { + return this.viewports.get(clientViewportId); + } else if (throws) { + throw Error( + \`Viewport server id not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } + /**********************************************************************/ + /* Handle messages from client */ + /**********************************************************************/ + setViewRange(viewport, message) { + const requestId = nextRequestId(); + const [serverRequest, rows, debounceRequest] = viewport.rangeRequest( + requestId, + message.range + ); + info3 == null ? void 0 : info3(\`setViewRange \${message.range.from} - \${message.range.to}\`); + if (serverRequest) { + if (true) { + info3 == null ? void 0 : info3( + \`CHANGE_VP_RANGE [\${message.range.from}-\${message.range.to}] => [\${serverRequest.from}-\${serverRequest.to}]\` + ); + } + this.sendIfReady( + serverRequest, + requestId, + viewport.status === "subscribed" + ); + } + if (rows) { + info3 == null ? void 0 : info3(\`setViewRange \${rows.length} rows returned from cache\`); + this.postMessageToClient({ + mode: "batch", + type: "viewport-update", + clientViewportId: viewport.clientViewportId, + rows + }); + } else if (debounceRequest) { + this.postMessageToClient(debounceRequest); + } + } + setConfig(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.setConfig(requestId, message.config); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + aggregate(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.aggregateRequest(requestId, message.aggregations); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + sort(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.sortRequest(requestId, message.sort); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + groupBy(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.groupByRequest(requestId, message.groupBy); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + filter(viewport, message) { + const requestId = nextRequestId(); + const { filter } = message; + const request = viewport.filterRequest(requestId, filter); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setColumns(viewport, message) { + const requestId = nextRequestId(); + const { columns } = message; + const request = viewport.columnRequest(requestId, columns); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setTitle(viewport, message) { + if (viewport) { + viewport.title = message.title; + this.updateTitleOnVisualLinks(viewport); + } + } + select(viewport, message) { + const requestId = nextRequestId(); + const { selected } = message; + const request = viewport.selectRequest(requestId, selected); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + disableViewport(viewport) { + const requestId = nextRequestId(); + const request = viewport.disable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + enableViewport(viewport) { + if (viewport.disabled) { + const requestId = nextRequestId(); + const request = viewport.enable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + } + suspendViewport(viewport) { + viewport.suspend(); + viewport.suspendTimer = setTimeout(() => { + info3 == null ? void 0 : info3("suspendTimer expired, escalate suspend to disable"); + this.disableViewport(viewport); + }, 3e3); + } + resumeViewport(viewport) { + if (viewport.suspendTimer) { + debug4 == null ? void 0 : debug4("clear suspend timer"); + clearTimeout(viewport.suspendTimer); + viewport.suspendTimer = null; + } + const rows = viewport.resume(); + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + type: "viewport-update" + }); + } + openTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.openTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + closeTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.closeTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + createLink(viewport, message) { + const { parentClientVpId, parentColumnName, childColumnName } = message; + const requestId = nextRequestId(); + const parentVpId = this.mapClientToServerViewport.get(parentClientVpId); + if (parentVpId) { + const request = viewport.createLink( + requestId, + childColumnName, + parentVpId, + parentColumnName + ); + this.sendMessageToServer(request, requestId); + } else { + error3("ServerProxy unable to create link, viewport not found"); + } + } + removeLink(viewport) { + const requestId = nextRequestId(); + const request = viewport.removeLink(requestId); + this.sendMessageToServer(request, requestId); + } + updateTitleOnVisualLinks(viewport) { + var _a; + const { serverViewportId, title } = viewport; + for (const vp of this.viewports.values()) { + if (vp !== viewport && vp.links && serverViewportId && title) { + if ((_a = vp.links) == null ? void 0 : _a.some((link) => link.parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + addTitleToLinks(vp.links, serverViewportId, title) + ); + this.postMessageToClient(messageToClient); + } + } + } + } + removeViewportFromVisualLinks(serverViewportId) { + var _a; + for (const vp of this.viewports.values()) { + if ((_a = vp.links) == null ? void 0 : _a.some(({ parentVpId }) => parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + vp.links.filter(({ parentVpId }) => parentVpId !== serverViewportId) + ); + this.postMessageToClient(messageToClient); + } + } + } + menuRpcCall(message) { + const viewport = this.getViewportForClient(message.vpId, false); + if (viewport == null ? void 0 : viewport.serverViewportId) { + const [requestId, rpcRequest] = stripRequestId(message); + this.sendMessageToServer( + { + ...rpcRequest, + vpId: viewport.serverViewportId + }, + requestId + ); + } + } + rpcCall(message) { + const [requestId, rpcRequest] = stripRequestId(message); + const module = getRpcServiceModule(rpcRequest.service); + this.sendMessageToServer(rpcRequest, requestId, { module }); + } + handleMessageFromClient(message) { + var _a; + if (isViewporttMessage(message)) { + if (message.type === "disable") { + const viewport = this.getViewportForClient(message.viewport, false); + if (viewport !== null) { + return this.disableViewport(viewport); + } else { + return; + } + } else { + const viewport = this.getViewportForClient(message.viewport); + switch (message.type) { + case "setViewRange": + return this.setViewRange(viewport, message); + case "config": + return this.setConfig(viewport, message); + case "aggregate": + return this.aggregate(viewport, message); + case "sort": + return this.sort(viewport, message); + case "groupBy": + return this.groupBy(viewport, message); + case "filter": + return this.filter(viewport, message); + case "select": + return this.select(viewport, message); + case "suspend": + return this.suspendViewport(viewport); + case "resume": + return this.resumeViewport(viewport); + case "enable": + return this.enableViewport(viewport); + case "openTreeNode": + return this.openTreeNode(viewport, message); + case "closeTreeNode": + return this.closeTreeNode(viewport, message); + case "createLink": + return this.createLink(viewport, message); + case "removeLink": + return this.removeLink(viewport); + case "setColumns": + return this.setColumns(viewport, message); + case "setTitle": + return this.setTitle(viewport, message); + default: + } + } + } else if (isVuuMenuRpcRequest(message)) { + return this.menuRpcCall(message); + } else { + const { type, requestId } = message; + switch (type) { + case "GET_TABLE_LIST": { + (_a = this.tableList) != null ? _a : this.tableList = this.awaitResponseToMessage( + { type }, + requestId + ); + this.tableList.then((response) => { + this.postMessageToClient({ + type: "TABLE_LIST_RESP", + tables: response.tables, + requestId + }); + }); + return; + } + case "GET_TABLE_META": { + this.getTableMeta(message.table, requestId).then((tableSchema) => { + if (tableSchema) { + this.postMessageToClient({ + type: "TABLE_META_RESP", + tableSchema, + requestId + }); + } + }); + return; + } + case "RPC_CALL": + return this.rpcCall(message); + default: + } + } + error3( + \`Vuu ServerProxy Unexpected message from client \${JSON.stringify( + message + )}\` + ); + } + getTableMeta(table, requestId = nextRequestId()) { + if (isSessionTable(table)) { + return Promise.resolve(void 0); + } + const key = \`\${table.module}:\${table.table}\`; + let tableMetaRequest = this.cachedTableMetaRequests.get(key); + if (!tableMetaRequest) { + tableMetaRequest = this.awaitResponseToMessage( + { type: "GET_TABLE_META", table }, + requestId + ); + this.cachedTableMetaRequests.set(key, tableMetaRequest); + } + return tableMetaRequest == null ? void 0 : tableMetaRequest.then((response) => this.cacheTableMeta(response)); + } + awaitResponseToMessage(message, requestId = nextRequestId()) { + return new Promise((resolve, reject) => { + this.sendMessageToServer(message, requestId); + this.pendingRequests.set(requestId, { reject, resolve }); + }); + } + sendIfReady(message, requestId, isReady = true) { + if (isReady) { + this.sendMessageToServer(message, requestId); + } else { + this.queuedRequests.push(message); + } + return isReady; + } + sendMessageToServer(body, requestId = \`\${_requestId++}\`, options = DEFAULT_OPTIONS) { + const { module = "CORE" } = options; + if (this.authToken) { + this.connection.send({ + requestId, + sessionId: this.sessionId, + token: this.authToken, + user: this.user, + module, + body + }); + } + } + handleMessageFromServer(message) { + var _a, _b, _c; + const { body, requestId, sessionId } = message; + const pendingRequest = this.pendingRequests.get(requestId); + if (pendingRequest) { + const { resolve } = pendingRequest; + this.pendingRequests.delete(requestId); + resolve(body); + return; + } + const { viewports } = this; + switch (body.type) { + case HB: + this.sendMessageToServer( + { type: HB_RESP, ts: +/* @__PURE__ */ new Date() }, + "NA" + ); + break; + case "LOGIN_SUCCESS": + if (sessionId) { + this.sessionId = sessionId; + (_a = this.pendingLogin) == null ? void 0 : _a.resolve(sessionId); + this.pendingLogin = void 0; + } else { + throw Error("LOGIN_SUCCESS did not provide sessionId"); + } + break; + case "REMOVE_VP_SUCCESS": + { + const viewport = viewports.get(body.viewPortId); + if (viewport) { + this.mapClientToServerViewport.delete(viewport.clientViewportId); + viewports.delete(body.viewPortId); + this.removeViewportFromVisualLinks(body.viewPortId); + } + } + break; + case SET_SELECTION_SUCCESS: + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + viewport.completeOperation(requestId); + } + } + break; + case CHANGE_VP_SUCCESS: + case DISABLE_VP_SUCCESS: + if (viewports.has(body.viewPortId)) { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response !== void 0) { + this.postMessageToClient(response); + if (debugEnabled4) { + debug4(\`postMessageToClient \${JSON.stringify(response)}\`); + } + } + } + } + break; + case ENABLE_VP_SUCCESS: + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response) { + this.postMessageToClient(response); + const rows = viewport.currentData(); + debugEnabled4 && debug4( + \`Enable Response (ServerProxy to Client): \${JSON.stringify( + response + )}\` + ); + if (viewport.size === 0) { + debugEnabled4 && debug4(\`Viewport Enabled but size 0, resend to server\`); + } else { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + size: viewport.size, + type: "viewport-update" + }); + debugEnabled4 && debug4( + \`Enable Response (ServerProxy to Client): send size \${viewport.size} \${rows.length} rows from cache\` + ); + } + } + } + } + break; + case TABLE_ROW: + { + const viewportRowMap = groupRowsByViewport(body.rows); + if (debugEnabled4) { + const [firstRow, secondRow] = body.rows; + if (body.rows.length === 0) { + debug4("handleMessageFromServer TABLE_ROW 0 rows"); + } else if ((firstRow == null ? void 0 : firstRow.rowIndex) === -1) { + if (body.rows.length === 1) { + if (firstRow.updateType === "SIZE") { + debug4( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE ONLY \${firstRow.vpSize}\` + ); + } else { + debug4( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE \${firstRow.vpSize} rowIdx \${firstRow.rowIndex}\` + ); + } + } else { + debug4( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows, SIZE \${firstRow.vpSize}, [\${secondRow == null ? void 0 : secondRow.rowIndex}] - [\${(_b = body.rows[body.rows.length - 1]) == null ? void 0 : _b.rowIndex}]\` + ); + } + } else { + debug4( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows [\${firstRow == null ? void 0 : firstRow.rowIndex}] - [\${(_c = body.rows[body.rows.length - 1]) == null ? void 0 : _c.rowIndex}]\` + ); + } + } + for (const [viewportId, rows] of Object.entries(viewportRowMap)) { + const viewport = viewports.get(viewportId); + if (viewport) { + viewport.updateRows(rows); + } else { + warn3 == null ? void 0 : warn3( + \`TABLE_ROW message received for non registered viewport \${viewportId}\` + ); + } + } + this.processUpdates(); + } + break; + case CHANGE_VP_RANGE_SUCCESS: + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const { from, to } = body; + if (true) { + info3 == null ? void 0 : info3(\`CHANGE_VP_RANGE_SUCCESS \${from} - \${to}\`); + } + viewport.completeOperation(requestId, from, to); + } + } + break; + case OPEN_TREE_SUCCESS: + case CLOSE_TREE_SUCCESS: + break; + case "CREATE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + const parentViewport = this.viewports.get(body.parentVpId); + if (viewport && parentViewport) { + const { childColumnName, parentColumnName } = body; + const response = viewport.completeOperation( + requestId, + childColumnName, + parentViewport.clientViewportId, + parentColumnName + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case "REMOVE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + if (viewport) { + const response = viewport.completeOperation( + requestId + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case "VP_VISUAL_LINKS_RESP": + { + const activeLinkDescriptors = this.getActiveLinks(body.links); + const viewport = this.viewports.get(body.vpId); + if (activeLinkDescriptors.length && viewport) { + const linkDescriptorsWithLabels = addLabelsToLinks( + activeLinkDescriptors, + this.viewports + ); + const [clientMessage, pendingLink] = viewport.setLinks( + linkDescriptorsWithLabels + ); + this.postMessageToClient(clientMessage); + if (pendingLink) { + const { link, parentClientVpId } = pendingLink; + const requestId2 = nextRequestId(); + const serverViewportId = this.mapClientToServerViewport.get(parentClientVpId); + if (serverViewportId) { + const message2 = viewport.createLink( + requestId2, + link.fromColumn, + serverViewportId, + link.toColumn + ); + this.sendMessageToServer(message2, requestId2); + } + } + } + } + break; + case "VIEW_PORT_MENUS_RESP": + if (body.menu.name) { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + const clientMessage = viewport.setMenu(body.menu); + this.postMessageToClient(clientMessage); + } + } + break; + case "VP_EDIT_RPC_RESPONSE": + { + this.postMessageToClient({ + action: body.action, + requestId, + rpcName: body.rpcName, + type: "VP_EDIT_RPC_RESPONSE" + }); + } + break; + case "VP_EDIT_RPC_REJECT": + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + this.postMessageToClient({ + requestId, + type: "VP_EDIT_RPC_REJECT", + error: body.error + }); + } + } + break; + case "VIEW_PORT_MENU_REJ": { + console.log(\`send menu error back to client\`); + const { error: error4, rpcName, vpId } = body; + const viewport = this.viewports.get(vpId); + if (viewport) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + error: error4, + rpcName, + type: "VIEW_PORT_MENU_REJ", + requestId + }); + } + break; + } + case "VIEW_PORT_MENU_RESP": + { + if (isSessionTableActionMessage(body)) { + const { action, rpcName } = body; + this.awaitResponseToMessage({ + type: "GET_TABLE_META", + table: action.table + }).then((response) => { + const tableSchema = createSchemaFromTableMetadata( + response + ); + this.postMessageToClient({ + rpcName, + type: "VIEW_PORT_MENU_RESP", + action: { + ...action, + tableSchema + }, + tableAlreadyOpen: this.isTableOpen(action.table), + requestId + }); + }); + } else { + const { action } = body; + this.postMessageToClient({ + type: "VIEW_PORT_MENU_RESP", + action: action || NO_ACTION, + tableAlreadyOpen: action !== null && this.isTableOpen(action.table), + requestId + }); + } + } + break; + case RPC_RESP: + { + const { method, result } = body; + this.postMessageToClient({ + type: RPC_RESP, + method, + result, + requestId + }); + } + break; + case "ERROR": + error3(body.msg); + break; + default: + infoEnabled3 && info3(\`handleMessageFromServer \${body["type"]}.\`); + } + } + cacheTableMeta(messageBody) { + const { module, table } = messageBody.table; + const key = \`\${module}:\${table}\`; + let tableSchema = this.cachedTableSchemas.get(key); + if (!tableSchema) { + tableSchema = createSchemaFromTableMetadata(messageBody); + this.cachedTableSchemas.set(key, tableSchema); + } + return tableSchema; + } + isTableOpen(table) { + if (table) { + const tableName = table.table; + for (const viewport of this.viewports.values()) { + if (!viewport.suspended && viewport.table.table === tableName) { + return true; + } + } + } + } + // Eliminate links to suspended viewports + getActiveLinks(linkDescriptors) { + return linkDescriptors.filter((linkDescriptor) => { + const viewport = this.viewports.get(linkDescriptor.parentVpId); + return viewport && !viewport.suspended; + }); + } + processUpdates() { + this.viewports.forEach((viewport) => { + var _a; + if (viewport.hasUpdatesToProcess) { + const result = viewport.getClientRows(); + if (result !== NO_DATA_UPDATE) { + const [rows, mode] = result; + const size = viewport.getNewRowCount(); + if (size !== void 0 || rows && rows.length > 0) { + debugEnabled4 && debug4( + \`postMessageToClient #\${viewport.clientViewportId} viewport-update \${mode}, \${(_a = rows == null ? void 0 : rows.length) != null ? _a : "no"} rows, size \${size}\` + ); + if (mode) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode, + rows, + size, + type: "viewport-update" + }); + } + } + } + } + }); + } +}; + +// src/worker.ts +var server; +var { info: info4, infoEnabled: infoEnabled4 } = logger("worker"); +async function connectToServer(url, protocol, token, username, onConnectionStatusChange, retryLimitDisconnect, retryLimitStartup) { + const connection = await connect( + url, + protocol, + // if this was called during connect, we would get a ReferenceError, but it will + // never be called until subscriptions have been made, so this is safe. + //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? + (msg) => { + if (isConnectionQualityMetrics(msg)) { + console.log("post connection metrics"); + postMessage({ type: "connection-metrics", messages: msg }); + } else if (isConnectionStatusMessage(msg)) { + onConnectionStatusChange(msg); + if (msg.status === "reconnected") { + server.reconnect(); + } + } else { + server.handleMessageFromServer(msg); + } + }, + retryLimitDisconnect, + retryLimitStartup + ); + server = new ServerProxy(connection, (msg) => sendMessageToClient(msg)); + if (connection.requiresLogin) { + await server.login(token, username); + } +} +function sendMessageToClient(message) { + postMessage(message); +} +var handleMessageFromClient = async ({ + data: message +}) => { + switch (message.type) { + case "connect": + await connectToServer( + message.url, + message.protocol, + message.token, + message.username, + postMessage, + message.retryLimitDisconnect, + message.retryLimitStartup + ); + postMessage({ type: "connected" }); + break; + case "subscribe": + infoEnabled4 && info4(\`client subscribe: \${JSON.stringify(message)}\`); + server.subscribe(message); + break; + case "unsubscribe": + infoEnabled4 && info4(\`client unsubscribe: \${JSON.stringify(message)}\`); + server.unsubscribe(message.viewport); + break; + default: + infoEnabled4 && info4(\`client message: \${JSON.stringify(message)}\`); + server.handleMessageFromClient(message); + } +}; +self.addEventListener("message", handleMessageFromClient); +postMessage({ type: "ready" }); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data/src/remote-data-source.ts b/vuu-ui/packages/vuu-data/src/remote-data-source.ts index 52ee46980..8a34ca384 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -5,6 +5,7 @@ import { ClientToServerMenuRPC, LinkDescriptorWithLabel, VuuAggregation, + VuuDataRowDto, VuuGroupBy, VuuMenu, VuuRange, @@ -457,6 +458,7 @@ export class RemoteDataSource } set columns(columns: string[]) { + console.log(`set columns ${columns.join(",")}`); this.#config = { ...this.#config, columns, @@ -662,4 +664,33 @@ export class RemoteDataSource } }); } + + insertRow(key: string, data: VuuDataRowDto) { + console.log("RemoteDataSource insertRow ${key}", { + data, + }); + return this.menuRpcCall({ + rowKey: key, + data, + type: "VP_EDIT_ADD_ROW_RPC", + }).then((response) => { + if (response?.error) { + return response.error; + } else { + return true; + } + }); + } + deleteRow(rowKey: string) { + return this.menuRpcCall({ + rowKey, + type: "VP_EDIT_DELETE_ROW_RPC", + }).then((response) => { + if (response?.error) { + return response.error; + } else { + return true; + } + }); + } } diff --git a/vuu-ui/packages/vuu-data/src/worker.ts b/vuu-ui/packages/vuu-data/src/worker.ts index 881746c68..0bab408d7 100644 --- a/vuu-ui/packages/vuu-data/src/worker.ts +++ b/vuu-ui/packages/vuu-data/src/worker.ts @@ -37,7 +37,7 @@ async function connectToServer( //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? (msg) => { if (isConnectionQualityMetrics(msg)) { - console.log("post connection metrics"); + // console.log("post connection metrics"); postMessage({ type: "connection-metrics", messages: msg }); } else if (isConnectionStatusMessage(msg)) { onConnectionStatusChange(msg); diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 630f6dc0f..7cd0b2df2 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -1,4 +1,3 @@ -import type { ValueFormatter } from "@finos/vuu-table"; import type { Filter } from "@finos/vuu-filter-types"; import type { VuuAggType, @@ -7,9 +6,11 @@ import type { VuuSortType, VuuTable, } from "@finos/vuu-protocol-types"; -import type { FunctionComponent, MouseEvent } from "react"; +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import type { ValueFormatter } from "@finos/vuu-table"; import type { ClientSideValidationChecker } from "@finos/vuu-ui-controls"; import type { ColumnMap } from "@finos/vuu-utils"; +import type { FunctionComponent, MouseEvent } from "react"; export type TableSelectionModel = "none" | "single" | "checkbox" | "extended"; @@ -42,6 +43,14 @@ export type DataItemCommitHandler< T extends VuuRowDataItemType = VuuRowDataItemType > = (value: T) => CommitResponse; +export type TableRowClickHandler = (row: VuuDataRow) => void; + +export type RowClickHandler = ( + row: DataSourceRow, + rangeSelect: boolean, + keepExistingSelection: boolean +) => void; + export interface TableCellRendererProps extends Omit { onCommit?: DataItemCommitHandler; @@ -51,7 +60,7 @@ export interface TableAttributes { columnDefaultWidth?: number; columnFormatHeader?: "capitalize" | "uppercase"; columnSeparators?: boolean; - showHighlightedRow?: boolean; + // showHighlightedRow?: boolean; rowSeparators?: boolean; zebraStripes?: boolean; } diff --git a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx index 88192586d..30450c84e 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx +++ b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx @@ -8,6 +8,7 @@ export type PopupPlacement = | "absolute" | "below" | "below-center" + | "below-right" | "below-full-width" | "center" | "right"; diff --git a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts index 9ca9cc747..09d367664 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts +++ b/vuu-ui/packages/vuu-popups/src/popup/useAnchoredPosition.ts @@ -36,6 +36,8 @@ const getPositionRelativeToAnchor = ( return { left: right + offsetLeft, top: top + offsetTop }; case "below-center": return { left: left + width / 2 + offsetLeft, top: bottom + offsetTop }; + case "below-right": + return { left: left, minWidth, top: bottom + offsetTop }; case "below-full-width": return { left: left + offsetLeft, diff --git a/vuu-ui/packages/vuu-protocol-types/index.d.ts b/vuu-ui/packages/vuu-protocol-types/index.d.ts index 7faca10cf..278819e07 100644 --- a/vuu-ui/packages/vuu-protocol-types/index.d.ts +++ b/vuu-ui/packages/vuu-protocol-types/index.d.ts @@ -443,12 +443,25 @@ export interface ClientToServerEditRowRpc { type: "VP_EDIT_ROW_RPC"; row: VuuDataRow; } + +export type VuuDataRowDto = { [key: string]: VuuRowDataItemType }; +export interface ClientToServerAddRowRpc { + rowKey: string; + type: "VP_EDIT_ADD_ROW_RPC"; + data: VuuDataRowDto; +} +export interface ClientToServerDeleteRowRpc { + rowKey: string; + type: "VP_EDIT_DELETE_ROW_RPC"; +} export interface ClientToServerSubmitFormRpc { type: "VP_EDIT_SUBMIT_FORM_RPC"; } export type ClientToServerEditRpc = | ClientToServerEditCellRpc + | ClientToServerAddRowRpc + | ClientToServerDeleteRowRpc | ClientToServerEditRowRpc | ClientToServerSubmitFormRpc; diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx index 24b2b65a4..0ed98b641 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx @@ -38,11 +38,12 @@ const getFlashStyle = (colType?: ColumnType) => { }; // export to avoid tree shaking, component is not consumed directly -export const BackgroundCell = ({ column, row }: TableCellProps) => { +export const BackgroundCell = ({ column, columnMap, row }: TableCellProps) => { //TODO what about click handling - const { key, type, valueFormatter } = column; - const value = row[key]; + const { name, type, valueFormatter } = column; + const dataIdx = columnMap[name]; + const value = row[dataIdx]; const flashStyle = getFlashStyle(type); const direction = useDirection(row[KEY], value, column); const arrow = @@ -66,7 +67,7 @@ export const BackgroundCell = ({ column, row }: TableCellProps) => { return (
{arrow}
- {valueFormatter(row[column.key])} + {valueFormatter(row[dataIdx])}
); }; diff --git a/vuu-ui/packages/vuu-table/src/index.ts b/vuu-ui/packages/vuu-table/src/index.ts index 21dec6528..8c80575ce 100644 --- a/vuu-ui/packages/vuu-table/src/index.ts +++ b/vuu-ui/packages/vuu-table/src/index.ts @@ -1,4 +1,8 @@ export * from "./table"; -export { GroupHeaderCellNext, TableNext } from "./table-next"; +export { + GroupHeaderCellNext, + TableNext, + useControlledTableNavigation, +} from "./table-next"; export type { RowProps } from "./table-next"; export { updateTableConfig } from "./table-next/table-config"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/Row.css b/vuu-ui/packages/vuu-table/src/table-next/Row.css index 4fccc0fb9..1307f4eb0 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/Row.css +++ b/vuu-ui/packages/vuu-table/src/table-next/Row.css @@ -13,6 +13,11 @@ --row-background: var(--row-background-even); } + .vuuTableNextRow-highlighted { + background-color: var(--vuu-color-gray-10); + } + + .vuuTableNextRow-selected, .vuuTableNextRow-selectedEnd { /* background-color: rgb(133,133,137,.16); */ @@ -109,4 +114,10 @@ .vuuTableNextRow-expanded { --toggle-icon-transform: rotate(90deg); } - \ No newline at end of file + + .vuuDraggable .vuuTableNextRow { + --cell-borderColor: transparent; + --vuu-selection-decorator-bg: transparent; + transform: none!important; + z-index: 1; + } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table-next/Row.tsx b/vuu-ui/packages/vuu-table/src/table-next/Row.tsx index e71aa7f20..2e713c02d 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/Row.tsx @@ -2,8 +2,8 @@ import { DataSourceRow } from "@finos/vuu-data-types"; import { DataCellEditHandler, KeyedColumnDescriptor, + RowClickHandler, } from "@finos/vuu-datagrid-types"; -import { RowClickHandler } from "@finos/vuu-table"; import { ColumnMap, isGroupColumn, @@ -23,6 +23,7 @@ export interface RowProps { className?: string; columnMap: ColumnMap; columns: KeyedColumnDescriptor[]; + highlighted?: boolean; row: DataSourceRow; offset: number; onClick?: RowClickHandler; @@ -41,6 +42,7 @@ export const Row = memo( className: classNameProp, columnMap, columns, + highlighted, row, offset, onClick, @@ -69,6 +71,7 @@ export const Row = memo( const className = cx(classBase, classNameProp, { [`${classBase}-even`]: zebraStripes && rowIndex % 2 === 0, [`${classBase}-expanded`]: isExpanded, + [`${classBase}-highlighted`]: highlighted, [`${classBase}-selected`]: selectionStatus & True, [`${classBase}-selectedStart`]: selectionStatus & First, [`${classBase}-selectedEnd`]: selectionStatus & Last, diff --git a/vuu-ui/packages/vuu-table/src/table-next/TableNext.css b/vuu-ui/packages/vuu-table/src/table-next/TableNext.css index 9b4836589..32b951131 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.css +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.css @@ -134,4 +134,7 @@ .vuuDraggable-vuuTableNext { --header-height: 25px; --vuuTableNextHeaderCell-background: var(--vuu-color-gray-25); - } \ No newline at end of file + } +.vuuDraggable-vuuTableNext { + --row-height: 25px; +} diff --git a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx index 5d26c7fb0..1a1106f8a 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx @@ -1,22 +1,16 @@ +import { MeasuredContainer, useId } from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; import { TableProps } from "@finos/vuu-table"; import { isGroupColumn, metadataKeys, notHidden } from "@finos/vuu-utils"; +import { useForkRef } from "@salt-ds/core"; import cx from "classnames"; -import { - CSSProperties, - ForwardedRef, - forwardRef, - useEffect, - useRef, -} from "react"; +import { CSSProperties, ForwardedRef, forwardRef, useRef } from "react"; import { GroupHeaderCellNext as GroupHeaderCell, HeaderCell, } from "./header-cell"; import { Row as DefaultRow } from "./Row"; import { useTable } from "./useTableNext"; -import { MeasuredContainer, useId } from "@finos/vuu-layout"; -import { useForkRef } from "@salt-ds/core"; import "./TableNext.css"; @@ -27,15 +21,21 @@ const { IDX, RENDER_IDX } = metadataKeys; export const TableNext = forwardRef(function TableNext( { Row = DefaultRow, + allowDragDrop, availableColumns, className: classNameProp, config, dataSource, + disableFocus = false, + highlightedIndex: highlightedIndexProp, id: idProp, navigationStyle = "cell", onAvailableColumnsChange, onConfigChange, + onDragStart, + onDrop, onFeatureInvocation, + onHighlight, onRowClick: onRowClickProp, onSelect, onSelectionChange, @@ -51,15 +51,17 @@ export const TableNext = forwardRef(function TableNext( forwardedRef: ForwardedRef ) { const id = useId(idProp); - const containerRef = useRef(null); const { columnMap, columns, data, + draggableColumn, + draggableRow, dragDropHook, handleContextMenuAction, headerProps, + highlightedIndex, onDataEdited, onRemoveGroupColumn, onResize, @@ -71,15 +73,22 @@ export const TableNext = forwardRef(function TableNext( viewportMeasurements, ...tableProps } = useTable({ + allowDragDrop, availableColumns, config, containerRef, dataSource, + disableFocus, headerHeight, + highlightedIndex: highlightedIndexProp, + id, navigationStyle, onAvailableColumnsChange, onConfigChange, + onDragStart, + onDrop, onFeatureInvocation, + onHighlight, onRowClick: onRowClickProp, onSelect, onSelectionChange, @@ -87,6 +96,7 @@ export const TableNext = forwardRef(function TableNext( rowHeight, selectionModel, }); + const getStyle = () => { return { ...styleProp, @@ -105,7 +115,7 @@ export const TableNext = forwardRef(function TableNext( const className = cx(classBase, classNameProp, { [`${classBase}-colLines`]: tableAttributes.columnSeparators, [`${classBase}-rowLines`]: tableAttributes.rowSeparators, - [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, + // [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, [`${classBase}-zebra`]: tableAttributes.zebraStripes, // [`${classBase}-loading`]: isDataLoading(tableProps.columns), }); @@ -118,6 +128,7 @@ export const TableNext = forwardRef(function TableNext( -
+
{showColumnHeaders ? (
@@ -159,7 +174,7 @@ export const TableNext = forwardRef(function TableNext( /> ) )} - {dragDropHook.draggable} + {draggableColumn}
) : null} @@ -168,6 +183,7 @@ export const TableNext = forwardRef(function TableNext(
+ {draggableRow}
); diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx index 52c3b975f..0fe5b4508 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/dropdown-cell/DropdownCell.tsx @@ -1,13 +1,12 @@ import { useLookupValues } from "@finos/vuu-data-react"; import { ListOption, TableCellRendererProps } from "@finos/vuu-datagrid-types"; import { - dispatchCommitEvent, Dropdown, DropdownOpenKey, SingleSelectionHandler, WarnCommit, } from "@finos/vuu-ui-controls"; -import { registerComponent } from "@finos/vuu-utils"; +import { dispatchCustomEvent, registerComponent } from "@finos/vuu-utils"; import { VuuColumnDataType } from "@finos/vuu-protocol-types"; import { memo, useCallback, useState } from "react"; import { dataAndColumnUnchanged } from "../cell-utils"; @@ -36,7 +35,7 @@ export const DropdownCell = memo(function DropdownCell({ setValue(selectedOption); onCommit(selectedOption.value as VuuColumnDataType).then((response) => { if (response === true && evt) { - dispatchCommitEvent(evt.target as HTMLElement); + dispatchCustomEvent(evt.target as HTMLElement, "vuu-commit"); } }); } diff --git a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx index 4c5bbe9d1..dab706857 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/cell-renderers/toggle-cell/ToggleCell.tsx @@ -2,12 +2,9 @@ import { ColumnDescriptor, TableCellRendererProps, } from "@finos/vuu-datagrid-types"; +import { CycleStateButtonProps, WarnCommit } from "@finos/vuu-ui-controls"; import { - CycleStateButtonProps, - dispatchCommitEvent, - WarnCommit, -} from "@finos/vuu-ui-controls"; -import { + dispatchCustomEvent, isTypeDescriptor, isValueListRenderer, registerComponent, @@ -46,7 +43,7 @@ export const ToggleCell = memo(function ToggleCell({ (evt, value) => { return onCommit(value).then((response) => { if (response === true) { - dispatchCommitEvent(evt.target as HTMLElement); + dispatchCustomEvent(evt.target as HTMLElement, "vuu-commit"); } return response; }); diff --git a/vuu-ui/packages/vuu-table/src/table-next/index.ts b/vuu-ui/packages/vuu-table/src/table-next/index.ts index e4c1a2c0a..280e4c965 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/index.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/index.ts @@ -3,3 +3,4 @@ export * from "./TableNext"; export * from "./table-config"; export * from "./cell-renderers"; export type { RowProps } from "./Row"; +export * from "./useControlledTableNavigation"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx index c6c3ecf61..1d521dfcf 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.tsx @@ -2,10 +2,6 @@ import { DataItemCommitHandler, TableCellProps, } from "@finos/vuu-datagrid-types"; -import { - VuuColumnDataType, - VuuRowDataItemType, -} from "@finos/vuu-protocol-types"; import { isNumericColumn } from "@finos/vuu-utils"; import { MouseEventHandler, useCallback } from "react"; import { useCell } from "../useCell"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts index b0006e1a3..c123d20e4 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/table-dom-utils.ts @@ -35,3 +35,18 @@ export const cellIsEditable = (cell: HTMLDivElement) => export const cellIsTextInput = (cell: HTMLElement) => cell.querySelector(".vuuTableInputCell") !== null; + +export function getRowIndex(rowEl?: HTMLElement) { + if (rowEl) { + const idx: string | null = rowEl.ariaRowIndex; + if (idx !== null) { + return parseInt(idx, 10); + } + } + return -1; +} + +const closestRow = (el: HTMLElement) => + el.closest('[role="row"]') as HTMLElement; + +export const closestRowIndex = (el: HTMLElement) => getRowIndex(closestRow(el)); diff --git a/vuu-ui/packages/vuu-table/src/table-next/useControlledTableNavigation.ts b/vuu-ui/packages/vuu-table/src/table-next/useControlledTableNavigation.ts new file mode 100644 index 000000000..54833bc2e --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/useControlledTableNavigation.ts @@ -0,0 +1,48 @@ +import { useStateRef } from "@finos/vuu-ui-controls"; +import { dispatchMouseEvent } from "@finos/vuu-utils"; +import { KeyboardEventHandler, useCallback, useRef } from "react"; + +export const useControlledTableNavigation = ( + initialValue: number, + rowCount: number +) => { + const tableRef = useRef(null); + + const [highlightedIndexRef, setHighlightedIndex] = useStateRef< + number | undefined + >(initialValue); + + const handleKeyDown = useCallback( + (e) => { + if (e.key === "ArrowDown") { + setHighlightedIndex((index = -1) => Math.min(rowCount - 1, index + 1)); + } else if (e.key === "ArrowUp") { + setHighlightedIndex((index = -1) => Math.max(0, index - 1)); + } else if (e.key === "Enter" || e.key === " ") { + const { current: rowIdx } = highlightedIndexRef; + // induce an onSelect event by 'clicking' the row + const rowEl = tableRef.current?.querySelector( + `[aria-rowindex="${rowIdx}"]` + ) as HTMLElement; + if (rowEl) { + dispatchMouseEvent(rowEl, "click"); + } + } + }, + [highlightedIndexRef, rowCount, setHighlightedIndex] + ); + + const handleHighlight = useCallback( + (idx: number) => { + setHighlightedIndex(idx); + }, + [setHighlightedIndex] + ); + + return { + highlightedIndexRef, + onHighlight: handleHighlight, + onKeyDown: handleKeyDown, + tableRef, + }; +}; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts b/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts index 0f96a7b13..d02def46b 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts @@ -44,7 +44,6 @@ export const useDataSource = ({ const setData = useCallback( (updates: DataSourceRow[]) => { - console.table(updates); for (const row of updates) { dataWindow.add(row); } @@ -135,6 +134,7 @@ export const useDataSource = ({ return { data: data.current, + dataRef: data, getSelectedRows, range: rangeRef.current, setRange, diff --git a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts index 426c3c73a..bb4e6e48e 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts @@ -1,3 +1,4 @@ +import { useControlled } from "@salt-ds/core"; import { VuuRange } from "@finos/vuu-protocol-types"; import { KeyboardEvent, @@ -5,12 +6,12 @@ import { RefObject, useCallback, useEffect, - useMemo, useRef, } from "react"; import { ScrollDirection, ScrollRequestHandler } from "./useTableScroll"; import { CellPos, + closestRowIndex, dataCellQuery, getTableCell, headerCellQuery, @@ -56,34 +57,58 @@ const NULL_CELL_POS: CellPos = [-1, -1]; const NO_SCROLL_NECESSARY = [undefined, undefined] as const; -const howFarIsCellOutsideViewport = ( - cellEl: HTMLElement +const howFarIsRowOutsideViewport = ( + rowEl: HTMLElement, + contentContainer = rowEl.closest(".vuuTableNext-contentContainer") ): readonly [ScrollDirection | undefined, number | undefined] => { //TODO lots of scope for optimisation here - const contentContainer = cellEl.closest(".vuuTableNext-contentContainer"); if (contentContainer) { const viewport = contentContainer?.getBoundingClientRect(); - const cell = cellEl.closest(".vuuTableNextCell")?.getBoundingClientRect(); - if (cell) { - if (cell.bottom > viewport.bottom) { - return ["down", cell.bottom - viewport.bottom]; - } else if (cell.top < viewport.top) { - return ["up", cell.top - viewport.top]; - } else if (cell.right > viewport.right) { - return ["right", cell.right + 6 - viewport.right]; - } else if (cell.left < viewport.left) { - return ["left", cell.left - viewport.left]; + const row = rowEl.getBoundingClientRect(); + if (row) { + if (row.bottom > viewport.bottom) { + return ["down", row.bottom - viewport.bottom]; + } else if (row.top < viewport.top) { + return ["up", row.top - viewport.top]; } else { return NO_SCROLL_NECESSARY; } } else { - throw Error("Whats going on, cell not found"); + throw Error("Whats going on, row not found"); } } else { throw Error("Whats going on, scrollbar container not found"); } }; +const howFarIsCellOutsideViewport = ( + cellEl: HTMLElement +): readonly [ScrollDirection | undefined, number | undefined] => { + //TODO lots of scope for optimisation here + const contentContainer = cellEl.closest(".vuuTableNext-contentContainer"); + if (contentContainer) { + const rowEl = cellEl.closest(".vuuTableNextRow") as HTMLElement; + if (rowEl) { + const result = howFarIsRowOutsideViewport(rowEl, contentContainer); + if (result !== NO_SCROLL_NECESSARY) { + return result; + } + const viewport = contentContainer?.getBoundingClientRect(); + const cell = cellEl.closest(".vuuTableNextCell")?.getBoundingClientRect(); + if (cell) { + if (cell.right > viewport.right) { + return ["right", cell.right + 6 - viewport.right]; + } else if (cell.left < viewport.left) { + return ["left", cell.left - viewport.left]; + } + } else { + throw Error("Whats going on, cell not found"); + } + } + } + return NO_SCROLL_NECESSARY; +}; + function nextCellPos( key: ArrowKey, [rowIdx, colIdx]: CellPos, @@ -124,10 +149,14 @@ function nextCellPos( export interface NavigationHookProps { containerRef: RefObject; columnCount?: number; + defaultHighlightedIndex?: number; + disableFocus?: boolean; disableHighlightOnFocus?: boolean; + highlightedIndex?: number; label?: string; navigationStyle: TableNavigationStyle; viewportRange: VuuRange; + onHighlight?: (idx: number) => void; requestScroll?: ScrollRequestHandler; restoreLastFocus?: boolean; rowCount?: number; @@ -138,9 +167,13 @@ export interface NavigationHookProps { export const useKeyboardNavigation = ({ columnCount = 0, containerRef, + disableFocus = false, + defaultHighlightedIndex, disableHighlightOnFocus, + highlightedIndex: highlightedIndexProp, navigationStyle, requestScroll, + onHighlight, rowCount = 0, viewportRowCount, }: // viewportRange, @@ -149,6 +182,28 @@ NavigationHookProps) => { const focusedCellPos = useRef([-1, -1]); const focusableCell = useRef(); const activeCellPos = useRef([-1, 0]); + // Keep this in sync with state value. This can be used by functions that need + // to reference highlightedIndex at call time but do not need to be regenerated + // every time it changes (i.e keep highlightedIndex out of their dependency + // arrays, as it can update frequently) + const highlightedIndexRef = useRef(); + + const [highlightedIndex, setHighlightedIdx] = useControlled({ + controlled: highlightedIndexProp, + default: defaultHighlightedIndex, + name: "UseKeyboardNavigation", + }); + highlightedIndexRef.current = highlightedIndex; + const setHighlightedIndex = useCallback( + (idx: number, fromKeyboard = false) => { + onHighlight?.(idx); + setHighlightedIdx(idx); + if (fromKeyboard) { + // lastFocus.current = idx; + } + }, + [onHighlight, setHighlightedIdx] + ); const getFocusedCell = (element: HTMLElement | Element | null) => element?.closest( @@ -198,12 +253,16 @@ NavigationHookProps) => { (rowIdx: number, colIdx: number, fromKeyboard = false) => { const pos: CellPos = [rowIdx, colIdx]; activeCellPos.current = pos; - focusCell(pos); + if (navigationStyle === "row") { + setHighlightedIdx(rowIdx); + } else { + focusCell(pos); + } if (fromKeyboard) { focusedCellPos.current = pos; } }, - [focusCell] + [focusCell, navigationStyle, setHighlightedIdx] ); const nextPageItemIdx = useCallback( @@ -248,17 +307,24 @@ NavigationHookProps) => { const focusedCell = getFocusedCell(document.activeElement); if (focusedCell) { focusedCellPos.current = getTableCellPos(focusedCell); + if (navigationStyle === "row") { + setHighlightedIdx(focusedCellPos.current[0]); + } } } } - }, [disableHighlightOnFocus, containerRef]); + }, [ + disableHighlightOnFocus, + containerRef, + navigationStyle, + setHighlightedIdx, + ]); const navigateChildItems = useCallback( async (key: NavigationKey) => { const [nextRowIdx, nextColIdx] = isPagingKey(key) ? await nextPageItemIdx(key, activeCellPos.current) : nextCellPos(key, activeCellPos.current, columnCount, rowCount); - const [rowIdx, colIdx] = activeCellPos.current; if (nextRowIdx !== rowIdx || nextColIdx !== colIdx) { setActiveCell(nextRowIdx, nextColIdx, true); @@ -267,15 +333,62 @@ NavigationHookProps) => { [columnCount, nextPageItemIdx, rowCount, setActiveCell] ); + const scrollRowIntoViewIfNecessary = useCallback( + (rowIndex: number) => { + const { current: container } = containerRef; + const activeRow = container?.querySelector( + `[aria-rowindex="${rowIndex}"]` + ) as HTMLElement; + if (activeRow) { + const [direction, distance] = howFarIsRowOutsideViewport(activeRow); + if (direction && distance) { + requestScroll?.({ type: "scroll-distance", distance, direction }); + } + } + }, + [containerRef, requestScroll] + ); + + const moveHighlightedRow = useCallback( + async (key: NavigationKey) => { + console.log(`moveHighlightedRow`); + const { current: highlighted } = highlightedIndexRef; + const [nextRowIdx] = isPagingKey(key) + ? await nextPageItemIdx(key, [highlighted ?? -1, 0]) + : nextCellPos(key, [highlighted ?? -1, 0], columnCount, rowCount); + if (nextRowIdx !== highlighted) { + setHighlightedIndex(nextRowIdx); + scrollRowIntoViewIfNecessary(nextRowIdx); + } + }, + [ + columnCount, + nextPageItemIdx, + rowCount, + scrollRowIntoViewIfNecessary, + setHighlightedIndex, + ] + ); + + useEffect(() => { + if (highlightedIndexProp !== undefined && highlightedIndexProp !== -1) { + scrollRowIntoViewIfNecessary(highlightedIndexProp); + } + }, [highlightedIndexProp, scrollRowIntoViewIfNecessary]); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (rowCount > 0 && isNavigationKey(e.key, navigationStyle)) { e.preventDefault(); e.stopPropagation(); - void navigateChildItems(e.key); + if (navigationStyle === "row") { + moveHighlightedRow(e.key); + } else { + void navigateChildItems(e.key); + } } }, - [rowCount, navigationStyle, navigateChildItems] + [rowCount, navigationStyle, moveHighlightedRow, navigateChildItems] ); const handleClick = useCallback( @@ -291,25 +404,30 @@ NavigationHookProps) => { [setActiveCell] ); + const handleMouseLeave = useCallback(() => { + setHighlightedIndex(-1); + }, [setHighlightedIndex]); + + const handleMouseMove = useCallback( + (evt: MouseEvent) => { + const idx = closestRowIndex(evt.target as HTMLElement); + if (idx !== -1 && idx !== highlightedIndexRef.current) { + setHighlightedIndex(idx); + } + }, + [setHighlightedIndex] + ); + const navigate = useCallback(() => { navigateChildItems("ArrowDown"); }, [navigateChildItems]); - const containerProps = useMemo(() => { - return { - navigate, - onClick: handleClick, - onFocus: handleFocus, - onKeyDown: handleKeyDown, - }; - }, [handleClick, handleFocus, handleKeyDown, navigate]); - // First render will only render the outer container when explicit // sizing has not been provided. Outer container is measured and // only then, on second render, is content rendered. const fullyRendered = containerRef.current?.firstChild != null; useEffect(() => { - if (fullyRendered && focusableCell.current === undefined) { + if (fullyRendered && focusableCell.current === undefined && !disableFocus) { const { current: container } = containerRef; const cell = (container?.querySelector(headerCellQuery(0)) || container?.querySelector(dataCellQuery(0, 0))) as HTMLElement; @@ -320,5 +438,13 @@ NavigationHookProps) => { } }, [containerRef, fullyRendered]); - return containerProps; + return { + highlightedIndexRef, + navigate, + onClick: handleClick, + onFocus: handleFocus, + onKeyDown: handleKeyDown, + onMouseLeave: navigationStyle === "row" ? handleMouseLeave : undefined, + onMouseMove: navigationStyle === "row" ? handleMouseMove : undefined, + }; }; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useSelection.ts b/vuu-ui/packages/vuu-table/src/table-next/useSelection.ts new file mode 100644 index 000000000..7248b5d9f --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/table-next/useSelection.ts @@ -0,0 +1,100 @@ +import { + RowClickHandler, + Selection, + SelectionChangeHandler, + TableSelectionModel, +} from "@finos/vuu-datagrid-types"; +import { + deselectItem, + dispatchMouseEvent, + isRowSelected, + metadataKeys, + selectItem, +} from "@finos/vuu-utils"; +import { DataSourceRow } from "packages/vuu-data-types"; +import { + KeyboardEvent, + KeyboardEventHandler, + MutableRefObject, + useCallback, + useRef, +} from "react"; + +const { IDX } = metadataKeys; + +const NO_SELECTION: Selection = []; + +const defaultSelectionKeys = ["Enter", " "]; + +export interface SelectionHookProps { + highlightedIndexRef: MutableRefObject; + selectionKeys?: string[]; + selectionModel: TableSelectionModel; + onSelect?: (row: DataSourceRow) => void; + onSelectionChange: SelectionChangeHandler; +} + +export const useSelection = ({ + highlightedIndexRef, + selectionKeys = defaultSelectionKeys, + selectionModel, + onSelect, + onSelectionChange, +}: SelectionHookProps) => { + selectionModel === "extended" || selectionModel === "checkbox"; + const lastActiveRef = useRef(-1); + const selectedRef = useRef(NO_SELECTION); + + const isSelectionEvent = useCallback( + (evt: KeyboardEvent) => selectionKeys.includes(evt.key), + [selectionKeys] + ); + + const handleRowClick = useCallback( + (row, rangeSelect, keepExistingSelection) => { + const { [IDX]: idx } = row; + const { current: active } = lastActiveRef; + const { current: selected } = selectedRef; + + const selectOperation = isRowSelected(row) ? deselectItem : selectItem; + + const newSelected = selectOperation( + selectionModel, + selected, + idx, + rangeSelect, + keepExistingSelection, + active + ); + + selectedRef.current = newSelected; + lastActiveRef.current = idx; + + onSelect?.(row); + onSelectionChange?.(newSelected); + }, + [onSelect, onSelectionChange, selectionModel] + ); + + const handleKeyDown = useCallback>( + (e) => { + if (isSelectionEvent(e)) { + const { current: rowIndex } = highlightedIndexRef; + if (rowIndex !== undefined && rowIndex !== -1) { + const rowEl = (e.target as HTMLElement).querySelector( + `[aria-rowindex="${rowIndex}"]` + ) as HTMLElement; + if (rowEl) { + dispatchMouseEvent(rowEl, "click"); + } + } + } + }, + [highlightedIndexRef, isSelectionEvent] + ); + + return { + onKeyDown: handleKeyDown, + onRowClick: handleRowClick, + }; +}; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts index ad72ae840..d0ff0c6b5 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -1,5 +1,4 @@ import { - DataSource, DataSourceConfig, DataSourceSubscribedMessage, JsonDataSource, @@ -9,6 +8,7 @@ import { ColumnDescriptor, DataCellEditHandler, KeyedColumnDescriptor, + RowClickHandler, SelectionChangeHandler, TableConfig, TableSelectionModel, @@ -16,7 +16,10 @@ import { import { MeasuredSize, useLayoutEffectSkipFirst } from "@finos/vuu-layout"; import { VuuRange, VuuSortType } from "@finos/vuu-protocol-types"; import { useTableAndColumnSettings } from "@finos/vuu-table-extras"; -import { useDragDropNext as useDragDrop } from "@finos/vuu-ui-controls"; +import { + DragStartHandler, + useDragDropNext as useDragDrop, +} from "@finos/vuu-ui-controls"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { applySort, @@ -37,7 +40,6 @@ import { useCallback, useEffect, useMemo, - useRef, useState, } from "react"; import { @@ -45,14 +47,13 @@ import { ColumnActionHide, ColumnActionPin, MeasuredProps, - RowClickHandler, TableProps, - useSelection, } from "../table"; import { TableColumnResizeHandler } from "./column-resizing"; import { updateTableConfig } from "./table-config"; import { useDataSource } from "./useDataSource"; import { useInitialValue } from "./useInitialValue"; +import { useSelection } from "./useSelection"; import { useTableContextMenu } from "./useTableContextMenu"; import { useHandleTableContextMenu } from "./context-menu"; import { useCellEditing } from "./useCellEditing"; @@ -70,13 +71,20 @@ export interface TableHookProps extends MeasuredProps, Pick< TableProps, + | "allowDragDrop" | "availableColumns" | "config" | "dataSource" + | "disableFocus" + | "highlightedIndex" + | "id" | "navigationStyle" | "onAvailableColumnsChange" | "onConfigChange" + | "onDragStart" + | "onDrop" | "onFeatureInvocation" + | "onHighlight" | "onSelect" | "onSelectionChange" | "onRowClick" @@ -99,15 +107,22 @@ const addColumn = ( }); export const useTable = ({ + allowDragDrop = false, availableColumns, config, containerRef, dataSource, + disableFocus, headerHeight = 25, + highlightedIndex: highlightedIndexProp, + id, navigationStyle = "cell", onAvailableColumnsChange, onConfigChange, + onDragStart, + onDrop, onFeatureInvocation, + onHighlight, onRowClick: onRowClickProp, onSelect, onSelectionChange, @@ -116,7 +131,6 @@ export const useTable = ({ selectionModel, }: TableHookProps) => { const [rowCount, setRowCount] = useState(dataSource.size); - const dataSourceRef = useRef(); if (dataSource === undefined) { throw Error("no data source provided to Vuu Table"); } @@ -217,7 +231,7 @@ export const useTable = ({ [dispatchColumnAction] ); - const { data, getSelectedRows, range, setRange } = useDataSource({ + const { data, dataRef, getSelectedRows, range, setRange } = useDataSource({ dataSource, onFeatureInvocation, renderBufferSize, @@ -467,6 +481,7 @@ export const useTable = ({ }); const { + highlightedIndexRef, navigate, onFocus: navigationFocus, onKeyDown: navigationKeyDown, @@ -474,9 +489,12 @@ export const useTable = ({ } = useKeyboardNavigation({ columnCount: columns.filter((c) => c.hidden !== true).length, containerRef, + disableFocus, + highlightedIndex: highlightedIndexProp, navigationStyle, requestScroll, rowCount: dataSource?.size, + onHighlight, viewportRange: range, viewportRowCount: viewportMeasurements.rowCount, }); @@ -499,16 +517,6 @@ export const useTable = ({ [editingFocus, navigationFocus] ); - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - navigationKeyDown(e); - if (!e.defaultPrevented) { - editingKeyDown(e); - } - }, - [navigationKeyDown, editingKeyDown] - ); - const onContextMenu = useTableContextMenu({ columns, data, @@ -553,12 +561,29 @@ export const useTable = ({ [dataSource, onSelectionChange] ); - const selectionHookOnRowClick = useSelection({ + const { + onKeyDown: selectionHookKeyDown, + onRowClick: selectionHookOnRowClick, + } = useSelection({ + highlightedIndexRef, onSelect, onSelectionChange: handleSelectionChange, selectionModel, }); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + navigationKeyDown(e); + if (!e.defaultPrevented) { + editingKeyDown(e); + } + if (!e.defaultPrevented) { + selectionHookKeyDown(e); + } + }, + [navigationKeyDown, editingKeyDown, selectionHookKeyDown] + ); + const handleRowClick = useCallback( (row, rangeSelect, keepExistingSelection) => { selectionHookOnRowClick(row, rangeSelect, keepExistingSelection); @@ -585,7 +610,7 @@ export const useTable = ({ }); }, [dataSource, dispatchColumnAction]); - const handleDrop = useCallback( + const handleDropColumnHeader = useCallback( (moveFrom: number, moveTo: number) => { const column = tableConfig.columns[moveFrom]; @@ -604,40 +629,89 @@ export const useTable = ({ [dataSource.config, dispatchColumnAction, onConfigChange, tableConfig] ); + const handleDropRow = useCallback( + (dragDropState) => { + onDrop?.(dragDropState); + }, + [onDrop] + ); + const handleDataEdited = useCallback( async (row, columnName, value) => dataSource.applyEdit(row, columnName, value), [dataSource] ); - const { onMouseDown: dragDropHookHandleMouseDown, ...dragDropHook } = + // Drag Drop column headers + const { + onMouseDown: columnHeaderDragMouseDown, + draggable: draggableColumn, + ...dragDropHook + } = useDragDrop({ + allowDragDrop: true, + containerRef, + // this is for useDragDropNext + draggableClassName: `vuuTableNext`, + // extendedDropZone: overflowedItems.length > 0, + onDrop: handleDropColumnHeader, + orientation: "horizontal", + itemQuery: ".vuuTableNextHeaderCell", + }); + + const handleDragStartRow = useCallback( + (dragDropState) => { + const { initialDragElement } = dragDropState; + const rowIndex = initialDragElement.ariaRowIndex; + if (rowIndex) { + const index = parseInt(rowIndex); + const row = dataRef.current.find((row) => row[0] === index); + console.log(`handleDragStartRow setPayload`, { + row, + }); + if (row) { + dragDropState.setPayload(row); + } else { + // should we abort the operation ? + } + } + onDragStart?.(dragDropState); + }, + [dataRef, onDragStart] + ); + + // Drag Drop rowss + const { onMouseDown: rowDragMouseDown, draggable: draggableRow } = useDragDrop({ - allowDragDrop: true, + allowDragDrop, containerRef, - // this is for useDragDropNext draggableClassName: `vuuTableNext`, - // extendedDropZone: overflowedItems.length > 0, - onDrop: handleDrop, - orientation: "horizontal", - itemQuery: ".vuuTableNextHeaderCell", + id, + onDragStart: handleDragStartRow, + onDrop: handleDropRow, + orientation: "vertical", + itemQuery: ".vuuTableNextRow", }); const headerProps = { onClick: onHeaderClick, - onMouseDown: dragDropHookHandleMouseDown, + onMouseDown: columnHeaderDragMouseDown, onResize: onHeaderResize, }; return { ...containerProps, + draggableColumn, + draggableRow, onBlur: editingBlur, onFocus: handleFocus, onKeyDown: handleKeyDown, + onMouseDown: rowDragMouseDown, columnMap, columns, data, handleContextMenuAction, headerProps, + highlightedIndex: highlightedIndexRef.current, menuBuilder, onContextMenu, onDataEdited: handleDataEdited, diff --git a/vuu-ui/packages/vuu-table/src/table/TableRow.tsx b/vuu-ui/packages/vuu-table/src/table/TableRow.tsx index ffbe9d587..9f8b4925b 100644 --- a/vuu-ui/packages/vuu-table/src/table/TableRow.tsx +++ b/vuu-ui/packages/vuu-table/src/table/TableRow.tsx @@ -1,5 +1,8 @@ import { DataSourceRow } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { + KeyedColumnDescriptor, + RowClickHandler, +} from "@finos/vuu-datagrid-types"; import { ColumnMap, isGroupColumn, @@ -11,7 +14,6 @@ import { } from "@finos/vuu-utils"; import cx from "classnames"; import { HTMLAttributes, memo, MouseEvent, useCallback } from "react"; -import { RowClickHandler } from "./dataTableTypes"; import { TableCell } from "./TableCell"; import { TableGroupCell } from "./TableGroupCell"; diff --git a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts index 4faded45f..2468fdb3d 100644 --- a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts +++ b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts @@ -2,41 +2,45 @@ import { DataSource, SchemaColumn, VuuFeatureInvocationMessage, - VuuFeatureMessage, } from "@finos/vuu-data"; import { DataSourceRow } from "@finos/vuu-data-types"; import { KeyedColumnDescriptor, + RowClickHandler, SelectionChangeHandler, TableConfig, TableHeadings, + TableRowClickHandler, TableSelectionModel, } from "@finos/vuu-datagrid-types"; -import { VuuDataRow } from "@finos/vuu-protocol-types"; import { MeasuredContainerProps } from "@finos/vuu-layout"; +import { DragStartHandler, dragStrategy } from "@finos/vuu-ui-controls"; import { FC, MouseEvent } from "react"; import { RowProps } from "../table-next/Row"; -export type TableRowClickHandler = (row: VuuDataRow) => void; // TODO implement a Model object to represent a row data for better API export type TableRowSelectHandler = (row: DataSourceRow) => void; export type TableNavigationStyle = "none" | "cell" | "row"; -export interface TableProps extends Omit { +export interface TableProps + extends Omit { Row?: FC; allowConfigEditing?: boolean; + allowDragDrop?: boolean | dragStrategy; /** * required if a fully featured column picker is to be available */ availableColumns?: SchemaColumn[]; config: TableConfig; dataSource: DataSource; + disableFocus?: boolean; headerHeight?: number; /** * Defined how focus navigation within data cells will be handled by table. * Default is cell. */ + highlightedIndex?: number; navigationStyle?: TableNavigationStyle; /** * required if a fully featured column picker is to be available. @@ -50,12 +54,15 @@ export interface TableProps extends Omit { * prop, table state can be persisted across sessions. */ onConfigChange?: (config: TableConfig) => void; + onDragStart?: DragStartHandler; + onDrop?: () => void; /** * When a Vuu feature e.g. context menu action, has been invoked, the Vuu server * response must be handled. This callback provides that response. */ onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; + onHighlight?: (idx: number) => void; /** * callback invoked when user 'clicks' a table row. CLick triggered either * via mouse click or keyboard (default ENTER); @@ -125,9 +132,3 @@ export interface Viewport { rowCount: number; // contentWidth: number; } - -export type RowClickHandler = ( - row: DataSourceRow, - rangeSelect: boolean, - keepExistingSelection: boolean -) => void; diff --git a/vuu-ui/packages/vuu-table/src/table/useSelection.ts b/vuu-ui/packages/vuu-table/src/table/useSelection.ts index 7abab6794..c459ac9a5 100644 --- a/vuu-ui/packages/vuu-table/src/table/useSelection.ts +++ b/vuu-ui/packages/vuu-table/src/table/useSelection.ts @@ -1,4 +1,5 @@ import { + RowClickHandler, Selection, SelectionChangeHandler, TableSelectionModel, @@ -11,7 +12,6 @@ import { } from "@finos/vuu-utils"; import { DataSourceRow } from "@finos/vuu-data-types"; import { useCallback, useRef } from "react"; -import { RowClickHandler } from "./dataTableTypes"; const { IDX } = metadataKeys; diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/index.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/index.ts index e0d5fed6b..4c54dfdc4 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/index.ts @@ -1,8 +1,9 @@ export * from "./collectionProvider"; export * from "./collectionTypes"; export * from "./itemToString"; +export * from "./useCollectionItems"; +export * from "./useControlled"; export * from "./use-resize-observer"; export * from "./navigationTypes"; export * from "./selectionTypes"; -export * from "./useCollectionItems"; -export * from "./useControlled"; +export * from "./useStateRef"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts index 3196773bc..304d18aff 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/selectionTypes.ts @@ -76,7 +76,7 @@ export interface ListHandlers { export interface SelectionHookProps extends SelectionProps { containerRef: RefObject; disableSelection?: boolean; - highlightedIdx: number; + highlightedIndex: number; itemQuery: string; label?: string; onClick?: MouseEventHandler; diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts index 0678b8942..9064e0fb2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useSelection.ts @@ -11,7 +11,6 @@ import { useRef, } from "react"; import { - ListHandlers, SelectionHookProps, SelectionHookResult, selectionIsDisallowed, @@ -24,8 +23,6 @@ export const GROUP_SELECTION_NONE = "none"; export const GROUP_SELECTION_SINGLE = "single"; export const GROUP_SELECTION_CASCADE = "cascade"; -const NO_SELECTION_HANDLERS: ListHandlers = {}; - export type GroupSelectionMode = "none" | "single" | "cascade"; const defaultSelectionKeys = ["Enter", " "]; @@ -39,7 +36,7 @@ export const useSelection = ({ defaultSelected, disableSelection = false, // groupSelection = GROUP_SELECTION_NONE, - highlightedIdx, + highlightedIndex, itemQuery, onClick, // label, @@ -180,7 +177,7 @@ export const useSelection = ({ const handleKeyDown = useCallback( (evt: KeyboardEvent) => { const { current: container } = containerRef; - const element = getElementByDataIndex(container, highlightedIdx); + const element = getElementByDataIndex(container, highlightedIndex); if (isSelectableElement(element)) { if (isSelectionEvent(evt) || (tabToSelect && evt.key === "Tab")) { // We do not inhibit Tab behaviour, if we are selecting on Tab then we apply @@ -190,18 +187,18 @@ export const useSelection = ({ } selectItemAtIndex( evt, - highlightedIdx, + highlightedIndex, false, evt.ctrlKey || evt.metaKey ); if (isExtendedSelect) { - lastActive.current = highlightedIdx; + lastActive.current = highlightedIndex; } } } }, [ - highlightedIdx, + highlightedIndex, containerRef, isSelectionEvent, tabToSelect, @@ -226,25 +223,25 @@ export const useSelection = ({ const handleClick = useCallback( (evt: MouseEvent) => { const { current: container } = containerRef; - const element = getElementByDataIndex(container, highlightedIdx); + const element = getElementByDataIndex(container, highlightedIndex); if (!disableSelection && isSelectableElement(element)) { evt.preventDefault(); evt.stopPropagation(); selectItemAtIndex( evt, - highlightedIdx, + highlightedIndex, evt.shiftKey, evt.ctrlKey || evt.metaKey ); if (isExtendedSelect) { - lastActive.current = highlightedIdx; + lastActive.current = highlightedIndex; } } onClick?.(evt); }, [ containerRef, - highlightedIdx, + highlightedIndex, disableSelection, onClick, selectItemAtIndex, diff --git a/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useStateRef.ts b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useStateRef.ts new file mode 100644 index 000000000..e28c8659e --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/common-hooks/useStateRef.ts @@ -0,0 +1,31 @@ +import { + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useRef, + useState, +} from "react"; + +const isSimpleStateValue = (arg: SetStateAction): arg is T => + typeof arg !== "function"; + +// Keeps a ref value in sync with a state value +export const useStateRef = ( + initialValue: T +): [MutableRefObject, Dispatch>] => { + const [value, _setValue] = useState(initialValue); + const ref = useRef(value); + + const setValue = useCallback>>((arg) => { + if (isSimpleStateValue(arg)) { + ref.current = arg; + _setValue(arg); + } else { + const { current: prev } = ref; + ref.current = arg(prev); + _setValue(ref.current); + } + }, []); + return [ref, setValue]; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx index 03d5b71eb..1b8d98606 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/cycle-state-button/CycleStateButton.tsx @@ -37,7 +37,6 @@ export const CycleStateButton = forwardRef(function CycleStateButton( const handleClick = useCallback( (evt: SyntheticEvent) => { const nextValue = getNextValue(value, values); - console.log(`CycleStateButton handleClick ${value} => ${nextValue}`); onCommit(evt, nextValue as VuuColumnDataType).then((response) => { if (response !== true) { console.error(response); diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx index 70f761135..ec73cea1e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx @@ -6,12 +6,15 @@ import React, { useMemo, } from "react"; import { DragDropState } from "./DragDropState"; -import { MouseOffset } from "./dragDropTypesNext"; -import { ResumeDragHandler, useGlobalDragDrop } from "./useGlobalDragDrop"; +import { + GlobalDropHandler, + ResumeDragHandler, + useGlobalDragDrop, +} from "./useGlobalDragDrop"; const NO_DRAG_CONTEXT = { - isDragSource: false, - isDropTarget: false, + isDragSource: undefined, + isDropTarget: undefined, register: () => undefined, }; @@ -25,7 +28,8 @@ export type DragOutHandler = ( export type DragDropRegistrationFn = ( id: string, - resumeDrag?: ResumeDragHandler + resumeDrag: ResumeDragHandler | false, + onDrop?: GlobalDropHandler ) => void; export type EndOfDragOperationHandler = (id: string) => void; @@ -74,6 +78,7 @@ export const DragDropProvider = ({ () => new Map(), [] ); + const dropHandlers = useMemo(() => new Map(), []); const handleDragOverDropTarget = useCallback( (dropTargetId: string, dragDropState: DragDropState) => { const resumeDrag = resumeDragHandlers.get(dropTargetId); @@ -86,8 +91,19 @@ export const DragDropProvider = ({ [resumeDragHandlers] ); + const handleDrop = useCallback( + (dropTargetId: string, dragDropState: DragDropState) => { + const handleDrop = dropHandlers.get(dropTargetId); + if (handleDrop) { + handleDrop(dragDropState); + } + }, + [dropHandlers] + ); + const { measuredDropTargetsRef, resumeDrag } = useGlobalDragDrop({ onDragOverDropTarget: handleDragOverDropTarget, + onDrop: handleDrop, }); const [dragSources, dropTargets] = useMemo(() => { const sources = new Map(); @@ -123,6 +139,7 @@ export const DragDropProvider = ({ const onDragOut = useCallback( (id, dragDropState) => { + console.log("DragDropProvider onDragOut"); // we call releaseItem if and when the dragged item is dropped onto a remote dropTarget measuredDropTargetsRef.current = measureDropTargets(dragSources.get(id)); resumeDrag(dragDropState); @@ -136,12 +153,15 @@ export const DragDropProvider = ({ }, []); const registerDragDropParty = useCallback( - (id, resumeDrag) => { + (id, resumeDrag, onDrop) => { + console.log(`register drag drop agent #${id}`); if (resumeDrag) { resumeDragHandlers.set(id, resumeDrag); + } else if (onDrop) { + dropHandlers.set(id, onDrop); } }, - [resumeDragHandlers] + [dropHandlers, resumeDragHandlers] ); const contextValue: DragDropContextProps = useMemo( @@ -169,8 +189,8 @@ export const DragDropProvider = ({ }; export interface DragDropProviderResult { - isDragSource: boolean; - isDropTarget: boolean; + isDragSource?: boolean; + isDropTarget?: boolean; onDragOut?: DragOutHandler; onEndOfDragOperation?: (id: string) => void; register: DragDropRegistrationFn; @@ -184,7 +204,7 @@ export const useDragDropProvider = (id?: string): DragDropProviderResult => { onEndOfDragOperation, registerDragDropParty, } = useContext(DragDropContext); - if (id) { + if (id && (dragSources || dropTargets)) { const isDragSource = dragSources?.has(id) ?? false; const isDropTarget = dropTargets?.has(id) ?? false; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx index 87391b994..1e8d3be34 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/Draggable.tsx @@ -3,7 +3,9 @@ import cx from "classnames"; import { CSSProperties, forwardRef, + HTMLAttributes, MutableRefObject, + RefCallback, TransitionEventHandler, useCallback, useMemo, @@ -14,58 +16,66 @@ import "./Draggable.css"; const makeClassNames = (classNames: string) => classNames.split(" ").map((className) => `vuuDraggable-${className}`); -export const Draggable = forwardRef< - HTMLDivElement, - { - wrapperClassName: string; - element: HTMLElement; - onTransitionEnd?: TransitionEventHandler; - scale?: number; - style: CSSProperties; - } ->(function Draggable( - { wrapperClassName, element, onTransitionEnd, style, scale = 1 }, - forwardedRef -) { - const callbackRef = useCallback( - (el: HTMLDivElement) => { - if (el) { - el.innerHTML = ""; - el.appendChild(element); - if (scale !== 1) { - el.style.transform = `scale(${scale},${scale})`; + +export interface DraggableProps extends HTMLAttributes { + wrapperClassName: string; + element: HTMLElement; + onDropped?: () => void; + onTransitionEnd?: TransitionEventHandler; + scale?: number; + style: CSSProperties; +} + +export const Draggable = forwardRef( + function Draggable( + { wrapperClassName, element, onDropped, onTransitionEnd, style, scale = 1 }, + forwardedRef + ) { + const handleVuuDrop = useCallback(() => { + onDropped?.(); + }, [onDropped]); + + const callbackRef = useCallback>( + (el: HTMLDivElement) => { + if (el) { + el.innerHTML = ""; + el.appendChild(element); + if (scale !== 1) { + el.style.transform = `scale(${scale},${scale})`; + } + el.addEventListener("vuu-dropped", handleVuuDrop); } - } - }, - [element, scale] - ); - const forkedRef = useForkRef(forwardedRef, callbackRef); + }, + [element, handleVuuDrop, scale] + ); + const forkedRef = useForkRef(forwardedRef, callbackRef); - const position = useMemo( - () => ({ - left: 0, - top: 0, - }), - [] - ); + const position = useMemo( + () => ({ + left: 0, + top: 0, + }), + [] + ); - return ( - - -
- - - ); -}); + return ( + + +
+ + + ); + } +); // const colors = ["black", "red", "green", "yellow"]; // let color_idx = 0; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts index c8e7e1eac..6bd327b31 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts @@ -27,7 +27,11 @@ export type dimensionsType = { //----------------------------------- -export type dragStrategy = "drop-indicator" | "natural-movement"; +export type dragStrategy = + | "drop-indicator" + | "natural-movement" + | "drag-copy" + | "drop-only"; export type Direction = "fwd" | "bwd"; export const FWD: Direction = "fwd"; @@ -52,7 +56,7 @@ export interface DragHookResult { isDragging: boolean; isScrolling: RefObject; onMouseDown?: MouseEventHandler; - revealOverflowedItems: boolean; + revealOverflowedItems?: boolean; } export interface InternalDragHookResult @@ -60,8 +64,8 @@ export interface InternalDragHookResult beginDrag: (dragElement: HTMLElement) => void; drag: (dragPos: number, mouseMoveDirection: "fwd" | "bwd") => void; drop: () => void; - handleScrollStart: () => void; - handleScrollStop: ( + handleScrollStart?: () => void; + handleScrollStop?: ( scrollDirection: "fwd" | "bwd", _scrollPos: number, atEnd: boolean diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts index 7abc8f959..9585f0323 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts @@ -9,7 +9,7 @@ const TOP_BOTTOM = ["top", "bottom"]; export const NOT_OVERFLOWED = ":not(.wrapped)"; export const NOT_HIDDEN = ':not([aria-hidden="true"])'; -// TODO figure out which of these sttributes we no longer need +// TODO figure out which of these attributes we no longer need export type MeasuredDropTarget = { /** The index position currently occupied by this item. If draggable @@ -355,3 +355,33 @@ export const dropTargetsDebugString = (dropTargets: MeasuredDropTarget[]) => )}) ${d.element?.textContent} ` ) .join(""); + +export const getScrollableContainer = ( + container: HTMLElement, + itemQuery: string +) => { + // TODO if this is too fragile a way to identify scrollable container, we + // can add a prop to pass it 'scrollableContainerQuery' + const firstItem = container.querySelector( + `${itemQuery}:not([aria-hidden="true"])` + ); + // generally, we expect the immediateParent to be a contentContainer, the + // parent of that will be the scrollable container. This may or may not be + // the outer container (likely not) + const immediateParent = firstItem?.parentElement; + if (immediateParent === container) { + return container; + } else { + return immediateParent?.parentElement as HTMLElement; + } +}; + +export const isContainerScrollable = ( + scrollableContainer: HTMLElement, + orientation: orientationType +) => { + const { SCROLL_SIZE, CLIENT_SIZE } = dimensions(orientation); + const { [SCROLL_SIZE]: scrollSize, [CLIENT_SIZE]: clientSize } = + scrollableContainer; + return scrollSize > clientSize; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts new file mode 100644 index 000000000..e4ec1744c --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropCopy.ts @@ -0,0 +1,37 @@ +import { useCallback, useRef } from "react"; + +import { + InternalDragDropProps, + InternalDragHookResult, + ViewportRange, +} from "./dragDropTypesNext"; + +export const useDragDropCopy = ({ + selected, + viewportRange, +}: InternalDragDropProps): InternalDragHookResult => { + const rangeRef = useRef(); + rangeRef.current = viewportRange; + + const beginDrag = useCallback( + (dragElement: HTMLElement) => { + if ( + dragElement.ariaSelected && + Array.isArray(selected) && + selected.length > 1 + ) { + console.log("its a selected element, and we have a multi select"); + } + }, + [selected] + ); + + const drag = useCallback(() => undefined, []); + const drop = useCallback(() => undefined, []); + + return { + beginDrag, + drag, + drop, + }; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx index 3e2b83493..93231dafb 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx @@ -10,7 +10,7 @@ import { useDragDisplacers } from "./useDragDisplacers"; import { dispatchMouseEvent } from "@finos/vuu-utils"; import { dimensions, - dropTargetsDebugString, + // dropTargetsDebugString, getIndexOfDraggedItem, getNextDropTarget, MeasuredDropTarget, @@ -43,8 +43,6 @@ export const useDragDropNaturalMovement = ({ const draggedItemRef = useRef(); const fullItemQuery = `:is(${itemQuery}${NOT_OVERFLOWED}${NOT_HIDDEN},.vuuOverflowContainer-OverflowIndicator)`; - // const { setMeasurements: setVizData } = useListViz(); - const indexOf = (dropTarget: MeasuredDropTarget) => measuredDropTargets.current.findIndex((d) => d.id === dropTarget.id); @@ -129,7 +127,7 @@ export const useDragDropNaturalMovement = ({ )); if (internalDrag) { - console.log(dropTargetsDebugString(dropTargets)); + // console.log(dropTargetsDebugString(dropTargets)); const indexOfDraggedItem = getIndexOfDraggedItem(dropTargets); const draggedItem = dropTargets[indexOfDraggedItem]; if (draggedItem && container) { @@ -153,7 +151,7 @@ export const useDragDropNaturalMovement = ({ const index = dropTargets.indexOf(dropTarget); const { start, end, mid } = dropTarget; - console.log(`nextDropTarget ${dropTarget.element.textContent}`); + // console.log(`nextDropTarget ${dropTarget.element.textContent}`); // need to compute the correct position of this // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -178,7 +176,7 @@ export const useDragDropNaturalMovement = ({ target.start += size; } - console.log(dropTargetsDebugString(dropTargets)); + // console.log(dropTargetsDebugString(dropTargets)); const displaceFunction = dropTarget.isLast ? displaceLastItem diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx index 752e4ccfb..8f94ce721 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx @@ -1,4 +1,5 @@ import { isOverflowElement } from "@finos/vuu-layout"; +import { dispatchCustomEvent } from "@finos/vuu-utils"; import { MouseEventHandler, useCallback, @@ -21,9 +22,12 @@ import { cloneElement, constrainRect, dimensions, + getScrollableContainer, + isContainerScrollable, NOT_OVERFLOWED, } from "./drop-target-utils"; import { ScrollStopHandler, useAutoScroll } from "./useAutoScroll"; +import { useDragDropCopy } from "./useDragDropCopy"; import { useDragDropIndicator } from "./useDragDropIndicator"; import { useDragDropNaturalMovement } from "./useDragDropNaturalMovementNext"; import { ResumeDragHandler } from "./useGlobalDragDrop"; @@ -114,7 +118,8 @@ export const useDragDropNext: DragDropHook = ({ const startPosRef = useRef({ x: 0, y: 0 }); /** references the dragged Item during its final 'settling' phase post drop */ const settlingItemRef = useRef(null); - + /** the container which will scroll if content overflows */ + const scrollableContainerRef = useRef(null); const dropPosRef = useRef(-1); const dropIndexRef = useRef(-1); @@ -188,6 +193,9 @@ export const useDragDropNext: DragDropHook = ({ ); const terminateDrag = useCallback(() => { + const { current: settlingItem } = settlingItemRef; + settlingItemRef.current = null; + const { current: toIndex } = dropIndexRef; const droppedItem = containerRef.current?.querySelector( `${itemQuery}[data-index="${toIndex}"]` @@ -195,18 +203,21 @@ export const useDragDropNext: DragDropHook = ({ if (droppedItem) { droppedItem.classList.remove("vuuDropTarget-settling"); } - dropIndexRef.current = -1; onDropSettle?.(toIndex); setDraggableStatus((status) => ({ ...status, draggable: undefined, })); + + if (settlingItem) { + dispatchCustomEvent(settlingItem, "vuu-dropped"); + } }, [containerRef, itemQuery, onDropSettle]); const getScrollDirection = useCallback( (mousePos: number) => { - if (containerRef.current && dragDropStateRef.current) { + if (scrollableContainerRef.current && dragDropStateRef.current) { const { mouseOffset } = dragDropStateRef.current; const { POS, SCROLL_POS, SCROLL_SIZE, CLIENT_SIZE } = @@ -215,7 +226,7 @@ export const useDragDropNext: DragDropHook = ({ [SCROLL_POS]: scrollPos, [SCROLL_SIZE]: scrollSize, [CLIENT_SIZE]: clientSize, - } = containerRef.current; + } = scrollableContainerRef.current; const maxScroll = scrollSize - clientSize; const canScrollFwd = scrollPos < maxScroll; @@ -227,7 +238,7 @@ export const useDragDropNext: DragDropHook = ({ return bwd ? "bwd" : fwd ? "fwd" : ""; } }, - [containerRef, orientation] + [scrollableContainerRef, orientation] ); const useDragDropHook: InternalHook = @@ -235,6 +246,8 @@ export const useDragDropNext: DragDropHook = ({ ? useDragDropNaturalMovement : allowDragDrop === "drop-indicator" ? useDragDropIndicator + : allowDragDrop === "drag-copy" + ? useDragDropCopy : noDragDrop; const onScrollingStopped = useCallback( @@ -245,7 +258,7 @@ export const useDragDropNext: DragDropHook = ({ ); const { isScrolling, startScrolling, stopScrolling } = useAutoScroll({ - containerRef, + containerRef: scrollableContainerRef, onScrollingStopped, orientation, }); @@ -301,7 +314,14 @@ export const useDragDropNext: DragDropHook = ({ ? Math.abs(lastClientContraPos - clientContraPos) : 0; - if (dragDropStateRef.current && dragOutDistance - dragDistance > 5) { + // If isDropTarget is false, there are configured dropTargets in context + // but this is not one, so drag will be handed straight over to DragProvider + // (global drag). If isDropTarget is undefined, we have no DragProvider + // so we are dealing with a simple local drag drop operation. + const handoverToProvider = + isDropTarget === false || dragOutDistance - dragDistance > 5; + + if (dragDropStateRef.current && handoverToProvider) { if (onDragOut?.(id as string, dragDropStateRef.current)) { // TODO create a cleanup function removeDragHandlers(); @@ -313,7 +333,15 @@ export const useDragDropNext: DragDropHook = ({ return true; } }, - [id, isDragSource, onDragOut, orientation, releaseDrag, removeDragHandlers] + [ + id, + isDragSource, + isDropTarget, + onDragOut, + orientation, + releaseDrag, + removeDragHandlers, + ] ); const dragMouseMoveHandler = useCallback( @@ -327,7 +355,6 @@ export const useDragDropNext: DragDropHook = ({ const { current: dragDropState } = dragDropStateRef; if (dragHandedOvertoProvider(dragDistance, clientContraPos)) { - console.log("drag handed over to provider"); return; } @@ -352,7 +379,7 @@ export const useDragDropNext: DragDropHook = ({ isScrollableRef.current && !isScrolling.current ) { - handleScrollStart(); + handleScrollStart?.(); startScrolling(scrollDirection, 1); } else if (!scrollDirection && isScrolling.current) { stopScrolling(); @@ -372,15 +399,12 @@ export const useDragDropNext: DragDropHook = ({ } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps [ drag, + dragHandedOvertoProvider, getScrollDirection, handleScrollStart, - id, - isDragSource, isScrolling, - onDragOut, orientation, startScrolling, stopScrolling, @@ -408,12 +432,9 @@ export const useDragDropNext: DragDropHook = ({ (dragDropState: DragDropState) => { dragDropStateRef.current = dragDropState; // Note this is using the draggable element rather than the original draggedElement - const { draggableElement, mouseOffset, initialDragElement } = - dragDropState; + const { draggableElement, mouseOffset } = dragDropState; const { current: container } = containerRef; - console.log({ container, draggableElement, initialDragElement }); - if (container && draggableElement) { const containerRect = container.getBoundingClientRect(); const draggableRect = draggableElement.getBoundingClientRect(); @@ -441,11 +462,16 @@ export const useDragDropNext: DragDropHook = ({ const dragElement = getDraggableElement(target, itemQuery); const { current: container } = containerRef; if (container && dragElement) { - const { SCROLL_SIZE, CLIENT_SIZE } = dimensions(orientation); + const scrollableContainer = getScrollableContainer( + container, + itemQuery + ); - const { [SCROLL_SIZE]: scrollSize, [CLIENT_SIZE]: clientSize } = - container; - isScrollableRef.current = scrollSize > clientSize; + isScrollableRef.current = isContainerScrollable( + scrollableContainer, + orientation + ); + scrollableContainerRef.current = scrollableContainer; const containerRect = container.getBoundingClientRect(); const draggableRect = dragElement.getBoundingClientRect(); @@ -468,6 +494,7 @@ export const useDragDropNext: DragDropHook = ({ draggable: ( { + // TODO runtime check here for valid drop targets ? + const { current: container } = containerRef; // We don't want to prevent other handlers on this element from working // but we do want to stop a drag drop being initiated on a bubbled event. @@ -579,18 +608,24 @@ export const useDragDropNext: DragDropHook = ({ terminateDrag(); } }); - } else { - console.log(`dont have the dropped item (at ${dropPos})`); } - settlingItemRef.current = null; + // settlingItemRef.current = null; } }, [containerRef, itemQuery, settlingItem, terminateDrag]); useEffect(() => { if (id && (isDragSource || isDropTarget)) { - register(id, resumeDrag); + register(id, allowDragDrop === "drop-only" ? false : resumeDrag, onDrop); } - }, [id, isDragSource, isDropTarget, register, resumeDrag]); + }, [ + allowDragDrop, + id, + isDragSource, + isDropTarget, + onDrop, + register, + resumeDrag, + ]); return { ...dragResult, diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts index f3d67e188..579a452c2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts @@ -1,19 +1,23 @@ -import { boxContainsPoint } from "@finos/vuu-utils"; +import { boxContainsPoint, dispatchCustomEvent } from "@finos/vuu-utils"; import { useCallback, useRef } from "react"; import { MeasuredTarget } from "./DragDropProvider"; import { DragDropState } from "./DragDropState"; import { MouseOffset } from "./dragDropTypesNext"; export type ResumeDragHandler = (dragDropState: DragDropState) => boolean; +export type GlobalDropHandler = (dragDropState: DragDropState) => void; export const useGlobalDragDrop = ({ onDragOverDropTarget, + onDrop, }: { onDragOverDropTarget: ( dropTargetId: string, dragDropState: DragDropState ) => boolean; + onDrop: (dropTargetId: string, dragDropState: DragDropState) => void; }) => { + const dropTargetRef = useRef(); const measuredDropTargetsRef = useRef>(); const dragDropStateRef = useRef(null); @@ -48,14 +52,23 @@ export const useGlobalDragDrop = ({ draggableElement.style.top = `${dragPosY}px`; draggableElement.style.left = `${dragPosX}px`; - const dropTarget = overDropTarget(dragPosX, dragPosY); - if (dropTarget) { - if (onDragOverDropTarget(dropTarget, dragDropState)) { + const dropTargetId = overDropTarget(dragPosX, dragPosY); + if (dropTargetId) { + const dropTargetWillResumeDrag = onDragOverDropTarget( + dropTargetId, + dragDropState + ); + if (dropTargetWillResumeDrag) { // prettier-ignore document.removeEventListener("mousemove", dragMouseMoveHandler, false); document.removeEventListener("mouseup", dragMouseUpHandler, false); dragDropStateRef.current = null; + dropTargetRef.current = undefined; + } else { + dropTargetRef.current = dropTargetId; } + } else { + dropTargetRef.current = undefined; } } }, @@ -66,7 +79,17 @@ export const useGlobalDragDrop = ({ const dragMouseUpHandler = useCallback(() => { document.removeEventListener("mousemove", dragMouseMoveHandler, false); document.removeEventListener("mouseup", dragMouseUpHandler, false); - }, [dragMouseMoveHandler]); + const { current: dragDropState } = dragDropStateRef; + if (dragDropState) { + dragDropStateRef.current = null; + if (dropTargetRef.current) { + onDrop(dropTargetRef.current, dragDropState); + } + if (dragDropState.draggableElement) { + dispatchCustomEvent(dragDropState.draggableElement, "vuu-dropped"); + } + } + }, [dragMouseMoveHandler, onDrop]); const resumeDrag = useCallback( (dragDropState) => { diff --git a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts index 616a555be..8af1b4f7f 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts @@ -1,6 +1,7 @@ -import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; import { DataItemCommitHandler } from "@finos/vuu-datagrid-types"; import { useLayoutEffectSkipFirst } from "@finos/vuu-layout"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { dispatchCustomEvent } from "@finos/vuu-utils"; import { FocusEventHandler, FormEventHandler, @@ -27,11 +28,6 @@ export interface EditableTextHookProps< type?: "string" | "number" | "boolean"; } -export const dispatchCommitEvent = (el: HTMLElement) => { - const commitEvent = new Event("vuu-commit"); - el.dispatchEvent(commitEvent); -}; - export const useEditableText = ({ clientSideEditValidationCheck, initialValue, @@ -61,7 +57,7 @@ export const useEditableText = ({ onCommit(value as T).then((response) => { if (response === true) { isDirtyRef.current = false; - dispatchCommitEvent(target); + dispatchCustomEvent(target, "vuu-commit"); } else { setMessage(response); } @@ -69,7 +65,7 @@ export const useEditableText = ({ } } else { // why, if not dirty ? - dispatchCommitEvent(target); + dispatchCustomEvent(target, "vuu-commit"); hasCommittedRef.current = false; } }, diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx index e87ce9fa3..74058322b 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx @@ -54,11 +54,12 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( const id = useId(idProp); const { - controlProps, + highlightedIndex, inputProps, isOpen, onOpenChange, tableHandlers, + tableRef, value, } = useInstrumentPicker({ columnMap, @@ -76,7 +77,7 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( ...TableProps, config: { ...TableProps.config, - showHighlightedRow: true, + zebraStripes: false, }, }; @@ -95,7 +96,6 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( @@ -107,8 +107,10 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( {...tableHandlers} className={`${classBase}-list`} height={200} + highlightedIndex={highlightedIndex} dataSource={dataSource} navigationStyle="row" + ref={tableRef} showColumnHeaders={false} /> diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts index 49f54efb3..e9cbec1d6 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts @@ -1,7 +1,10 @@ import { DataSource } from "@finos/vuu-data"; import { DataSourceRow } from "@finos/vuu-data-types"; import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { TableRowSelectHandler } from "@finos/vuu-table"; +import { + TableRowSelectHandler, + useControlledTableNavigation, +} from "@finos/vuu-table"; import { ColumnMap } from "@finos/vuu-utils"; import { ChangeEvent, useCallback, useMemo, useState } from "react"; import { useControlled } from "../common-hooks"; @@ -43,6 +46,9 @@ export const useInstrumentPicker = ({ name: "useDropdownList", }); + const { highlightedIndexRef, onKeyDown, tableRef } = + useControlledTableNavigation(-1, dataSource.size); + const baseFilterPattern = useMemo( // TODO make this contains once server supports it () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), @@ -53,9 +59,6 @@ export const useInstrumentPicker = ({ (open, closeReason) => { setIsOpen(open); onOpenChange?.(open, closeReason); - // if (open === false) { - // dataSource.unsubscribe(); - // } }, [onOpenChange, setIsOpen] ); @@ -94,20 +97,21 @@ export const useInstrumentPicker = ({ const inputProps = { inputProps: { autoComplete: "off", + onKeyDown, }, onChange: handleInputChange, }; - const controlProps = {}; const tableHandlers = { onSelect: handleSelectRow, }; return { - controlProps, + highlightedIndex: highlightedIndexRef.current, inputProps, isOpen, onOpenChange: handleOpenChange, tableHandlers, + tableRef, value, }; }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.css b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.css index 16e0868ec..5782c1163 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.css +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.css @@ -8,8 +8,10 @@ .vuuInstrumentSearch-inputField { --vuu-icon-size: 16px; flex: 0 0 40px; + padding: 0 12px; } .vuuInstrumentSearch-list { + --vuuMeasuredContainer-flex: 1 1 1px; background-color: var(--salt-container-primary-background); flex: 1 1 auto; } diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx index 298c038dd..89a749e0a 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx @@ -1,12 +1,17 @@ import { DataSource } from "@finos/vuu-data"; import { TableConfig } from "@finos/vuu-datagrid-types"; import { registerComponent } from "@finos/vuu-layout"; -import { TableNext, TableProps } from "@finos/vuu-table"; -import { FormField, FormFieldLabel, Input } from "@salt-ds/core"; +import { + TableNext, + TableProps, + useControlledTableNavigation, +} from "@finos/vuu-table"; +import { Input } from "@salt-ds/core"; import cx from "classnames"; import { FormEvent, HTMLAttributes, + RefCallback, useCallback, useMemo, useState, @@ -36,6 +41,7 @@ const defaultTableConfig: TableConfig = { export interface InstrumentSearchProps extends HTMLAttributes { TableProps?: Partial; + autoFocus?: boolean; dataSource: DataSource; placeHolder?: string; searchColumns?: string[]; @@ -45,6 +51,7 @@ const searchIcon = ; export const InstrumentSearch = ({ TableProps, + autoFocus = false, className, dataSource, placeHolder, @@ -57,6 +64,9 @@ export const InstrumentSearch = ({ [searchColumns] ); + const { highlightedIndexRef, onHighlight, onKeyDown, tableRef } = + useControlledTableNavigation(-1, dataSource.size); + const [searchState, setSearchState] = useState<{ searchText: string; filter: string; @@ -77,26 +87,38 @@ export const InstrumentSearch = ({ [baseFilterPattern, dataSource] ); + const searchCallbackRef = useCallback>((el) => { + setTimeout(() => { + el?.querySelector("input")?.focus(); + }, 100); + }, []); + return (
- - +
- +
diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts index 17ac6213e..32ae7942b 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/useKeyboardNavigation.ts @@ -159,6 +159,7 @@ export const useKeyboardNavigation = ({ onKeyboardNavigation, restoreLastFocus, selected, + // TODO viewportItemCount, }: NavigationHookProps): NavigationHookResult => { const lastFocus = useRef(-1); diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts index e396d668e..5408309b5 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -164,7 +164,7 @@ export const useList = ({ const selectionHook = useSelection({ containerRef, defaultSelected, - highlightedIdx: highlightedIndex, + highlightedIndex: highlightedIndex, itemQuery: ".vuuListItem", label: `${label}:useList`, onClick: onClickProp, diff --git a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx index 32787e61e..00c3444d6 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/vuu-input/VuuInput.tsx @@ -7,6 +7,7 @@ import { ForwardedRef, forwardRef, KeyboardEventHandler, + ReactElement, SyntheticEvent, useCallback, } from "react"; @@ -124,4 +125,8 @@ export const VuuInput = forwardRef(function VuuInput< {tooltipProps ? : null} ); -}); +}) as ( + props: VuuInputProps & { + ref?: ForwardedRef; + } +) => ReactElement>; diff --git a/vuu-ui/packages/vuu-utils/src/html-utils.ts b/vuu-ui/packages/vuu-utils/src/html-utils.ts index 650d60bf0..a18e5d029 100644 --- a/vuu-ui/packages/vuu-utils/src/html-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/html-utils.ts @@ -148,3 +148,13 @@ export const dispatchMouseEvent = (el: HTMLElement, type: MouseEventTypes) => { }); el.dispatchEvent(evt); }; + +export type VuuDomEventType = "vuu-commit" | "vuu-dropped"; + +export const dispatchCustomEvent = (el: HTMLElement, type: VuuDomEventType) => { + const evt = new Event(type, { + bubbles: true, + cancelable: true, + }); + el.dispatchEvent(evt); +}; diff --git a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx index 332e584be..a7f3eb185 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx @@ -1,3 +1,7 @@ +import { + registerComponent, + useLayoutContextMenuItems, +} from "@finos/vuu-layout"; import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; import { LeftNav, @@ -6,20 +10,18 @@ import { ShellProps, VuuUser, } from "@finos/vuu-shell"; -import { getDefaultColumnConfig } from "./columnMetaData"; -import { createPlaceholder } from "./createPlaceholder"; -import { useFeatures } from "./useFeatures"; import { ColumnSettingsPanel, TableSettingsPanel, } from "@finos/vuu-table-extras"; -import { - registerComponent, - useLayoutContextMenuItems, -} from "@finos/vuu-layout"; +import { getDefaultColumnConfig } from "./columnMetaData"; +import { createPlaceholder } from "./createPlaceholder"; +import { useFeatures } from "./useFeatures"; +import { DragDropProvider } from "@finos/vuu-ui-controls"; import "./App.css"; import { useRpcResponseHandler } from "./useRpcResponseHandler"; +import { useMemo } from "react"; registerComponent("ColumnSettings", ColumnSettingsPanel, "view"); registerComponent("TableSettings", TableSettingsPanel, "view"); @@ -49,33 +51,42 @@ export const App = ({ user }: { user: VuuUser }) => { const { buildMenuOptions, handleMenuAction } = useLayoutContextMenuItems(setDialogState); + const dragSource = useMemo( + () => ({ + "basket-instruments": { dropTargets: "basket-constituents" }, + }), + [] + ); + // TODO get Context from Shell return ( - - - } - saveUrl="https://localhost:8443/api/vui" - serverUrl={serverUrl} - user={user} + + - {dialog} - - + + } + saveUrl="https://localhost:8443/api/vui" + serverUrl={serverUrl} + user={user} + > + {dialog} + + + ); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx index 56a3ccf18..d25898d3b 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx @@ -40,6 +40,7 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { dialog, onClickAddBasket, onCommitBasketChange, + onDropInstrument, onSendToMarket, onTakeOffMarket, } = useBasketTrading({ @@ -85,6 +86,7 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { data-tab-title="Design" contextMenuConfig={basketDesignContextMenuConfig} dataSource={dataSourceBasketTradingConstituentJoin} + onDrop={onDropInstrument} tableSchema={basketTradingConstituentJoinSchema} /> (null); const id = useId(idProp); - const { isOpen, onClickAddBasket, onOpenChange, tableProps } = + const { isOpen, onClickAddBasket, onOpenChange, tableProps, triggerRef } = useBasketSelector({ basketInstanceId, dataSourceBasketTradingSearch, @@ -49,9 +49,11 @@ export const BasketSelector = ({ return (
@@ -86,13 +88,14 @@ export const BasketSelector = ({
{basketName} - + {status === "ON MARKET" ? ( + + ) : ( + + )}
{basketId} diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts index 805f8569d..dcbf06ac6 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts @@ -1,7 +1,8 @@ -import { TableProps, TableRowClickHandler } from "@finos/vuu-table"; -import { buildColumnMap } from "@finos/vuu-utils"; +import { TableRowClickHandler } from "@finos/vuu-datagrid-types"; +import { TableProps } from "@finos/vuu-table"; import { OpenChangeHandler, useControlled } from "@finos/vuu-ui-controls"; -import { useCallback, useMemo } from "react"; +import { buildColumnMap } from "@finos/vuu-utils"; +import { useCallback, useMemo, useRef } from "react"; import { BasketSelectorProps } from "./BasketSelector"; import { BasketSelectorRow } from "./BasketSelectorRow"; @@ -25,6 +26,7 @@ export const useBasketSelector = ({ onOpenChange, onSelectBasket, }: BasketSelectorHookProps) => { + const triggerRef = useRef(null); const [isOpen, setIsOpen] = useControlled({ controlled: isOpenProp, default: defaultIsOpen ?? false, @@ -41,8 +43,12 @@ export const useBasketSelector = ({ setIsOpen(open); onOpenChange?.(open, closeReason); if (open === false) { - console.log(`%cdisable basketSearch`, "color:red;font-weight:bold;"); dataSourceBasketTradingSearch.disable?.(); + if (closeReason !== "Tab") { + setTimeout(() => { + triggerRef.current?.focus(); + }, 100); + } } }, [dataSourceBasketTradingSearch, onOpenChange, setIsOpen] @@ -68,7 +74,7 @@ export const useBasketSelector = ({ Row: BasketSelectorRow, config: { columns: [ - { name: "instanceId", width: 365 }, + { name: "instanceId", width: 380 }, { name: "basketId", width: 100, hidden: true }, { name: "name", @@ -105,5 +111,6 @@ export const useBasketSelector = ({ onClickAddBasket: handleClickAddBasket, onOpenChange: handleOpenChange, tableProps, + triggerRef, }; }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx index 668974f03..3c73713ec 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/BasketTableEdit.tsx @@ -31,11 +31,15 @@ export const BasketTableEdit = ({ [] ); + console.log({ dataSource }); + return ( { + if (notional === undefined) { + return ""; + } else { + return notional.toLocaleString(); + } +}; + export type BasketChangeHandler = ( columnName: string, value: VuuRowDataItemType @@ -43,7 +51,6 @@ export const BasketToolbar = ({ onTakeOffMarket, }: BasketToolbarProps) => { const handleMenuAction: MenuActionHandler = () => { - console.log("Menu Action"); return true; }; @@ -92,7 +99,7 @@ export const BasketToolbar = ({ @@ -126,7 +133,7 @@ export const BasketToolbar = ({ Total USD Not - {basket?.totalNotional ?? ""} + {formatNotional(basket?.totalNotional)} ); @@ -135,7 +142,7 @@ export const BasketToolbar = ({ Total Not - {basket?.totalNotionalUsd ?? ""} + {formatNotional(basket?.totalNotionalUsd)} ); diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx index ab6f57007..5076a3ae4 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx @@ -12,7 +12,7 @@ import { import { Button, FormField, FormFieldLabel } from "@salt-ds/core"; import cx from "classnames"; import { DataSourceRow } from "@finos/vuu-data-types"; -import { HTMLAttributes, useMemo } from "react"; +import { HTMLAttributes, RefCallback, useCallback, useMemo } from "react"; import "./NewBasketPanel.css"; import { useNewBasketPanel } from "./useNewBasketPanel"; @@ -45,6 +45,7 @@ export const NewBasketPanel = ({ onSave, onSelectBasket, saveButtonDisabled, + saveButtonRef, } = useNewBasketPanel({ basketDataSource, basketSchema, @@ -70,6 +71,12 @@ export const NewBasketPanel = ({ const itemToString = displayName(columnMap.name); + const inputCallbackRef = useCallback>((el) => { + setTimeout(() => { + el?.querySelector("input")?.focus(); + }, 100); + }, []); + return ( @@ -78,7 +85,11 @@ export const NewBasketPanel = ({
Basket Name - + Basket Definition @@ -98,9 +109,10 @@ export const NewBasketPanel = ({ Cancel diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts index a2092bf17..d16e2b8ad 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts @@ -6,7 +6,7 @@ import { import { TableRowSelectHandler } from "@finos/vuu-table"; import { Commithandler, OpenChangeHandler } from "@finos/vuu-ui-controls"; import { buildColumnMap, metadataKeys } from "@finos/vuu-utils"; -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { NewBasketPanelProps } from "./NewBasketPanel"; const { KEY } = metadataKeys; @@ -43,6 +43,7 @@ export const useNewBasketPanel = ({ const columnMap = buildColumnMap(basketSchema.columns); const [basketName, setBasketName] = useState(""); const [basketId, setBasketId] = useState(); + const saveButtonRef = useRef(null); const saveBasket = useCallback(() => { if (basketName && basketId) { @@ -76,6 +77,9 @@ export const useNewBasketPanel = ({ const basketId = row[KEY] as string; console.log({ basketId, columnMap }); setBasketId(basketId); + setTimeout(() => { + saveButtonRef.current?.focus(); + }, 60); }, [columnMap] ); @@ -105,5 +109,6 @@ export const useNewBasketPanel = ({ onSave: saveBasket, onSelectBasket: handleSelectBasket, saveButtonDisabled: basketName === "" || basketId === undefined, + saveButtonRef, }; }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts index 2a02733e1..b370a1d1b 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts @@ -43,10 +43,11 @@ export const useBasketContextMenus = ({ content: { type: "InstrumentSearch", props: { + TableProps: { + allowDragDrop: "drag-copy", + id: "basket-instruments", + }, dataSource: dataSourceInstruments, - // columnName: action.column.name, - // onConfigChange, - // tableConfig, }, }, title: "Add Ticker", @@ -56,5 +57,5 @@ export const useBasketContextMenus = ({ return false; }, ]; - }, []); + }, [dataSourceInstruments, dispatchLayoutAction]); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx index ad2412923..b6f8f2035 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx @@ -10,6 +10,7 @@ import { NewBasketPanel } from "./new-basket-panel"; import { useBasketContextMenus } from "./useBasketContextMenus"; import { useBasketTradingDataSources } from "./useBasketTradingDatasources"; import { BasketTradingFeatureProps } from "./VuuBasketTradingFeature"; +import { VuuDataRow, VuuDataRowDto } from "packages/vuu-protocol-types"; export class Basket { basketId: string; @@ -28,7 +29,7 @@ export class Basket { this.basketName = data[columnMap.basketName] as string; this.filledPct = data[columnMap.filledPct] as number; this.fxRateToUsd = data[columnMap.fxRateToUsd] as number; - this.side = "BUY"; + this.side = data[columnMap.side] as string; this.totalNotional = data[columnMap.totalNotional] as number; this.totalNotionalUsd = data[columnMap.totalNotionalUsd] as number; this.units = data[columnMap.units] as number; @@ -43,6 +44,13 @@ export type BasketTradingHookProps = Pick< | "instrumentsSchema" >; +const toDataDto = (dataSourceRow: VuuDataRow, columnMap: ColumnMap) => { + Object.entries(columnMap).reduce((dto, [colName, index]) => { + dto[colName] = dataSourceRow[index]; + return dto; + }, {}); +}; + type BasketState = { basketInstanceId?: string; dialog?: JSX.Element; @@ -89,10 +97,14 @@ export const useBasketTrading = ({ dialog: undefined, }); - const columnMap = useMemo( + const columnMapBasketTrading = useMemo( () => buildColumnMap(dataSourceBasketTradingControl.columns), [dataSourceBasketTradingControl.columns] ); + const columnMapInstrument = useMemo( + () => buildColumnMap(dataSourceInstruments.columns), + [dataSourceInstruments.columns] + ); useMemo(() => { dataSourceBasketTradingControl.subscribe( @@ -105,7 +117,10 @@ export const useBasketTrading = ({ setBasketCount(message.size); } if (message.rows && message.rows.length > 0) { - setBasket(new Basket(message.rows[0], columnMap)); + const basket = new Basket(message.rows[0], columnMapBasketTrading); + console.log({ basket, row: message.rows[0] }); + + setBasket(new Basket(message.rows[0], columnMapBasketTrading)); } } } @@ -115,7 +130,7 @@ export const useBasketTrading = ({ setTimeout(() => { setBasketCount((count) => (count === -1 ? 0 : count)); }, 800); - }, [columnMap, dataSourceBasketTradingControl]); + }, [columnMapBasketTrading, dataSourceBasketTradingControl]); useEffect(() => { return () => { @@ -188,6 +203,7 @@ export const useBasketTrading = ({ const handleCommitBasketChange = useCallback( (columnName, value) => { if (basket) { + console.log(`handleCommitBasketChange ${columnName} => ${value}`); const { dataSourceRow } = basket; return dataSourceBasketTradingControl.applyEdit( dataSourceRow, @@ -226,6 +242,38 @@ export const useBasketTrading = ({ menuBuilder: buildViewserverMenuOptions, }; + const handleDropInstrument = useCallback( + (dragDropState) => { + console.log(`useBasketTrading handleDropInstrument`, { + instrument: dragDropState.payload, + }); + const key = "steve-00001.AAA.L"; + const data = { + algo: -1, + algoParams: "", + basketId: ".FTSE100", + description: "Test", + instanceId: "steve-00001", + instanceIdRic: "steve-00001.AAA.L", + limitPrice: 0, + notionalLocal: 0, + notionalUsd: 0, + pctFilled: 0, + priceSpread: 0, + priceStrategyId: 2, + quantity: 0, + ric: "AAL.L", + side: "BUY", + venue: "", + weighting: 1, + }; + dataSourceBasketTradingControl.insertRow?.(key, data).then((response) => { + console.log({ response }); + }); + }, + [dataSourceBasketTradingControl] + ); + return { ...basketState, activeTabIndex, @@ -237,6 +285,7 @@ export const useBasketTrading = ({ dataSourceBasketTradingConstituentJoin, onClickAddBasket: handleAddBasket, onCommitBasketChange: handleCommitBasketChange, + onDropInstrument: handleDropInstrument, onSendToMarket, onTakeOffMarket, }; diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index 8a317b3ac..29f89b913 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -15,9 +15,10 @@ import { ColumnSettingsPanel, TableSettingsPanel, } from "@finos/vuu-table-extras"; -import { CSSProperties } from "react"; +import { CSSProperties, useMemo } from "react"; import { FilterTableFeatureProps } from "feature-vuu-filter-table"; import { getAllSchemas } from "@finos/vuu-data-test"; +import { DragDropProvider } from "@finos/vuu-ui-controls"; import "./NewTheme.examples.css"; @@ -97,34 +98,43 @@ const ShellWithNewTheme = () => { const { buildMenuOptions, handleMenuAction } = useLayoutContextMenuItems(setDialogState); + const dragSource = useMemo( + () => ({ + "basket-instruments": { dropTargets: "basket-constituents" }, + }), + [] + ); + return ( - - } - loginUrl={window.location.toString()} - user={user} - style={ - { - "--vuuShell-height": "100vh", - "--vuuShell-width": "100vw", - } as CSSProperties - } - > - {dialog} - + + + } + loginUrl={window.location.toString()} + user={user} + style={ + { + "--vuuShell-height": "100vh", + "--vuuShell-width": "100vw", + } as CSSProperties + } + > + {dialog} + + ); }; diff --git a/vuu-ui/showcase/src/examples/Shell/AppHeader.examples.tsx b/vuu-ui/showcase/src/examples/Shell/AppHeader.examples.tsx index d4b0b82bb..fe5840d81 100644 --- a/vuu-ui/showcase/src/examples/Shell/AppHeader.examples.tsx +++ b/vuu-ui/showcase/src/examples/Shell/AppHeader.examples.tsx @@ -3,6 +3,12 @@ import { AppHeader } from "@finos/vuu-shell"; let displaySequence = 1; export const DefaultAppHeader = () => { - return ; + return ( + console.log("onNavigate")} + /> + ); }; DefaultAppHeader.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Shell/LoginPanel.examples.tsx b/vuu-ui/showcase/src/examples/Shell/LoginPanel.examples.tsx index 29dda3843..915720f71 100644 --- a/vuu-ui/showcase/src/examples/Shell/LoginPanel.examples.tsx +++ b/vuu-ui/showcase/src/examples/Shell/LoginPanel.examples.tsx @@ -1,5 +1,5 @@ import { LoginPanel } from "@finos/vuu-shell"; export const DefaultLoginPanel = () => { - return ; + return console.log("onSubmit")} />; }; diff --git a/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx index f0fc3130b..1a8617138 100644 --- a/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/TableNext.examples.tsx @@ -3,10 +3,11 @@ import { FlexboxLayout, LayoutProvider, registerComponent, + Toolbar, View, } from "@finos/vuu-layout"; import { ContextPanel } from "@finos/vuu-shell"; -import { TableNext } from "@finos/vuu-table"; +import { TableNext, TableProps } from "@finos/vuu-table"; import { ColumnSettingsPanel, TableSettingsPanel, @@ -15,42 +16,104 @@ import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { CSSProperties, useCallback, useMemo, useState } from "react"; import { useTableConfig, useTestDataSource } from "../utils"; import { GroupHeaderCellNext } from "@finos/vuu-table"; -import { getAllSchemas } from "@finos/vuu-data-test"; +import { + getAllSchemas, + getSchema, + SimulTableName, + vuuModule, +} from "@finos/vuu-data-test"; import "./TableNext.examples.css"; +import { Button } from "@salt-ds/core"; let displaySequence = 1; export const NavigationStyle = () => { - const { - typeaheadHook: _, - config: configProp, - ...props - } = useTableConfig({ - rangeChangeRowset: "full", - table: { module: "SIMUL", table: "instruments" }, - }); - - const [config, setConfig] = useState(configProp); + const tableProps = useMemo>(() => { + const tableName: SimulTableName = "instruments"; + return { + config: { + columns: getSchema(tableName).columns, + rowSeparators: true, + zebraStripes: true, + }, + dataSource: + vuuModule("SIMUL").createDataSource(tableName), + }; + }, []); - const handleConfigChange = useCallback((config: TableConfig) => { - setConfig(config); + const onSelect = useCallback((row) => { + console.log({ row }); + }, []); + const onSelectionChange = useCallback((selected) => { + console.log({ selected }); }, []); return ( ); }; NavigationStyle.displaySequence = displaySequence++; +export const ControlledNavigation = () => { + const tableProps = useMemo>(() => { + const tableName: SimulTableName = "instruments"; + return { + config: { + columns: getSchema(tableName).columns, + rowSeparators: true, + zebraStripes: true, + }, + dataSource: + vuuModule("SIMUL").createDataSource(tableName), + }; + }, []); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + + const handlePrevClick = useCallback(() => { + setHighlightedIndex((idx) => Math.max(0, idx - 1)); + }, []); + + const handleNextClick = useCallback(() => { + setHighlightedIndex((idx) => idx + 1); + }, []); + + const handleHighlight = useCallback((idx: number) => { + setHighlightedIndex(idx); + }, []); + + return ( + <> + + + + + + + ); +}; +ControlledNavigation.displaySequence = displaySequence++; + export const EditableTableNextArrayData = () => { const { config, dataSource } = useTableConfig({ columnConfig: { @@ -209,19 +272,10 @@ export const TableNextInLayoutWithContextPanel = () => { table: { module: "SIMUL", table: "instruments" }, }); - const handleConfigChange = useCallback((tableConfig: TableConfig) => { - console.log("config changed"); - }, []); - return ( - + @@ -239,11 +293,7 @@ export const AutoTableNext = () => { table: { module: "SIMUL", table: "instruments" }, }); - const [config, setConfig] = useState(configProp); - - const handleConfigChange = (config: TableConfig) => { - setConfig(config); - }; + const [config] = useState(configProp); return ( { config={{ ...config, }} - onConfigChange={handleConfigChange} renderBufferSize={0} /> ); diff --git a/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx index 455242833..90acc57d0 100644 --- a/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx @@ -18,7 +18,7 @@ export const DraggableListsOneWayDrag = () => { ); const dragSource = useMemo( () => ({ - list1: { dropTargets: "list2" }, + list1: { dropTargets: ["list1", "list2"] }, }), [] ); @@ -98,6 +98,7 @@ export const DraggableListsOneWayDrag = () => { { onDragStart={handleDragStart1} onMoveListItem={handleMoveListItem1} source={state1} - allowDragDrop + width={200} />
{ onDrop={handleDrop2} onMoveListItem={handleMoveListItem2} source={state2} - allowDragDrop + width={200} /> diff --git a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx index 0eed499d6..d56eb2e5a 100644 --- a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx @@ -1,48 +1,57 @@ import { InstrumentPicker } from "@finos/vuu-ui-controls"; import { - createArrayDataSource, getAllSchemas, getSchema, + SimulTableName, + vuuModule, } from "@finos/vuu-data-test"; import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; import { useCallback, useMemo } from "react"; import { TableProps, TableRowSelectHandler } from "@finos/vuu-table"; import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; import { useTestDataSource } from "../utils"; +import { DataSourceRow } from "packages/vuu-data-types"; let displaySequence = 0; export const DefaultInstrumentPicker = () => { - const schema = getSchema("instruments"); - const [columnMap, searchColumns, tableProps] = useMemo< - [ColumnMap, string[], Pick] - >( - () => [ - buildColumnMap(schema.columns), - ["bbg", "description"], + const tableName: SimulTableName = "instruments"; + const schema = getSchema(tableName); + + const [tableProps, columnMap, searchColumns] = useMemo< + [Pick, ColumnMap, string[]] + >(() => { + return [ { config: { - // TODO need to inject this value - showHighlightedRow: true, - columns: [ - { name: "bbg", serverDataType: "string" }, - { name: "description", serverDataType: "string", width: 280 }, - ] as ColumnDescriptor[], + columns: schema.columns, + rowSeparators: true, + zebraStripes: true, }, - dataSource: createArrayDataSource({ table: schema.table }), + dataSource: + vuuModule("SIMUL").createDataSource(tableName), }, - ], - [schema] + buildColumnMap(schema.columns), + ["bbg", "description"], + ]; + }, [schema.columns]); + + const itemToString = useCallback( + (row: DataSourceRow) => { + return [row[columnMap.description]]; + }, + [columnMap.description] ); - const handleSelect = useCallback((row) => { - console.log(`row selected ${row.join(",")}`); + const handleSelect = useCallback((index) => { + console.log(`row selected ${index}`); }, []); return ( { - const { dataSource } = useTableConfig({ - dataSourceConfig: { - columns: ["bbg", "description"], - }, - table: { module: "SIMUL", table: "instruments" }, - }); + const dataSource = useMemo( + () => vuuModule("SIMUL").createDataSource("instruments"), + [] + ); return ( @@ -22,10 +35,8 @@ export const DefaultInstrumentSearch = () => { DefaultInstrumentSearch.displaySequence = displaySequence++; export const InstrumentSearchVuuInstruments = () => { - const schemas = getAllSchemas(); const { dataSource, error } = useTestDataSource({ - // bufferSize: 1000, - schemas, + schemas: getAllSchemas(), }); if (error) { @@ -42,3 +53,74 @@ export const InstrumentSearchVuuInstruments = () => { }; InstrumentSearchVuuInstruments.displaySequence = displaySequence++; + +type DropTargetProps = HTMLAttributes; +const DropTarget = ({ id, ...htmlAttributes }: DropTargetProps) => { + const [instrument, setInstrument] = useState(); + const { isDragSource, isDropTarget, register } = useDragDropProvider(id); + + console.log( + `DropTarget isDragSource ${isDragSource} isDropTarget ${isDropTarget}` + ); + + const acceptDrop = useCallback((dragState) => { + console.log({ payload: dragState.payload }); + setInstrument(dragState.payload as DataSourceRow); + }, []); + + useEffect(() => { + if (id && (isDragSource || isDropTarget)) { + register(id, false, acceptDrop); + } + }, [acceptDrop, id, isDragSource, isDropTarget, register]); + + return ( +
+ {instrument ? ( + <> + {instrument[8]} + - + {instrument[10]} + + ) : null} +
+ ); +}; + +export const InstrumentSearchDragDrop = () => { + const dataSource = useMemo( + () => vuuModule("SIMUL").createDataSource("instruments"), + [] + ); + + const dragSource = useMemo( + () => ({ + "source-table": { dropTargets: "drop-target" }, + }), + [] + ); + + const handleDragStart = useCallback(() => { + console.log("DragStart"); + }, []); + + return ( + + + + + + + ); +}; + +InstrumentSearchDragDrop.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx b/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx index c83bdbe80..7d40a7907 100644 --- a/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx +++ b/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx @@ -1,21 +1,91 @@ import { BasketSelector } from "feature-basket-trading"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo } from "react"; import { vuuModule } from "@finos/vuu-data-test"; import { Basket } from "feature-basket-trading"; +import { ArrayDataSource } from "@finos/vuu-data"; +import { createBasketTradingRow } from "@finos/vuu-data-test"; let displaySequence = 1; +const testBaskets = [ + ["Amber-0001", "Amber Basket", "OFF MARKET", "BUY"], + ["Blue-0002", "Blue Basket", "ON MARKET", "SELL"], + ["Charcoal-0003", "Charcoal Basket", "OFF MARKET", "BUY"], + ["Dandruff-0004", "Dandruff Basket", "ON MARKET", "BUY"], + ["Elephant-0005", "Elephant Basket", "OFF MARKET", "SELL"], + ["Frogger-0006", "Frogger Basket", "OFF MARKET", "BUY"], + ["Gray-0007", "Gray Basket", "ON MARKET", "SELL"], + ["Helium-0008", "Helium Basket", "OFF MARKET", "BUY"], + ["Indigo-0009", "Indigo Basket", "OFF MARKET", "BUY"], +]; + export const DefaultBasketSelector = () => { - const [basket] = useState({ - basketId: "basket-001", + const testBasket: Basket = { + dataSourceRow: [] as any, + basketId: ".FTSE", + basketName: "Test Basket", + filledPct: 0, + fxRateToUsd: 1.25, + side: "BUY", + totalNotional: 1000, + totalNotionalUsd: 1000, + units: 120, + }; + + const dataSource = useMemo(() => { + const dataSource = vuuModule("BASKET").createDataSource( + "basketTrading" + ) as ArrayDataSource; + for (const [basketId, basketName, side, status] of testBaskets) { + dataSource["insert"]( + createBasketTradingRow(basketId, basketName, status, side) + ); + } + return dataSource; + }, []); + + const handleClickAddBasket = useCallback(() => { + console.log("Add Basket"); + }, []); + + const handleSelectBasket = useCallback(() => {}, []); + + return ( + + ); +}; +DefaultBasketSelector.displaySequence = displaySequence++; + +export const OpenBasketSelector = () => { + const testBasket: Basket = { + dataSourceRow: [] as any, + basketId: ".FTSE", basketName: "Test Basket", - filledPct: 0.7, - fxRateToUsd: 1.234, - totalNotional: 1_000_123, - totalNotionalUsd: 1_234_000, - units: 100, - }); - const dataSource = vuuModule("BASKET").createDataSource("basketTrading"); + filledPct: 0, + fxRateToUsd: 1.25, + side: "BUY", + totalNotional: 1000, + totalNotionalUsd: 1000, + units: 120, + }; + + const dataSource = useMemo(() => { + const dataSource = vuuModule("BASKET").createDataSource( + "basketTrading" + ) as ArrayDataSource; + for (const [basketId, basketName, side, status] of testBaskets) { + dataSource["insert"]( + createBasketTradingRow(basketId, basketName, status, side) + ); + } + return dataSource; + }, []); const handleClickAddBasket = useCallback(() => { console.log("Add Basket"); @@ -27,12 +97,13 @@ export const DefaultBasketSelector = () => { return ( ); }; -DefaultBasketSelector.displaySequence = displaySequence++; +OpenBasketSelector.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/html/html-table-components/Row.tsx b/vuu-ui/showcase/src/examples/html/html-table-components/Row.tsx index 135547afb..437b770b9 100644 --- a/vuu-ui/showcase/src/examples/html/html-table-components/Row.tsx +++ b/vuu-ui/showcase/src/examples/html/html-table-components/Row.tsx @@ -1,6 +1,8 @@ import { DataSourceRow } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { RowClickHandler } from "@finos/vuu-table"; +import { + KeyedColumnDescriptor, + RowClickHandler, +} from "@finos/vuu-datagrid-types"; import { ColumnMap, isGroupColumn, metadataKeys } from "@finos/vuu-utils"; import { CSSProperties, memo, MouseEvent, useCallback } from "react"; import { TableCell } from "./TableCell"; diff --git a/vuu-ui/showcase/src/examples/html/html-table-components/vuu-table/Row.tsx b/vuu-ui/showcase/src/examples/html/html-table-components/vuu-table/Row.tsx index 1c35b701b..f2a9c6b09 100644 --- a/vuu-ui/showcase/src/examples/html/html-table-components/vuu-table/Row.tsx +++ b/vuu-ui/showcase/src/examples/html/html-table-components/vuu-table/Row.tsx @@ -1,6 +1,8 @@ import { DataSourceRow } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { RowClickHandler } from "@finos/vuu-table"; +import { + KeyedColumnDescriptor, + RowClickHandler, +} from "@finos/vuu-datagrid-types"; import { ColumnMap, isGroupColumn,