diff --git a/vuu-ui/package-lock.json b/vuu-ui/package-lock.json index c9c31a0e82..a5edb95dea 100644 --- a/vuu-ui/package-lock.json +++ b/vuu-ui/package-lock.json @@ -8777,9 +8777,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -18810,9 +18810,9 @@ } }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts index add3835c93..42cb853f8f 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts @@ -30,32 +30,20 @@ export const getTypeaheadParams = ( // const containSpace = (text: string) => text.indexOf(" ") !== -1; // const replaceSpace = (text: string) => text.replace(/\s/g, SPECIAL_SPACE); -export const useTypeaheadSuggestions = () => { - const getTypeaheadSuggestions: SuggestionFetcher = useCallback( - async (params: TypeaheadParams) => { - const rpcMessage = - params.length === 2 - ? ({ - method: "getUniqueFieldValues", - params, - ...TYPEAHEAD_MESSAGE_CONSTANTS, - } as ClientToServerGetUniqueValues) - : ({ - method: "getUniqueFieldValuesStartingWith", - params, - ...TYPEAHEAD_MESSAGE_CONSTANTS, - } as ClientToServerGetUniqueValuesStartingWith); - - const suggestions = await makeRpcCall(rpcMessage); - - // TODO replacing space with underscores like this is not being correctly handled elsewhere - return suggestions; - // return suggestions.some(containSpace) - // ? suggestions.map(replaceSpace) - // : suggestions; - }, - [] - ); - - return getTypeaheadSuggestions; -}; +export const useTypeaheadSuggestions = () => + useCallback(async (params: TypeaheadParams) => { + const rpcMessage = + params.length === 2 + ? ({ + method: "getUniqueFieldValues", + params, + ...TYPEAHEAD_MESSAGE_CONSTANTS, + } as ClientToServerGetUniqueValues) + : ({ + method: "getUniqueFieldValuesStartingWith", + params, + ...TYPEAHEAD_MESSAGE_CONSTANTS, + } as ClientToServerGetUniqueValuesStartingWith); + + return makeRpcCall(rpcMessage); + }, []); diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts index 3d1afb08d9..3780574a6e 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts @@ -213,7 +213,7 @@ export type VuuServerMenuOptions = { columnMap: ColumnMap; columnName: string; row: DataSourceRow; - selectedRows: DataSourceRow[]; + selectedRowsCount: number; viewport: string; }; @@ -373,6 +373,7 @@ export const useVuuMenuActions = ({ `useViewServer handleMenuAction, can't handle action type ${menuId}` ); } + return false; }, [clientSideMenuActionHandler, dataSource, onRpcResponse] diff --git a/vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts similarity index 98% rename from vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts rename to vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts index cacdc6eb12..f76e03023e 100644 --- a/vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts +++ b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts @@ -1,6 +1,6 @@ import { ArrayDataSource } from "@finos/vuu-data"; import { VuuRange } from "@finos/vuu-protocol-types"; -import { random } from "./reference-data"; +import { random } from "./simul/reference-data"; import { RowUpdates, UpdateGenerator, UpdateHandler } from "./rowUpdates"; const getNewValue = (value: number) => { diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts new file mode 100644 index 0000000000..23a7c07d2b --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts @@ -0,0 +1,95 @@ +import { TableSchema } from "@finos/vuu-data"; + +export type BasketsTableName = + | "algoType" + | "basket" + | "basketConstituent" + | "basketTrading" + | "basketTradingConstituent" + | "priceStrategyType"; + +export const schemas: Readonly< + Record> +> = { + algoType: { + columns: [ + { name: "algoType", serverDataType: "string" }, + { name: "id", serverDataType: "int" }, + ], + key: "id", + table: { module: "BASKET", table: "algoType" }, + }, + basket: { + columns: [ + { name: "id", serverDataType: "string" }, + { name: "name", serverDataType: "string" }, + { name: "notionalValue", serverDataType: "double" }, + { name: "notionalValueUsd", serverDataType: "double" }, + ], + key: "id", + table: { module: "BASKET", table: "basket" }, + }, + basketConstituent: { + columns: [ + { name: "basketId", serverDataType: "string" }, + { name: "change", serverDataType: "string" }, + { name: "lastTrade", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "ricBasketId", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "volume", serverDataType: "string" }, + { name: "weighting", serverDataType: "double" }, + ], + key: "ricBasketId", + table: { module: "BASKET", table: "basketConstituent" }, + }, + basketTrading: { + columns: [ + { name: "basketId", serverDataType: "string" }, + { name: "basketName", serverDataType: "string" }, + { name: "filledPct", serverDataType: "double" }, + { name: "fxRateToUsd", serverDataType: "double" }, + { name: "instanceId", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "totalNotional", serverDataType: "double" }, + { name: "totalNotionalUsd", serverDataType: "double" }, + { name: "units", serverDataType: "int" }, + ], + key: "instanceId", + table: { module: "BASKET", table: "basketTrading" }, + }, + basketTradingConstituent: { + columns: [ + { name: "algo", serverDataType: "string" }, + { name: "algoParams", serverDataType: "string" }, + { name: "basketId", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "description", serverDataType: "string" }, + { name: "instanceId", serverDataType: "string" }, + { name: "instanceIdRic", serverDataType: "string" }, + { name: "last", serverDataType: "double" }, + { name: "limitPrice", serverDataType: "double" }, + { name: "notionalLocal", serverDataType: "double" }, + { name: "notionalUsd", serverDataType: "double" }, + { name: "offer", serverDataType: "double" }, + { name: "pctFilled", serverDataType: "double" }, + { name: "priceSpread", serverDataType: "int" }, + { name: "priceStrategyId", serverDataType: "int" }, + { name: "quantity", serverDataType: "long" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "venue", serverDataType: "string" }, + { name: "weighting", serverDataType: "double" }, + ], + key: "instanceIdRic", + table: { module: "BASKET", table: "basketTradingConstituent" }, + }, + priceStrategyType: { + columns: [ + { name: "priceStrategy", serverDataType: "string" }, + { name: "id", serverDataType: "int" }, + ], + key: "", + table: { module: "BASKET", table: "priceStrategyType" }, + }, +}; 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 new file mode 100644 index 0000000000..7de4feae64 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts @@ -0,0 +1,10 @@ +import { BasketColumnMap, BasketReferenceData } from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basket", + BasketColumnMap, + BasketReferenceData +); + +export { RowGenerator, ColumnGenerator }; 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 new file mode 100644 index 0000000000..9db313bcfa --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts @@ -0,0 +1,13 @@ +import { + BasketConstituentColumnMap, + BasketConstituentReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketConstituent", + BasketConstituentColumnMap, + BasketConstituentReferenceData +); + +export { RowGenerator, ColumnGenerator }; 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 new file mode 100644 index 0000000000..2fe2e2ed83 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts @@ -0,0 +1,13 @@ +import { + BasketTradingColumnMap, + BasketTradingReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketTrading", + BasketTradingColumnMap, + BasketTradingReferenceData +); + +export { RowGenerator, ColumnGenerator }; 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 new file mode 100644 index 0000000000..90fd547c24 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts @@ -0,0 +1,13 @@ +import { + BasketTradingConstituentColumnMap, + BasketTradingConstituentReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketTradingConstituent", + BasketTradingConstituentColumnMap, + BasketTradingConstituentReferenceData +); + +export { RowGenerator, ColumnGenerator }; 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 new file mode 100644 index 0000000000..7c1a90d421 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts @@ -0,0 +1,4 @@ +export * as basket from "./basket-generator"; +export * as basketConstituent from "./basketConstituent-generator"; +export * as basketTrading from "./basketTrading-generator"; +export * as basketTradingConstituent from "./basketTradingConstituent-generator"; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts new file mode 100644 index 0000000000..5dbf416cc0 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts @@ -0,0 +1,22 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { getSchema } from "../../schemas"; + +const schema = getSchema("basket"); + +export const BasketColumnMap = Object.values(schema.columns).reduce( + (map, col, index) => { + map[col.name] = index; + return map; + }, + {} +); + +const data: VuuDataRow[] = [ + [".NASDAQ100", ".NASDAQ100", 0, 0], + [".HSI", ".HSI", 0, 0], + [".FTSE100", ".FTSE100", 0, 0], + [".SP500", ".SP500", 0, 0], +]; + +export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts new file mode 100644 index 0000000000..933435dd6f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts @@ -0,0 +1,43 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { getSchema } from "../../schemas"; +import { ColumnMap } from "@finos/vuu-utils"; +import ftse from "./ftse100"; + +const schema = getSchema("basketConstituent"); + +export const BasketConstituentColumnMap = Object.values( + schema.columns +).reduce((map, col, index) => { + map[col.name] = index; + return map; +}, {}); + +const data: VuuDataRow[] = []; + +// const start = performance.now(); +// Create 100_000 Instruments + +for (const row of ftse) { + // prettier-ignore + const [ric, name, lastTrade, change, volume] = row; + + const basketId = ".FTSE100"; + const side = "BUY"; + const weighting = 1; + + data.push([ + basketId, + change, + lastTrade, + ric, + `${ric}-${basketId}`, + side, + volume, + weighting, + ]); +} + +// const end = performance.now(); +// console.log(`generating 100,000 instrumentPrices took ${end - start} ms`); + +export default data; 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 new file mode 100644 index 0000000000..3d56d8134f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts @@ -0,0 +1,16 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { getSchema } from "../../schemas"; + +const schema = getSchema("basketTrading"); + +export const BasketTradingColumnMap = 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/basketTradingConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts new file mode 100644 index 0000000000..01e3471ba6 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts @@ -0,0 +1,16 @@ +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/ftse100.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts new file mode 100644 index 0000000000..3adb247c54 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts @@ -0,0 +1,102 @@ +// Symbol,Name,Last Trade,Change,Volume +// prettier-ignore +export default [ + ["AAL.L","Anglo American PLC","436.35 13:13","5.35 (1.24%)","5,799,089"], + ["ABF.L","Associated British Foods PLC","3,435.60 13:12","7.40 (0.21%)","86,808"], + ["ADM.L","Admiral Group PLC","1,627.00 13:13",""], + ["ADN.L","Aberdeen Asset Management PLC","334.00 13:13","2.50 (0.75%)","806,880"], + ["AHT.L","Ashtead Group PLC","1,027.00 13:13","6.00 (0.59%)","331,255"], + ["ANTO.L","Antofagasta PLC","484.10 13:13","11.70 (2.48%)","1,753,976"], + ["ARM.L","ARM Holdings PLC","1,058.00 13:13","3.00 (0.28%)","475,927"], + ["AV.L","Aviva PLC","493.97 13:13","2.23 (0.45%)","2,226,835"], + ["AZN.L","AstraZeneca PLC","4,399.50 13:13","2.50 (0.06%)","815,133"], + ["BA.L","BAE Systems PLC","478.10 13:13","4.30 (0.91%)","2,039,934"], + ["BAB.L","Babcock International Group PLC","988.00 13:13","9.50 (0.97%)","209,614"], + ["BARC.L","Barclays PLC","226.30 13:13", "1.15 (0.51%)","6,575,664"], + ["BATS.L","British American Tobacco PLC","3,803.50 13:13", "8.50 (0.22%)","465,110"], + ["BDEV.L","Barratt Developments PLC","576.00 13:13", "0.50 (0.09%)","1,044,365"], + ["BG.L","BG Group PLC","1,013.50 13:13", "5.50 (0.55%)","1,507,332"], + ["BKG.L","Berkeley Group Holdings (The) PLC","3,126.00 13:13", "15.00 (0.48%)","95,071"], + ["BLND.L","British Land Co PLC","828.06 13:12", "10.44 (1.25%)","1,802,548"], + ["BLT.L","BHP Billiton PLC","881.40 13:13", "4.30 (0.49%)","4,947,287"], + ["BNZL.L","Bunzl PLC","1,875.40 13:05", "4.60 (0.24%)","104,541"], + ["BP.L","BP PLC","381.50 13:13", "2.95 (0.78%)","10,493,561"], + ["BRBY.L","Burberry Group PLC","1,269.00 13:13", "7.00 (0.55%)","295,647"], + ["BT-A","L,BT Group PLC","489.20 13:13", "3.70 (0.75%)","3,914,982"], + ["CCL.L","Carnival PLC","3,426.00 13:12", "22.00 (0.64%)","86,257"], + ["CNA.L","Centrica PLC","212.80 13:13", "0.60 (0.28%)","2,144,540"], + ["CPG.L","Compass Group PLC","1,054.00 13:08", "5.00 (0.48%)","1,001,167"], + ["CPI.L","Capita PLC","1,235.00 13:11", "1.00 (0.08%)","244,591"], + ["CRH.L","CRH PLC","1,783.20 13:12", "17.80 (0.99%)","897,325"], + ["DC.L","DIXONS CARPHONE","462.10 13:11",""], + ["DGE.L","Diageo PLC","1,881.50 13:13", "6.50 (0.34%)","756,906"], + ["DLG.L","Direct Line Insurance Group PLC","403.80 13:13", "0.40 (0.10%)","1,095,340"], + ["EXPN.L","Experian PLC","1,191.00 13:12", "2.00 (0.17%)","467,283"], + ["EZJ.L","easyJet PLC","1,682.00 13:12", "28.00 (1.64%)","1,191,230"], + ["FRES.L","Fresnillo PLC","678.50 13:12", "6.50 (0.97%)","381,871"], + ["GFS.L","G4S PLC","232.30 13:03", "2.00 (0.85%)","1,096,551"], + ["GKN.L","GKN PLC","294.80 13:12", "2.50 (0.86%)","792,247"], + ["GLEN.L","Glencore PLC","90.48 13:13", "1.65 (1.86%)","41,631,528"], + ["GSK.L","GlaxoSmithKline PLC","1,345.00 13:13", "0.50 (0.04%)","1,767,356"], + ["HIK.L","Hikma Pharmaceuticals PLC","2,010.00 13:04", "57.00 (2.92%)","261,511"], + ["HL.L","Hargreaves Lansdown PLC","1,488.03 13:12", "9.97 (0.67%)","372,261"], + ["HMSO.L","Hammerson PLC","597.50 13:11", "3.50 (0.58%)","478,301"], + ["HSBA.L","HSBC Holdings PLC","519.70 13:13", "0.50 (0.10%)","7,415,629"], + ["IAG.L","International Consolidated Airlines Group SA","575.40 13:12", "16.10 (2.72%)","4,311,514"], + ["IHG.L","InterContinental Hotels Group PLC","2,481.00 13:12", "19.00 (0.76%)","219,918"], + ["III.L","3i Group PLC","487.30 13:11", "4.50 (0.92%)","189,987"], + ["IMT.L","Imperial Tobacco Group PLC","3,571.00 13:13", "29.00 (0.81%)","926,816"], + ["INTU.L","intu properties PLC","319.90 13:09", "4.60 (1.42%)","514,821"], + ["ISAT.L","Inmarsat PLC","1,054.44 13:13", "3.44 (0.33%)","988,089"], + ["ITRK.L","Intertek Group PLC","2,643.00 13:14", "3.00 (0.11%)","45,868"], + ["ITV.L","ITV PLC","267.30 13:14", "2.60 (0.96%)","3,453,208"], + ["JMAT.L","Johnson Matthey PLC","2,445.00 13:14", "29.00 (1.20%)","276,397"], + ["KGF.L","Kingfisher PLC","346.20 13:14", "4.30 (1.23%)","1,021,408"], + ["LAND.L","Land Securities Group PLC","1,239.00 13:13", "7.00 (0.56%)","384,973"], + ["LGEN.L","Legal & General Group PLC","266.00 13:14", "1.60 (0.60%)","1,998,399"], + ["LLOY.L","Lloyds Banking Group PLC","73.86 13:14", "0.02 (0.03%)","18,907,878"], + ["LSE.L","London Stock Exchange Group PLC","2,544.00 13:11", "6.00 (0.24%)","129,657"], + ["MGGT.L","Meggitt PLC","386.00 13:15", "3.20 (0.84%)","611,044"], + ["MKS.L","Marks & Spencer Group PLC","514.75 13:12", "3.25 (0.63%)","920,128"], + ["MNDI.L","Mondi PLC","1,463.00 13:14", "7.00 (0.48%)","383,546"], + ["MRW.L","Morrison (Wm) Supermarkets PLC","155.20 13:14",""], + ["NG.L","National Grid PLC","926.40 13:14", "1.10 (0.12%)","1,659,592"], + ["NXT.L","Next PLC","7,765.00 13:11", "95.00 (1.21%)","114,062"], + ["OML.L","Old Mutual PLC","198.50 13:14", "0.40 (0.20%)","2,040,849"], + ["PRU.L","Prudential PLC","1,499.50 13:15", "14.00 (0.93%)","580,870"], + ["PSON.L","Pearson PLC","794.00 13:09", "5.00 (0.63%)","1,177,953"], + ["RB.L","Reckitt Benckiser Group PLC","6,293.00 13:14", "34.00 (0.54%)","281,172"], + ["RBS.L","Royal Bank of Scotland Group PLC","313.40 13:14", "2.40 (0.77%)","2,100,058"], + ["RDSA.L","Royal Dutch Shell PLC","1,636.00 13:14", "18.00 (1.11%)","2,467,461"], + ["RDSB.L","Royal Dutch Shell PLC","1,652.00 13:15", "14.50 (0.89%)","1,457,434"], + ["REL.L","Reed Elsevier PLC","1,170.00 13:14","0.00 (0.00%)","908,802"], + ["RIO.L","Rio Tinto PLC","2,235.00 13:15", "21.00 (0.95%)","2,190,722"], + ["RMG.L","Royal Mail PLC","453.50 13:14", "1.20 (0.26%)","995,316"], + ["RR.L","Rolls-Royce Group PLC","546.63 13:14", "8.38 (1.51%)","2,792,915"], + ["RRS.L","Randgold Resources Ltd","3,929.00 13:14","0.00 (0.00%)","135,524"], + ["RSA.L","RSA Insurance Group PLC","437.10 13:14", "0.10 (0.02%)","395,477"], + ["SAB.L","SABMiller PLC","4,011.00 13:15", "1.00 (0.02%)","892,451"], + ["SBRY.L","Sainsbury (J) PLC","255.80 13:14", "7.40 (2.98%)","2,395,670"], + ["SDR.L","Schroders PLC","2,930.00 13:09", "12.00 (0.41%)","44,674"], + ["SGE.L","Sage Group (The) PLC","545.50 13:13", "0.50 (0.09%)","539,717"], + ["SHP.L","Shire PLC","4,685.00 13:14", "22.00 (0.47%)","221,318"], + ["SKY.L","SKY","1,095.00 13:12", "4.00 (0.37%)","925,016"], + ["SL.L","Standard Life PLC","399.90 13:14", "3.20 (0.79%)","861,636"], + ["SMIN.L","Smiths Group PLC","992.50 13:14", "27.50 (2.70%)","640,309"], + ["SN.L","Smith & Nephew PLC","1,110.00 13:14", "9.00 (0.82%)","480,018"], + ["SPD.L","Sports Direct International PLC","694.50 13:11", "1.50 (0.22%)","157,981"], + ["SSE.L","SSE PLC","1,463.00 13:13", "2.00 (0.14%)","562,454"], + ["STAN.L","Standard Chartered PLC","583.00 13:14", "0.60 (0.10%)","2,018,697"], + ["STJ.L","St James's Place PLC","964.00 13:14", "11.00 (1.13%)","418,480"], + ["SVT.L","Severn Trent PLC","2,199.00 13:12", "1.00 (0.05%)","95,342"], + ["TPK.L","Travis Perkins PLC","1,945.00 13:13", "4.00 (0.21%)","92,916"], + ["TSCO.L","Tesco PLC","171.54 13:14", "2.54 (1.50%)","9,831,136"], + ["TUI.L","TUI AG","1,115.00 13:10", "5.00 (0.45%)","458,970"], + ["TW.L","Taylor Wimpey PLC","183.90 13:15", "1.10 (0.59%)","3,180,729"], + ["ULVR.L","Unilever PLC","2,791.00 13:14", "29.00 (1.03%)","824,827"], + ["UU.L","United Utilities Group PLC","959.00 13:10", "2.50 (0.26%)","436,911"], + ["VOD.L","Vodafone Group PLC","224.25 13:15", "1.30 (0.58%)","17,572,036"], + ["WOS.L","Wolseley PLC","3,657.00 13:14", "4.00 (0.11%)","179,536"], + ["WPP.L","WPP PLC","1,502.00 13:15", "12.00 (0.79%)","857,887"], + ["WTB.L","Whitbread PLC","4,484.00 13:16", "60.00 (1.32%)","141,036"] +] 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 new file mode 100644 index 0000000000..4ece82ad21 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts @@ -0,0 +1,13 @@ +export { default as BasketReferenceData, BasketColumnMap } from "./basket"; +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/generatorTemplate.ts b/vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts new file mode 100644 index 0000000000..01c76e1a76 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts @@ -0,0 +1,44 @@ +import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; +import { getSchema, VuuTableName } from "./schemas"; + +export const getGenerators = ( + tableName: VuuTableName, + columnMap: ColumnMap, + data: VuuDataRow[] +): [RowGeneratorFactory, ColumnGeneratorFn] => [ + (columnNames?: string[]) => (index: number) => { + if (index >= data.length) { + return undefined; + } + if (columnNames) { + return columnNames.map((name) => data[index][columnMap[name]]); + } else { + return data[index].slice(0, 7); + } + }, + + ( + columns = [] + //columnConfig: ExtendedColumnConfig = {} + ) => { + const schema = getSchema(tableName); + const result: ColumnDescriptor[] = schema.columns; + if (typeof columns === "number") { + throw Error(`${tableName}Generator must be passed columns (strings)`); + } else if (columns.length === 0) { + return result; + } else { + return columns.map((name) => { + const column = result.find((col) => col.name === name); + if (column) { + return column; + } else { + throw Error(`${tableName}Generator no column ${name}`); + } + }); + } + }, +]; diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts index 8aa66f675f..da4cd37692 100644 --- a/vuu-ui/packages/vuu-data-test/src/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -1 +1,2 @@ -export * from "./tableSchemas"; +export * from "./schemas"; +export * from "./vuu-row-generator"; diff --git a/vuu-ui/showcase/src/examples/utils/rowUpdates.ts b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/rowUpdates.ts rename to vuu-ui/packages/vuu-data-test/src/rowUpdates.ts diff --git a/vuu-ui/packages/vuu-data-test/src/schemas.ts b/vuu-ui/packages/vuu-data-test/src/schemas.ts new file mode 100644 index 0000000000..79adf5250f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/schemas.ts @@ -0,0 +1,29 @@ +import { TableSchema } from "@finos/vuu-data"; +import { + type BasketsTableName, + schemas as basketSchemas, +} from "./basket/basket-schemas"; +import { + type SimulTableName, + schemas as simulSchemas, +} from "./simul/simul-schemas"; + +export type VuuTableName = BasketsTableName | SimulTableName; +export const schemas: Record = { + ...basketSchemas, + ...simulSchemas, +}; + +const allSchemas: Readonly>> = { + ...basketSchemas, + ...simulSchemas, +}; + +export const getAllSchemas = () => schemas; + +export const getSchema = (tableName: VuuTableName) => { + if (allSchemas[tableName]) { + return allSchemas[tableName]; + } + throw Error(`getSchema no schema for table ${tableName}`); +}; diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/child-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts similarity index 86% rename from vuu-ui/showcase/src/examples/utils/data-generators/child-order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts index 2ba1fbc75c..d1af767b5f 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/child-order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts @@ -1,7 +1,9 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "../useTableConfig"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; -import { getSchema } from "@finos/vuu-data-test"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; import { currencies, locations, suffixes } from "./generatedData"; function random(min: number, max: number) { @@ -75,10 +77,7 @@ export const RowGenerator: RowGeneratorFactory = () => (index: number) => { ]; }; -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { const schema = getSchema("childOrders"); const schemaColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/generate-data-utils.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/data-generators/generate-data-utils.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/generatedData.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/data-generators/generatedData.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts similarity index 58% rename from vuu-ui/showcase/src/examples/utils/data-generators/index.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts index 771b51673e..b378012d47 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts @@ -1,7 +1,3 @@ -export * as basket from "./basket-generator"; -export * as basketDefinitions from "./basket-definitions-generator"; -export * as basketDesign from "./basket-design-generator"; -export * as basketOrders from "./basket-orders-generator"; export * as childOrders from "./child-order-generator"; export * as instruments from "./instrument-generator"; export * as instrumentPrices from "./instrument-prices-generator"; diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/instrument-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts similarity index 89% rename from vuu-ui/showcase/src/examples/utils/data-generators/instrument-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts index dd941fdc10..609c7eda5b 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/instrument-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts @@ -1,12 +1,16 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; -import { getSchema } from "@finos/vuu-data-test"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; import { InstrumentReferenceData, InstrumentColumnMap, } from "../reference-data"; import { getCalculatedColumnType, isCalculatedColumn } from "@finos/vuu-utils"; -import { ExtendedColumnConfig } from "../useTableConfig"; + +export type ExtendedColumnConfig = { [key: string]: Partial }; export const RowGenerator: RowGeneratorFactory = (columnNames?: string[]) => (index: number) => { diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/instrument-prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts similarity index 90% rename from vuu-ui/showcase/src/examples/utils/data-generators/instrument-prices-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts index 7dc4a18c01..3059b73ccc 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/instrument-prices-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts @@ -4,9 +4,12 @@ import { InstrumentPricesColumnMap, InstrumentPricesReferenceData, } from "../reference-data"; -import { BaseUpdateGenerator } from "../UpdateGenerator"; -import { getSchema } from "@finos/vuu-data-test"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; +import { BaseUpdateGenerator } from "../../UpdateGenerator"; +import { getSchema } from "../../index"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; const instrumentPriceSchema = getSchema("instrumentPrices"); diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts similarity index 92% rename from vuu-ui/showcase/src/examples/utils/data-generators/order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts index 2342c6cb70..df7389829e 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts @@ -1,7 +1,11 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "../useTableConfig"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; -import { getSchema } from "@finos/vuu-data-test"; +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); diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/parent-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts similarity index 84% rename from vuu-ui/showcase/src/examples/utils/data-generators/parent-order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts index 72b7a6afa2..78c773c1c7 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/parent-order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts @@ -1,7 +1,9 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "../useTableConfig"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; -import { getSchema } from "@finos/vuu-data-test"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; import { currencies, locations, suffixes } from "./generatedData"; function random(min: number, max: number) { @@ -67,11 +69,7 @@ export const RowGenerator: RowGeneratorFactory = () => (index: number) => { ]; }; -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { const schema = getSchema("parentOrders"); const schemaColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts similarity index 72% rename from vuu-ui/showcase/src/examples/utils/data-generators/prices-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts index 9d111016b5..223cbf01e4 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/prices-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts @@ -1,10 +1,12 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; import { buildColumnMap } from "@finos/vuu-utils"; import { PriceReferenceData } from "../reference-data"; -import { ExtendedColumnConfig } from "../useTableConfig"; -import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; -import { BaseUpdateGenerator } from "../UpdateGenerator"; -import { getAllSchemas } from "@finos/vuu-data-test"; +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) { @@ -21,11 +23,7 @@ const tickingColumns = [bid, bidSize, ask, askSize]; export const createUpdateGenerator = () => new BaseUpdateGenerator(tickingColumns); -export const ColumnGenerator: ColumnGeneratorFn = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { const schemaColumns: ColumnDescriptor[] = pricesSchema.columns; if (typeof columns === "number") { throw Error("PricesColumnGenerator must be passed columns (strings)"); diff --git a/vuu-ui/packages/vuu-data-test/src/simul/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/currencies.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/currencies.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/currencies.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/currencies.ts diff --git a/vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts new file mode 100644 index 0000000000..35f607a34f --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts @@ -0,0 +1,12 @@ +export * from "./currencies"; +export { + default as InstrumentReferenceData, + InstrumentColumnMap, +} from "./instruments"; +export { + default as InstrumentPricesReferenceData, + InstrumentPricesColumnMap, +} from "./instrument-prices"; +export { default as PriceReferenceData } from "./prices"; +export * from "./locations"; +export * from "./utils"; diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/instrument-prices.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instrument-prices.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/instrument-prices.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/instrument-prices.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/instruments.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/instruments.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/locations.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/locations.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/locations.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/locations.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/lotsizes.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/lotsizes.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/lotsizes.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/lotsizes.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/priceStrategies.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/priceStrategies.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/priceStrategies.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/priceStrategies.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/prices.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/prices.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/utils.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/utils.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/utils.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/utils.ts diff --git a/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts b/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts new file mode 100644 index 0000000000..b15bab971b --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts @@ -0,0 +1,137 @@ +import type { TableSchema } from "@finos/vuu-data"; +import type { ColumnDescriptor } from "@finos/vuu-datagrid-types"; + +export type SimulTableName = + | "instruments" + | "instrumentPrices" + | "orders" + | "childOrders" + | "parentOrders" + | "prices"; + +// These Schemas take the form of the schemas that we create +// with TABLE_META returned by Vuu. +export const schemas: Readonly>> = + { + instruments: { + columns: [ + { name: "bbg", serverDataType: "string" }, + { name: "currency", serverDataType: "string" }, + { name: "description", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "isin", serverDataType: "string" }, + { name: "lotSize", serverDataType: "int" }, + { name: "ric", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "instruments" }, + }, + instrumentPrices: { + columns: [ + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, // type: "int" + { name: "bbg", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", serverDataType: "double" }, + { name: "currency", serverDataType: "string" }, + { name: "description", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "isin", serverDataType: "string" }, + { name: "last", serverDataType: "double" }, + { name: "lotSize", serverDataType: "int" }, + { name: "open", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "scenario", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "instrumentPrices" }, + }, + orders: { + columns: [ + { name: "ccy", serverDataType: "string" }, + { name: "created", serverDataType: "long" }, + { name: "filledQuantity", serverDataType: "double" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "orderId", serverDataType: "string" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "trader", serverDataType: "string" }, + ], + key: "orderId", + table: { module: "SIMUL", table: "orders" }, + }, + childOrders: { + columns: [ + { name: "account", serverDataType: "string" }, + { name: "averagePrice", serverDataType: "double" }, + { name: "ccy", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "filledQty", serverDataType: "double" }, + { name: "id", serverDataType: "string" }, + { name: "idAsInt", serverDataType: "int" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "openQty", serverDataType: "double" }, + { name: "parentOrderId", serverDataType: "string" }, + { name: "price", serverDataType: "double" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "strategy", serverDataType: "string" }, + { name: "volLimit", serverDataType: "int" }, + ], + key: "id", + table: { module: "SIMUL", table: "childOrders" }, + }, + parentOrders: { + columns: [ + { name: "account", serverDataType: "string" }, + { name: "algo", serverDataType: "string" }, + { name: "averagePrice", serverDataType: "double" }, + { name: "ccy", serverDataType: "string" }, + { name: "childCount", serverDataType: "int" }, + { name: "exchange", serverDataType: "string" }, + { name: "filledQty", serverDataType: "double" }, + { name: "id", serverDataType: "string" }, + { name: "idAsInt", serverDataType: "int" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "openQty", serverDataType: "double" }, + { name: "price", serverDataType: "double" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "volLimit", serverDataType: "int" }, + ], + key: "id", + table: { module: "SIMUL", table: "parentOrders" }, + }, + prices: { + columns: [ + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, // type: "int" + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", serverDataType: "double" }, + { name: "last", serverDataType: "double" }, + { name: "open", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "scenario", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "prices" }, + }, + }; + +export type ColumnState = { [key: string]: TableSchema }; + +export interface ColumnActionUpdate { + type: "updateColumn"; + column: ColumnDescriptor; +} + +export type ColumnAction = ColumnActionUpdate; diff --git a/vuu-ui/packages/vuu-data-test/src/tableSchemas.ts b/vuu-ui/packages/vuu-data-test/src/tableSchemas.ts deleted file mode 100644 index 1d35440f48..0000000000 --- a/vuu-ui/packages/vuu-data-test/src/tableSchemas.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { TableSchema } from "@finos/vuu-data"; -import type { ColumnDescriptor } from "@finos/vuu-datagrid-types"; - -export type VuuTableName = - | "instruments" - | "instrumentPrices" - | "orders" - | "childOrders" - | "parentOrders" - | "prices" - | "basket" - | "basketDesign" - | "basketDefinitions" - | "basketOrders"; - -// These Schemas take the form of the schemas that we create -// with TABLE_META returned by Vuu. -export const schemas: Readonly>> = { - basket: { - columns: [ - { name: "ID", serverDataType: "string" }, - { name: "name", serverDataType: "string" }, - { name: "notionalValue", serverDataType: "double" }, - { name: "notionalValueUsd", serverDataType: "double" }, - ], - key: "id", - table: { module: "BASKET", table: "basket" }, - }, - basketDefinitions: { - columns: [ - { name: "id", serverDataType: "string" }, - { name: "symbolName", serverDataType: "string" }, - { name: "name", serverDataType: "string" }, - { name: "units", serverDataType: "int" }, - { name: "totalDollarNotional", serverDataType: "double" }, - { name: "totalNotional", serverDataType: "double" }, - { name: "currency", serverDataType: "string" }, - { name: "exchangeRateToUSD", serverDataType: "double" }, - ], - key: "id", - table: { module: "BASKET", table: "basketDefinitions" }, - }, - - basketDesign: { - columns: [ - { name: "ric", serverDataType: "string" }, - { name: "name", serverDataType: "string" }, - { name: "quantity", serverDataType: "double" }, - { name: "weighting", serverDataType: "double" }, - { name: "last", serverDataType: "double" }, - { name: "bid", serverDataType: "double" }, - { name: "ask", serverDataType: "double" }, - { name: "limitPrice", serverDataType: "double" }, - { name: "priceStrategy", serverDataType: "string" }, - { name: "dollarNotional", serverDataType: "double" }, - { name: "localNotional", serverDataType: "double" }, - { name: "venue", serverDataType: "string" }, - { name: "algo", serverDataType: "string" }, - { name: "algoParams", serverDataType: "string" }, - ], - key: "ric", - table: { module: "BASKET", table: "basketDesign" }, - }, - basketOrders: { - columns: [ - { name: "ric", serverDataType: "string" }, - { name: "status", serverDataType: "string" }, - { name: "name", serverDataType: "string" }, - { name: "quantity", serverDataType: "double" }, - { name: "filled", serverDataType: "double" }, - { name: "weighting", serverDataType: "double" }, - { name: "last", serverDataType: "double" }, - { name: "bid", serverDataType: "double" }, - { name: "ask", serverDataType: "double" }, - { name: "limitPrice", serverDataType: "double" }, - { name: "priceSpread", serverDataType: "double" }, - { name: "priceStrategy", serverDataType: "string" }, - { name: "dollarNotional", serverDataType: "double" }, - { name: "localNotional", serverDataType: "double" }, - { name: "venue", serverDataType: "string" }, - { name: "algo", serverDataType: "string" }, - { name: "algoParams", serverDataType: "string" }, - ], - key: "ric", - table: { module: "BASKET", table: "basketOrders" }, - }, - instruments: { - columns: [ - { name: "bbg", serverDataType: "string" }, - { name: "currency", serverDataType: "string" }, - { name: "description", serverDataType: "string" }, - { name: "exchange", serverDataType: "string" }, - { name: "isin", serverDataType: "string" }, - { name: "lotSize", serverDataType: "int" }, - { name: "ric", serverDataType: "string" }, - ], - key: "ric", - table: { module: "SIMUL", table: "instruments" }, - }, - instrumentPrices: { - columns: [ - { name: "ask", serverDataType: "double" }, - { name: "askSize", serverDataType: "double" }, // type: "int" - { name: "bbg", serverDataType: "string" }, - { name: "bid", serverDataType: "double" }, - { name: "bidSize", serverDataType: "double" }, - { name: "close", serverDataType: "double" }, - { name: "currency", serverDataType: "string" }, - { name: "description", serverDataType: "string" }, - { name: "exchange", serverDataType: "string" }, - { name: "isin", serverDataType: "string" }, - { name: "last", serverDataType: "double" }, - { name: "lotSize", serverDataType: "int" }, - { name: "open", serverDataType: "double" }, - { name: "phase", serverDataType: "string" }, - { name: "ric", serverDataType: "string" }, - { name: "scenario", serverDataType: "string" }, - ], - key: "ric", - table: { module: "SIMUL", table: "instrumentPrices" }, - }, - orders: { - columns: [ - { name: "ccy", serverDataType: "string" }, - { name: "created", serverDataType: "long" }, - { name: "filledQuantity", serverDataType: "double" }, - { name: "lastUpdate", serverDataType: "long" }, - { name: "orderId", serverDataType: "string" }, - { name: "quantity", serverDataType: "double" }, - { name: "ric", serverDataType: "string" }, - { name: "side", serverDataType: "string" }, - { name: "trader", serverDataType: "string" }, - ], - key: "orderId", - table: { module: "SIMUL", table: "orders" }, - }, - childOrders: { - columns: [ - { name: "account", serverDataType: "string" }, - { name: "averagePrice", serverDataType: "double" }, - { name: "ccy", serverDataType: "string" }, - { name: "exchange", serverDataType: "string" }, - { name: "filledQty", serverDataType: "double" }, - { name: "id", serverDataType: "string" }, - { name: "idAsInt", serverDataType: "int" }, - { name: "lastUpdate", serverDataType: "long" }, - { name: "openQty", serverDataType: "double" }, - { name: "parentOrderId", serverDataType: "string" }, - { name: "price", serverDataType: "double" }, - { name: "quantity", serverDataType: "double" }, - { name: "ric", serverDataType: "string" }, - { name: "side", serverDataType: "string" }, - { name: "status", serverDataType: "string" }, - { name: "strategy", serverDataType: "string" }, - { name: "volLimit", serverDataType: "int" }, - ], - key: "id", - table: { module: "SIMUL", table: "childOrders" }, - }, - parentOrders: { - columns: [ - { name: "account", serverDataType: "string" }, - { name: "algo", serverDataType: "string" }, - { name: "averagePrice", serverDataType: "double" }, - { name: "ccy", serverDataType: "string" }, - { name: "childCount", serverDataType: "int" }, - { name: "exchange", serverDataType: "string" }, - { name: "filledQty", serverDataType: "double" }, - { name: "id", serverDataType: "string" }, - { name: "idAsInt", serverDataType: "int" }, - { name: "lastUpdate", serverDataType: "long" }, - { name: "openQty", serverDataType: "double" }, - { name: "price", serverDataType: "double" }, - { name: "quantity", serverDataType: "double" }, - { name: "ric", serverDataType: "string" }, - { name: "side", serverDataType: "string" }, - { name: "status", serverDataType: "string" }, - { name: "volLimit", serverDataType: "int" }, - ], - key: "id", - table: { module: "SIMUL", table: "parentOrders" }, - }, - prices: { - columns: [ - { name: "ask", serverDataType: "double" }, - { name: "askSize", serverDataType: "double" }, // type: "int" - { name: "bid", serverDataType: "double" }, - { name: "bidSize", serverDataType: "double" }, - { name: "close", serverDataType: "double" }, - { name: "last", serverDataType: "double" }, - { name: "open", serverDataType: "double" }, - { name: "phase", serverDataType: "string" }, - { name: "ric", serverDataType: "string" }, - { name: "scenario", serverDataType: "string" }, - ], - key: "ric", - table: { module: "SIMUL", table: "prices" }, - }, -}; - -export type ColumnState = { [key: string]: TableSchema }; - -export interface ColumnActionUpdate { - type: "updateColumn"; - column: ColumnDescriptor; -} - -export type ColumnAction = ColumnActionUpdate; - -export const getAllSchemas = () => schemas; - -export const getSchema = (tableName: VuuTableName) => { - if (schemas[tableName]) { - return schemas[tableName]; - } - throw Error(`getSchema no schema for table ${tableName}`); -}; diff --git a/vuu-ui/showcase/src/examples/utils/data-generators/vuu-row-generator.ts b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts similarity index 60% rename from vuu-ui/showcase/src/examples/utils/data-generators/vuu-row-generator.ts rename to vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts index 1c8f0c0eb4..3468f64c1d 100644 --- a/vuu-ui/showcase/src/examples/utils/data-generators/vuu-row-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts @@ -1,8 +1,10 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; import { VuuRowDataItemType, VuuTable } from "@finos/vuu-protocol-types"; -import { RowAtIndexFunc } from "../ArrayProxy"; -import * as dataGenerators from "."; -import { UpdateGenerator } from "../rowUpdates"; +import * as simulDataGenerators from "./simul/data-generators"; +import * as basketDataGenerators from "./basket/data-generators"; +import { UpdateGenerator } from "./rowUpdates"; + +type RowAtIndexFunc = (index: number) => T | undefined; export const VuuColumnGenerator = (columnCount: number): string[] => ["Row No"].concat( @@ -49,7 +51,7 @@ export const DefaultColumnGenerator: ColumnGeneratorFn = ( const defaultGenerators = { ColumnGenerator: DefaultColumnGenerator, - RowGeneratorFactory: DefaultRowGenerator, + RowGenerator: DefaultRowGenerator, }; export const getColumnAndRowGenerator = ( @@ -60,16 +62,31 @@ export const getColumnAndRowGenerator = ( const tableName = table?.table ?? ""; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - if (table?.table && dataGenerators[table?.table] === undefined) { - throw Error( - `vuu-row-gererator table ${table.table} was requested but no generator is registered` - ); + switch (table?.module) { + case "SIMUL": { + const { ColumnGenerator, RowGenerator, createUpdateGenerator } = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + simulDataGenerators[tableName] ?? defaultGenerators; + return [ColumnGenerator, RowGenerator, createUpdateGenerator]; + } + + case "BASKET": { + const { ColumnGenerator, RowGenerator, createUpdateGenerator } = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + basketDataGenerators[tableName] ?? defaultGenerators; + return [ColumnGenerator, RowGenerator, createUpdateGenerator]; + } + case undefined: { + const { ColumnGenerator, RowGenerator } = defaultGenerators; + return [ColumnGenerator, RowGenerator]; + } + default: + throw Error( + `vuu-row-gererator table ${table?.table} was requested but no generator is registered` + ); } - const { ColumnGenerator, RowGenerator, createUpdateGenerator } = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - dataGenerators[tableName] ?? defaultGenerators; - return [ColumnGenerator, RowGenerator, createUpdateGenerator]; }; export const populateArray = ( @@ -82,7 +99,12 @@ export const populateArray = ( const generateRow = rowGen(columnDescriptors.map((col) => col.name)); const data: Array = []; for (let i = 0; i < count; i++) { - data[i] = generateRow(i) as VuuRowDataItemType[]; + const row = generateRow(i); + if (row) { + data[i] = row; + } else { + break; + } } return data; }; diff --git a/vuu-ui/packages/vuu-data-test/tsconfig.json b/vuu-ui/packages/vuu-data-test/tsconfig.json new file mode 100644 index 0000000000..db9582a162 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} 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 0000792179..c919eccf21 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 @@ -16,6 +16,8 @@ import { buildColumnMap, ColumnMap, EventEmitter, + getAddedItems, + getMissingItems, getSelectionStatus, KeySet, logger, @@ -57,7 +59,6 @@ export interface ArrayDataSourceConstructorProps data: Array; rangeChangeRowset?: "delta" | "full"; } - const { debug } = logger("ArrayDataSource"); const { RENDER_IDX, SELECTED } = metadataKeys; @@ -148,6 +149,10 @@ export class ArrayDataSource }: ArrayDataSourceConstructorProps) { super(); + console.log(`ArrayDataSource`, { + columnDescriptors, + }); + if (!data || !columnDescriptors) { throw Error( "ArrayDataSource constructor called without data or without columnDescriptors" @@ -293,6 +298,10 @@ export class ArrayDataSource return this.processedData ?? this.#data; } + get table() { + return this.tableSchema.table; + } + get config() { return this.#config; } @@ -434,6 +443,17 @@ export class ArrayDataSource } set columns(columns: string[]) { + const addedColumns = getAddedItems(this.config.columns, columns); + if (addedColumns.length > 0) { + const columnsWithoutDescriptors = getMissingItems( + this.columnDescriptors, + addedColumns, + (col) => col.name + ); + console.log(`columnsWithoutDescriptors`, { + columnsWithoutDescriptors, + }); + } this.config = { ...this.#config, columns, diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index ca8ac416b4..6e5b083d44 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -212,7 +212,7 @@ const equivalentColumns: DataConfigPredicate = ( (cols1 === undefined && cols2?.length === 0) || (cols2 === undefined && cols1?.length === 0); -const columnsChanged: DataConfigPredicate = (config, newConfig) => { +export const columnsChanged: DataConfigPredicate = (config, newConfig) => { const { columns: cols1 } = config; const { columns: cols2 } = newConfig; @@ -475,11 +475,18 @@ export type DataSourceEvents = { }; export type DataSourceEditHandler = ( - rowIndex: number, + row: DataSourceRow, columnName: string, value: VuuColumnDataType ) => boolean; +export type RpcResponse = + | MenuRpcResponse + | VuuUIMessageInRPCEditReject + | VuuUIMessageInRPCEditResponse; + +export type RpcResponseHandler = (response: RpcResponse) => boolean; + export interface DataSource extends EventEmitter { aggregations: VuuAggregation[]; applyEdit: DataSourceEditHandler; @@ -494,12 +501,7 @@ export interface DataSource extends EventEmitter { groupBy: VuuGroupBy; menuRpcCall: ( rpcRequest: Omit | ClientToServerEditRpc - ) => Promise< - | MenuRpcResponse - | VuuUIMessageInRPCEditReject - | VuuUIMessageInRPCEditResponse - | undefined - >; + ) => Promise; openTreeNode: (key: string) => void; range: VuuRange; select: SelectionChangeHandler; @@ -510,6 +512,7 @@ export interface DataSource extends EventEmitter { props: SubscribeProps, callback: SubscribeCallback ) => Promise; + table?: VuuTable; title?: string; unsubscribe: () => void; viewport?: string; diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index e0563efacb..c9900c37e7 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1 +1,8 @@ -export const workerSourceCode = ""; +export const workerSourceCode = ` +var fe=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var p=(s,e,t)=>(fe(s,e,"read from private field"),t?t.call(s):e.get(s)),U=(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)},me=(s,e,t,n)=>(fe(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function he(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 Y({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ttypeof s=="string"&&ct.includes(s),dt="error",F=()=>{},gt="error",{loggingLevel:N=gt}=ft(),w=s=>{let e=N==="debug",t=e||N==="info",n=t||N==="warn",r=n||N==="error",o=t?g=>console.info(\`[\${s}] \${g}\`):F,a=n?g=>console.warn(\`[\${s}] \${g}\`):F,u=e?g=>console.debug(\`[\${s}] \${g}\`):F;return{errorEnabled:r,error:r?g=>console.error(\`[\${s}] \${g}\`):F}};function ft(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:mt()}}function mt(){let s=be("vuu-logging-level");return pt(s)?s:dt}var{debug:ht,debugEnabled:bt}=w("range-monitor"),W=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)bt&&ht(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function Ce(s){return Array.isArray(s)}function Ct(s){return!Array.isArray(s)}var y,ye=class{constructor(){U(this,y,new Map)}addListener(e,t){let n=p(this,y).get(e);n?Ce(n)?n.push(t):Ct(n)&&p(this,y).set(e,[n,t]):p(this,y).set(e,t)}removeListener(e,t){if(!p(this,y).has(e))return;let n=p(this,y).get(e),r=-1;if(n===t)p(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,p(this,y).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&p(this,y).has(e)?p(this,y).delete(e):e===void 0&&p(this,y).clear()}emit(e,...t){if(p(this,y)){let n=p(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=p(this,y).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(Ce(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 \$=String.fromCharCode(8200),m=String.fromCharCode(8199);var wn={DIGIT:m,TWO_DIGITS:m+m,THREE_DIGITS:m+m+m,FULL_PADDING:[null,\$+m,\$+m+m,\$+m+m+m,\$+m+m+m+m]};var En=m+m+m+m+m+m+m+m+m;var{COUNT:Bn}=x;var q=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 + 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}=x;var{SELECTED:er}=x,I={False:0,True:1,First:2,Last:4};var yt=(s,e)=>e>=s[0]&&e<=s[1],St=I.True+I.First+I.Last,Tt=I.True+I.First,Rt=I.True+I.Last,Z=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return St}else if(yt(t,e))return e===t[0]?Tt:e===t[1]?Rt:I.True;return I.False};var Se=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 wt=(()=>{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:we,info:V,infoEnabled:It,warn:_}=w("websocket-connection"),Ee="ws",vt=s=>s.startsWith(Ee+"://")||s.startsWith(Ee+"s://"),xe={},ee=Symbol("setWebsocket"),B=Symbol("connectionCallback");async function Ie(s,e,t,n=10,r=5){return xe[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},ve(s,e,t)}async function Q(s){throw Error("connection broken")}async function ve(s,e,t,n){let{status:r,connect:o,reconnect:a}=xe[s],u=r==="connecting"?o:a;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",g=await _t(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[ee](g);let i=n!=null?n:new te(g,s,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),i.status=l,u.remaining=u.allowed,i}catch{let g=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:g}),g)return Dt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var Dt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(ve(s,e,t,n))},r)}),_t=(s,e)=>new Promise((t,n)=>{let r=vt(s)?s:\`wss://\${s}\`;It&&e!==void 0&&V(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=a=>n(a)}),Ve=()=>{_==null||_("Connection cannot be closed, socket not yet opened")},Me=s=>{_==null||_(\`Message cannot be sent, socket closed \${s.body.type}\`)},Pt=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},te=class{constructor(e,t,n,r){this.close=Ve;this.requiresLogin=!0;this.send=Me;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=Pt(e.data);this.messagesCount+=1,this[B](t)};this.url=t,this.protocol=n,this[B]=r,this[ee](e)}reconnect(){Q(this)}[(B,ee)](e){let t=this[B];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=()=>{we("\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"?we("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Q(this),this.send=r)},e.onclose=()=>{V==null||V("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Q(this),this.send=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{V==null||V(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=Ve,this.send=Me,V==null||V("close websocket")}}};var Lt=["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"],De=s=>Lt.includes(s.type),ne=({requestId:s,...e})=>[s,e],_e=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]},Pe=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var re=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Le=s=>s.type==="connection-status",Oe=s=>s.type==="connection-metrics";var ke=s=>"viewport"in s,Ae=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&G(s.action.table),G=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var Ue="CHANGE_VP_SUCCESS",Fe="CHANGE_VP_RANGE_SUCCESS",Ne="CLOSE_TREE_NODE",We="CLOSE_TREE_SUCCESS";var \$e="CREATE_VP",qe="CREATE_VP_SUCCESS",Be="DISABLE_VP",Ge="DISABLE_VP_SUCCESS";var Ke="ENABLE_VP",He="ENABLE_VP_SUCCESS";var se="GET_VP_VISUAL_LINKS",je="GET_VIEW_PORT_MENUS";var ze="HB",Je="HB_RESP",Ye="LOGIN",Ze="LOGIN_SUCCESS",Xe="OPEN_TREE_NODE",Qe="OPEN_TREE_SUCCESS";var et="REMOVE_VP";var oe="RPC_RESP";var tt="SET_SELECTION_SUCCESS",ie="TABLE_META_RESP",ae="TABLE_LIST_RESP",nt="TABLE_ROW";var st=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var ot=[],T=w("array-backed-moving-window");function Ot(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 p(this,h).to-t0&&e-p(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-p(this,h).from;if(Ot(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 p(this,h).isWithin(e)&&this.internalData[e-p(this,h).from]!=null?this.internalData[e-p(this,h).from]:void 0}isWithinRange(e){return p(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var g;(g=T.debug)==null||g.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,ot];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let i=e;io.to){let i=Math.max(e,o.to);a=this.internalData.slice(i-u,t-u)}else{let i=Math.min(o.from,t);a=this.internalData.slice(e-u,i-u)}return[this.bufferBreakout(e,t),a]}setRange(e,t){var n,r;if(e!==p(this,h).from||t!==p(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,a]=p(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 kt=[],{debug:b,debugEnabled:H,error:At,info:d,infoEnabled:Ut,warn:P}=w("viewport"),Ft=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),j=[void 0,void 0],Nt={count:0,mode:void 0,size:0,ts:0},z=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:a,range:u,sort:c,title:g,viewport:i,visualLink:l},f){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.tableSchema=null;this.useBatchMode=!0;this.lastUpdateStatus=Nt;this.updateThrottleTimer=void 0;this.rangeMonitor=new W("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:a}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)?(d==null||d("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=i,this.columns=n,this.filter=r,this.groupBy=o,this.keys=new q(u),this.pendingLinkedParent=l,this.table=a,this.sort=c,this.title=g,Ut&&(d==null||d(\`constructor #\${i} \${a.table} bufferSize=\${t}\`)),this.dataWindow=new K(this.clientRange,u,this.bufferSize),this.postMessageToClient=f}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:\$e,table:this.table,range:Y(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:a,groupBy:u}){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:a,tableSchema:this.tableSchema}}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){At("no matching operation found to complete");return}let{type:a}=o;if(d==null||d(\`completeOperation \${a}\`),r.delete(e),a==="CHANGE_VP_RANGE"){let[c,g]=t;(u=this.dataWindow)==null||u.setRange(c,g);for(let i=this.pendingRangeRequests.length-1;i>=0;i--){let l=this.pendingRangeRequests[i];if(l.requestId===e){l.acked=!0;break}else P==null||P("range requests sent faster than they are being ACKed")}}else if(a==="config"){let{aggregations:c,columns:g,filter:i,groupBy:l,sort:f}=o.data;return this.aggregations=c,this.columns=g,this.filter=i,this.groupBy=l,this.sort=f,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),b==null||b(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:a,config:o.data}}else{if(a==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,b==null||b(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:a,groupBy:o.data};if(a==="columns")return this.columns=o.data,{clientViewportId:n,type:a,columns:o.data};if(a==="filter")return this.filter=o.data,{clientViewportId:n,type:a,filter:o.data};if(a==="aggregate")return this.aggregations=o.data,{clientViewportId:n,type:"aggregate",aggregations:this.aggregations};if(a==="sort")return this.sort=o.data,{clientViewportId:n,type:a,sort:this.sort};if(a!=="selection"){if(a==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:n};if(a==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:n};if(a==="CREATE_VISUAL_LINK"){let[c,g,i]=t;return this.linkedParent={colName:c,parentViewportId:g,parentColName:i},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:g,parentColName:i}}else if(a==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){H&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),a,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...Y(t,this.bufferSize,u)}:null;if(c){H&&(b==null||b(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});let i=this.pendingRangeRequests.at(-1);if(i)if(i.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:f}=i;this.dataWindow.outOfRange(l,f)?a={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 g=this.isTree?le:ue;return o.length?[c,o.map(i=>g(i,this.keys,this.selectedRows))]:a?[c,void 0,a]:[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}}setTableSchema(e){this.tableSchema=e}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Xe,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ne,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,d==null||d("suspend")}resume(){return this.suspended=!1,H&&(b==null||b(\`resume: \${this.currentData()}\`)),this.currentData()}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?le:ue;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),d==null||d(\`enable: \${this.serverViewportId}\`),{type:Ke,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),d==null||d(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Be,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),b==null||b(\`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 d==null||d(\`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),H?b==null||b(\`setConfig \${JSON.stringify(t)}\`):d==null||d("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}),d==null||d(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),d==null||d(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=kt){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}),d==null||d(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:Se(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],a=!0;if(e>=r&&er&&t0){e=[],t="update";for(let a of this.pendingUpdates)e.push(o(a,n,r));this.pendingUpdates.length=0}else{let a=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of a)e.push(o(u,n,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?j:[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}}},ue=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?Z(o,s):0].concat(n),le=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[a,u,,c,,g,...i]=n;return[s,r.keyFor(s),c,u,a,g,e,t?Z(o,s):0].concat(i)};var it=1;var{debug:E,debugEnabled:L,error:O,info:S,infoEnabled:Wt,warn:k}=w("server-proxy"),C=()=>\`\${it++}\`,\$t={},qt=s=>s.disabled!==!0&&s.suspended!==!0,Bt={type:"NO_ACTION"},Gt=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function Kt(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.pendingTableMetaRequests=new Map;this.pendingRequests=new Map;this.queuedRequests=[];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]=he(Array.from(this.viewports.values()),qt);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.forEach(o=>{let{clientViewportId:a}=o;this.viewports.set(a,o),this.sendMessageToServer(o.subscribe(),a)})};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:Ye,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&O("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))O(\`spurious subscribe call \${e.viewport}\`);else{if(!this.hasSchemaForTable(e.table)&&!G(e.table)){S==null||S(\`subscribe to \${e.table.table}, no metadata yet, request metadata\`);let n=C();this.sendMessageToServer({type:"GET_TABLE_META",table:e.table},n),this.pendingTableMetaRequests.set(n,e.viewport)}let t=new z(e,this.postMessageToClient);this.viewports.set(e.viewport,t),this.sendIfReady(t.subscribe(),e.viewport,this.sessionId!=="")}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(S==null||S(\`Unsubscribe Message (Client to Server): + \${t}\`),this.sendMessageToServer({type:et,viewPortId:t})):O(\`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=C(),[r,o,a]=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})):a&&this.postMessageToClient(a)}setConfig(e,t){let n=C(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=C(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=C(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=C(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=C(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=C(),{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=C(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=C(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=C(),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&&(E==null||E("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=C();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=C();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,a=C(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(a,o,u,r);this.sendMessageToServer(c,a)}else O("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=C(),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(a=>a.parentVpId===t)){let[a]=o.setLinks(Gt(o.links,t,n));this.postMessageToClient(a)}}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]=ne(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}rpcCall(e){let[t,n]=ne(e),r=st(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){if(E==null||E(\`handleMessageFromClient: \${e.type}\`),ke(e))if(e.type==="disable"){let t=this.getViewportForClient(e.viewport,!1);return t!==null?this.disableViewport(t):void 0}else{let t=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(t,e);case"config":return this.setConfig(t,e);case"aggregate":return this.aggregate(t,e);case"sort":return this.sort(t,e);case"groupBy":return this.groupBy(t,e);case"filter":return this.filter(t,e);case"select":return this.select(t,e);case"suspend":return this.suspendViewport(t);case"resume":return this.resumeViewport(t);case"enable":return this.enableViewport(t);case"openTreeNode":return this.openTreeNode(t,e);case"closeTreeNode":return this.closeTreeNode(t,e);case"createLink":return this.createLink(t,e);case"removeLink":return this.removeLink(t);case"setColumns":return this.setColumns(t,e);case"setTitle":return this.setTitle(t,e);default:}}else{if(De(e))return this.menuRpcCall(e);{let{type:t,requestId:n}=e;switch(t){case"GET_TABLE_LIST":return this.sendMessageToServer({type:t},n);case"GET_TABLE_META":return this.sendMessageToServer({type:t,table:e.table},n);case"RPC_CALL":return this.rpcCall(e);default:}}}O(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}awaitResponseToMessage(e){return new Promise((t,n)=>{let r=C();this.sendMessageToServer(e,r),this.pendingRequests.set(r,{reject:n,resolve:t})})}sendIfReady(e,t,n=!0){return n?this.sendMessageToServer(e,t):this.queuedRequests.push(e),n}sendMessageToServer(e,t=\`\${it++}\`,n=\$t){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:i}=o;this.pendingRequests.delete(n),i(t);return}let{viewports:a}=this;switch(t.type){case ze:this.sendMessageToServer({type:Je,ts:+new Date},"NA");break;case Ze: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 qe:{let i=a.get(n);if(i){let{status:l}=i,{viewPortId:f}=t;n!==f&&(a.delete(n),a.set(f,i)),this.mapClientToServerViewport.set(n,f);let R=i.handleSubscribed(t);R&&(this.postMessageToClient(R),L&&E(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(R)}\`)),i.disabled&&this.disableViewport(i),l==="subscribing"&&!G(i.table)&&(this.sendMessageToServer({type:se,vpId:f}),this.sendMessageToServer({type:je,vpId:f}),Array.from(a.entries()).filter(([M,{disabled:A}])=>M!==f&&!A).forEach(([M])=>{this.sendMessageToServer({type:se,vpId:M})}))}}break;case"REMOVE_VP_SUCCESS":{let i=a.get(t.viewPortId);i&&(this.mapClientToServerViewport.delete(i.clientViewportId),a.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case tt:{let i=this.viewports.get(t.vpId);i&&i.completeOperation(n)}break;case Ue:case Ge:if(a.has(t.viewPortId)){let i=this.viewports.get(t.viewPortId);if(i){let l=i.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),L&&E(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case He:{let i=this.viewports.get(t.viewPortId);if(i){let l=i.completeOperation(n);if(l){this.postMessageToClient(l);let f=i.currentData();L&&E(\`Enable Response (ServerProxy to Client): \${JSON.stringify(l)}\`),i.size===0?L&&E("Viewport Enabled but size 0, resend to server"):(this.postMessageToClient({clientViewportId:i.clientViewportId,mode:"batch",rows:f,size:i.size,type:"viewport-update"}),L&&E(\`Enable Response (ServerProxy to Client): send size \${i.size} \${f.length} rows from cache\`))}}}break;case nt:{let i=Pe(t.rows);for(let[l,f]of Object.entries(i)){let R=a.get(l);R?R.updateRows(f):k==null||k(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case Fe:{let i=this.viewports.get(t.viewPortId);if(i){let{from:l,to:f}=t;i.completeOperation(n,l,f)}}break;case Qe:case We:break;case"CREATE_VISUAL_LINK_SUCCESS":{let i=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(i&&l){let{childColumnName:f,parentColumnName:R}=t,M=i.completeOperation(n,f,l.clientViewportId,R);M&&this.postMessageToClient(M)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let i=this.viewports.get(t.childVpId);if(i){let l=i.completeOperation(n);l&&this.postMessageToClient(l)}}break;case ae:this.postMessageToClient({type:ae,tables:t.tables,requestId:n});break;case ie:{let i=this.cacheTableMeta(t),l=this.pendingTableMetaRequests.get(n);if(l){this.pendingTableMetaRequests.delete(n);let f=this.viewports.get(l);f?f.setTableSchema(i):k==null||k("Message has come back AFTER CREATE_VP_SUCCESS, what do we do now")}else this.postMessageToClient({type:ie,tableSchema:i,requestId:n})}break;case"VP_VISUAL_LINKS_RESP":{let i=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(i.length&&l){let f=Kt(i,this.viewports),[R,M]=l.setLinks(f);if(this.postMessageToClient(R),M){let{link:A,parentClientVpId:at}=M,de=C(),ge=this.mapClientToServerViewport.get(at);if(ge){let ut=l.createLink(de,A.fromColumn,ge,A.toColumn);this.sendMessageToServer(ut,de)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let i=this.viewports.get(t.vpId);if(i){let l=i.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_RESP":if(Ae(t)){let{action:i,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:i.table}).then(f=>{let R=re(f);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...i,tableSchema:R},tableAlreadyOpen:this.isTableOpen(i.table),requestId:n})})}else{let{action:i}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:i||Bt,tableAlreadyOpen:i!==null&&this.isTableOpen(i.table),requestId:n})}break;case oe:{let{method:i,result:l}=t;this.postMessageToClient({type:oe,method:i,result:l,requestId:n})}break;case"ERROR":O(t.msg);break;default:Wt&&S(\`handleMessageFromServer \${t.type}.\`)}}hasSchemaForTable(e){return this.cachedTableSchemas.has(\`\${e.module}:\${e.table}\`)}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=re(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!==j){let[r,o]=n,a=e.getNewRowCount();(a!==void 0||r&&r.length>0)&&(L&&E(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${a}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:a,type:"viewport-update"}))}}})}};var D,{info:ce,infoEnabled:pe}=w("worker");async function Ht(s,e,t,n,r,o,a){let u=await Ie(s,e,c=>{Oe(c)?(console.log("post connection metrics"),postMessage({type:"connection-metrics",messages:c})):Le(c)?(r(c),c.status==="reconnected"&&D.reconnect()):D.handleMessageFromServer(c)},o,a);D=new J(u,c=>jt(c)),u.requiresLogin&&await D.login(t,n)}function jt(s){postMessage(s)}var zt=async({data:s})=>{switch(s.type){case"connect":await Ht(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":pe&&ce(\`client subscribe: \${JSON.stringify(s)}\`),D.subscribe(s);break;case"unsubscribe":pe&&ce(\`client unsubscribe: \${JSON.stringify(s)}\`),D.unsubscribe(s.viewport);break;default:pe&&ce(\`client message: \${JSON.stringify(s)}\`),D.handleMessageFromClient(s)}};self.addEventListener("message",zt);postMessage({type:"ready"}); + +`; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data/src/json-data-source.ts b/vuu-ui/packages/vuu-data/src/json-data-source.ts index 3f032bea74..b7879cf587 100644 --- a/vuu-ui/packages/vuu-data/src/json-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/json-data-source.ts @@ -92,6 +92,7 @@ export class JsonDataSource } [this.columnDescriptors, this.#data] = jsonToDataSourceRows(data); + this.visibleRows = this.#data .filter((row) => row[DEPTH] === 0) .map((row, index) => @@ -133,8 +134,6 @@ export class JsonDataSource ) { this.clientCallback = callback; - console.log(`subscribe range ${range?.from} ${range?.to}`); - if (aggregations) { this.#aggregations = aggregations; } @@ -320,6 +319,10 @@ export class JsonDataSource this.#aggregations = aggregations; } + set data(data: JsonData) { + console.log(`set JsonDataSource data`); + } + get sort() { return this.#sort; } 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 3ebc48a577..aaa8245fa3 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -1,4 +1,4 @@ -import { DataSourceFilter } from "@finos/vuu-data-types"; +import { DataSourceFilter, DataSourceRow } from "@finos/vuu-data-types"; import { Selection } from "@finos/vuu-datagrid-types"; import { ClientToServerEditRpc, @@ -18,6 +18,7 @@ import { EventEmitter, itemsOrOrderChanged, logger, + metadataKeys, throttle, uuid, } from "@finos/vuu-utils"; @@ -43,6 +44,19 @@ type RangeRequest = (range: VuuRange) => void; const { info } = logger("RemoteDataSource"); +const { KEY } = metadataKeys; + +type DataSourceStatus = + | "disabled" + | "disabling" + | "enabled" + | "enabling" + | "initialising" + | "subscribing" + | "subscribed" + | "suspended" + | "unsubscribed"; + /*----------------------------------------------------------------- A RemoteDataSource manages a single subscription via the ServerProxy ----------------------------------------------------------------*/ @@ -52,7 +66,7 @@ export class RemoteDataSource { private bufferSize: number; private server: ServerAPI | null = null; - private status = "initialising"; + private status: DataSourceStatus = "initialising"; private clientCallback: SubscribeCallback | undefined; private configChangePending: DataSourceConfig | undefined; private rangeRequest: RangeRequest; @@ -115,6 +129,7 @@ export class RemoteDataSource }: SubscribeProps, callback: SubscribeCallback ) { + console.log(`%csubscribe`, "color:red;font-weight:bold;"); this.clientCallback = callback; if (aggregations || columns || filter || groupBy || sort) { @@ -135,7 +150,7 @@ export class RemoteDataSource this.#range = range; } - if (this.status !== "initialising") { + if (this.status !== "initialising" && this.status !== "unsubscribed") { return; } @@ -198,12 +213,16 @@ export class RemoteDataSource }; unsubscribe() { + console.log("%cunsubscribe", "color:red;font-weight:bold;"); + info?.(`unsubscribe #${this.viewport}`); if (this.viewport) { this.server?.unsubscribe(this.viewport); } this.server?.destroy(this.viewport); this.removeAllListeners(); + this.status = "unsubscribed"; + this.viewport = undefined; } suspend() { @@ -598,8 +617,19 @@ export class RemoteDataSource } } - applyEdit(rowIndex: number, columnName: string, value: VuuColumnDataType) { - console.log(`ArrayDataSource applyEdit ${rowIndex} ${columnName} ${value}`); + applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { + console.log( + `ArrayDataSource applyEdit ${row.join(",")} ${columnName} ${value}` + ); + + this.menuRpcCall({ + rowKey: row[KEY], + field: columnName, + value: parseInt(value), + type: "VP_EDIT_CELL_RPC", + }).then(() => { + console.log("response"); + }); return true; } } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts index 682a332a12..40cebe351b 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts @@ -12,6 +12,10 @@ function dataIsUnchanged(newRow: VuuRow, existingRow?: VuuRow) { return false; } + if (existingRow.data.length !== newRow.data.length) { + return false; + } + if (existingRow.sel !== newRow.sel) { return false; } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts index 035bb129d8..5f824d06df 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts @@ -720,7 +720,7 @@ export class ServerProxy { case "REMOVE_VP_SUCCESS": { - const viewport = this.viewports.get(body.viewPortId); + const viewport = viewports.get(body.viewPortId); if (viewport) { this.mapClientToServerViewport.delete(viewport.clientViewportId); viewports.delete(body.viewPortId); diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts index 80b7c2a37c..3858fcab22 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -918,7 +918,6 @@ export class Viewport { // alleviate pressure on UI DataTable. private shouldThrottleMessage = (mode: DataUpdateMode) => { const elapsedTime = this.setLastUpdate(mode); - console.log(`elapsed time = ${elapsedTime}`); return ( mode === "size-only" && elapsedTime > 0 && diff --git a/vuu-ui/packages/vuu-data/src/websocket-connection.ts b/vuu-ui/packages/vuu-data/src/websocket-connection.ts index e0a8a5976c..604138cbd4 100644 --- a/vuu-ui/packages/vuu-data/src/websocket-connection.ts +++ b/vuu-ui/packages/vuu-data/src/websocket-connection.ts @@ -69,12 +69,14 @@ export async function connect( async function reconnect(connection: WebsocketConnection) { //TODO it's not enough to reconnect with a new websocket, we have to log back in as well - makeConnection( - connection.url, - connection.protocol, - connection[connectionCallback], - connection - ); + // Temp don't try to reconnect at all until better interop with a proxy is implemented + // makeConnection( + // connection.url, + // connection.protocol, + // connection[connectionCallback], + // connection + // ); + throw Error("connection broken"); } async function makeConnection( diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 503c026b90..92b7b7ea7d 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -20,7 +20,7 @@ export type TableHeading = { label: string; width: number }; export type TableHeadings = TableHeading[][]; export type DataCellEditHandler = ( - rowIndex: number, + row: DataSourceRow, columnName: string, value: VuuColumnDataType ) => boolean; @@ -129,7 +129,6 @@ export interface ColumnDescriptor { align?: ColumnAlignment; className?: string; editable?: boolean; - expression?: string; flex?: number; /** Optional additional level(s) of heading to display above label. diff --git a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx b/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx index 0afc6f2eaf..1b60895f86 100644 --- a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx @@ -1,5 +1,5 @@ import { GridConfig } from "@finos/vuu-datagrid-types"; -import { Table, TableProps } from "@finos/vuu-table"; +import { Table, TablePropsDeprecated as TableProps } from "@finos/vuu-table"; import { ReactElement, useCallback, useState } from "react"; import { Dialog } from "@finos/vuu-popups"; diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx index 373b1459ad..5c128e1d20 100644 --- a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx @@ -2,7 +2,7 @@ import { TableProps } from "@finos/vuu-table"; import { JsonData } from "@finos/vuu-utils"; import { TableNext } from "@finos/vuu-table"; import { JsonDataSource } from "@finos/vuu-data"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { TableConfig } from "@finos/vuu-datagrid-types"; export interface JsonTableProps @@ -16,19 +16,39 @@ export interface JsonTableProps export const JsonTable = ({ config, - source = { "": "" }, + source: sourceProp = { "": "" }, ...tableProps }: JsonTableProps) => { - const [dataSource, tableConfig] = useMemo< - [JsonDataSource, TableConfig] - >(() => { - const ds = new JsonDataSource({ - data: source, + const sourceRef = useRef(sourceProp); + const dataSourceRef = useRef(); + useMemo(() => { + dataSourceRef.current = new JsonDataSource({ + data: sourceRef.current, }); + }, []); + + const tableConfig = useMemo(() => { + return { + ...config, + columns: dataSourceRef.current?.columnDescriptors ?? [], + }; + }, [config]); + + useEffect(() => { + if (dataSourceRef.current) { + dataSourceRef.current.data = sourceProp; + } + }, [sourceProp]); + + if (dataSourceRef.current === undefined) { + return null; + } - return [ds, { ...config, columns: ds.columnDescriptors }]; - }, [config, source]); return ( - + ); }; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx index faefa63b94..7186d9d318 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx @@ -1,7 +1,7 @@ import { TableSchema } from "@finos/vuu-data"; import { DataSourceFilter } from "@finos/vuu-data-types"; import { Filter } from "@finos/vuu-filter-types"; -import { Toolbar } from "@finos/vuu-layout"; +import { ActiveItemChangeHandler, Toolbar } from "@finos/vuu-layout"; import { Prompt } from "@finos/vuu-popups"; import { Button } from "@salt-ds/core"; import cx from "classnames"; @@ -19,6 +19,7 @@ export interface FilterBarProps extends HTMLAttributes { activeFilterIndex?: number[]; filters: Filter[]; onApplyFilter: (filter: DataSourceFilter) => void; + onChangeActiveFilterIndex: ActiveItemChangeHandler; onFiltersChanged?: (filters: Filter[]) => void; showMenu?: boolean; tableSchema: TableSchema; @@ -32,6 +33,7 @@ export const FilterBar = ({ className: classNameProp, filters: filtersProp, onApplyFilter, + onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp = false, tableSchema, @@ -58,6 +60,7 @@ export const FilterBar = ({ containerRef: rootRef, filters: filtersProp, onApplyFilter, + onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp, }); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts index 63b59f86b4..130e755579 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts @@ -33,6 +33,7 @@ export interface FilterBarHookProps | "activeFilterIndex" | "filters" | "onApplyFilter" + | "onChangeActiveFilterIndex" | "onFiltersChanged" | "showMenu" > { @@ -46,6 +47,7 @@ export const useFilterBar = ({ containerRef, filters: filtersProp, onApplyFilter, + onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp, }: FilterBarHookProps) => { @@ -308,8 +310,9 @@ export const useFilterBar = ({ const handleChangeActiveFilterIndex = useCallback( (itemIndex) => { setActiveFilterIndex(itemIndex); + onChangeActiveFilterIndexProp?.(itemIndex); }, - [] + [onChangeActiveFilterIndexProp] ); const handleClickAddFilter = useCallback(() => { @@ -327,7 +330,6 @@ export const useFilterBar = ({ }; const handleChangeFilterClause = (filterClause: Partial) => { - console.log({ filterClause }); if (filterClause !== undefined) { setEditFilter((filter) => replaceClause(filter, filterClause)); setShowMenu(true); diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.css b/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.css deleted file mode 100644 index 7debb14502..0000000000 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.css +++ /dev/null @@ -1,23 +0,0 @@ -.vuu-theme { - --salt-actionable-secondary-background: #ffffff; - --salt-actionable-secondary-background-hover: #f37880; - --salt-actionable-secondary-background-disabled: #ffffff; - --salt-actionable-secondary-foreground: #606477; - --salt-actionable-secondary-foreground-hover: #15171b; - --salt-actionable-secondary-foreground-active: #ffffff; - --salt-actionable-secondary-foreground-disabled: #9b9ea8; -} - -.vuuFilterClause-closeButton { - height: 16px; - width: 16px; - border-radius: 6px; - padding: 0; -} - -.vuuFilterClause-closeButton:focus-visible { -} - -.vuuFilterClause-closeButton:not(:hover) { - background: var(--salt-actionable-secondary-background); -} diff --git a/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.tsx b/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.tsx deleted file mode 100644 index 27defd0167..0000000000 --- a/vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Button } from "@salt-ds/core"; -import "./CloseButton.css"; - -type CloseButtonProps = { - className: string; - onClick: () => void; -}; - -export const CloseButton = ({ className, onClick }: CloseButtonProps) => ( - - - - ); }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts index e83f0f1ebc..7345c6e75f 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-expression-panel/useColumnExpression.ts @@ -10,7 +10,7 @@ import { ColumnExpressionPanelProps } from "./ColumnExpressionPanel"; export type ColumnExpressionHookProps = Pick< ColumnExpressionPanelProps, - "column" | "onChangeName" | "onSave" + "column" | "onChangeName" >; const applyDefaults = (column: ColumnDescriptor) => { @@ -18,7 +18,7 @@ const applyDefaults = (column: ColumnDescriptor) => { if (type === "") { return { ...column, - name: `${name}:${expression}:string`, + name: `${name}:string:${expression}`, }; } else { return column; @@ -28,27 +28,32 @@ const applyDefaults = (column: ColumnDescriptor) => { export const useColumnExpression = ({ column: columnProp, onChangeName: onChangeNameProp, - onSave: onSaveProp, }: ColumnExpressionHookProps) => { - const [column, setColumn] = useState( + const [column, _setColumn] = useState( applyDefaults(columnProp) ); + const columnRef = useRef(columnProp); + const setColumn = useCallback((column: ColumnDescriptor) => { + columnRef.current = column; + _setColumn(column); + }, []); + // We need to track column name in a ref because ColunExpressionInput // is not a pure React component, it hosts a CodeMirror editor. We // do not want to cause it to render mid-edit. Therefore it uses memo // and only renders on initial load. onChangeExpression must be stable. - const columnNameRef = useRef(column.name); - const expressionRef = useRef(getCalculatedColumnDetails(column)[1]); + // const columnNameRef = useRef(column.name); + // const expressionRef = useRef(getCalculatedColumnDetails(column)[1]); const onChangeName = useCallback( (evt) => { const { value } = evt.target as HTMLInputElement; const newColumn = setCalculatedColumnName(column, value); - columnNameRef.current = newColumn.name; + // columnNameRef.current = newColumn.name; setColumn(newColumn); onChangeNameProp?.(newColumn.name); }, - [column, onChangeNameProp] + [column, onChangeNameProp, setColumn] ); const onChangeExpression = useCallback( @@ -56,12 +61,19 @@ export const useColumnExpression = ({ // we do not set state when this changes as the codemirror editor // manages state of the expression for us until complete const expression = value.trim(); - expressionRef.current = expression; - const [name, , type] = columnNameRef.current.split(":"); - columnNameRef.current = `${name}:${expression}:${type}`; - onChangeNameProp?.(columnNameRef.current); + // expressionRef.current = expression; + // const [name, , type] = column.name.split(":"); + // columnNameRef.current = `${name}:${expression}:${type}`; + + const { current: column } = columnRef; + const newColumn = setCalculatedColumnExpression(column, expression); + setColumn(newColumn); + + onChangeNameProp?.(newColumn.name); + + // console.log(`calculatedColumnName ${columnNameRef.current}`); }, - [onChangeNameProp] + [onChangeNameProp, setColumn] ); const onChangeType = useCallback( @@ -72,23 +84,13 @@ export const useColumnExpression = ({ onChangeNameProp?.(newColumn.name); } }, - [column, onChangeNameProp] + [column, onChangeNameProp, setColumn] ); - const onSave = useCallback(() => { - const newColumn = setCalculatedColumnExpression( - column, - expressionRef.current - ); - setColumn(newColumn); - onSaveProp(newColumn); - }, [column, onSaveProp]); - return { column, onChangeExpression, onChangeName, onChangeType, - onSave, }; }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css index 7bdb502bfd..eeafbcb297 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.css @@ -1,6 +1,7 @@ .vuuColumnList { + --vuuMeasuredContainer-flex: 1 1 1px; --vuu-svg-function: url('data:image/svg+xml;utf8,'); --vuuList-borderStyle: none; --vuuListItem-padding: 0; @@ -26,6 +27,11 @@ flex: 1 1 auto; } +.vuuColumnList-text:hover { + font-weight: 600; + text-decoration: underline; +} + .vuuColumnList-checkBox { flex: 0 0 20px; } diff --git a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx index e76725906a..8831f89f89 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-list/ColumnList.tsx @@ -8,10 +8,16 @@ import { Checkbox } from "@salt-ds/core"; import { Switch } from "@salt-ds/lab"; import cx from "classnames"; import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { HTMLAttributes, SyntheticEvent, useCallback } from "react"; +import { + HTMLAttributes, + MouseEventHandler, + SyntheticEvent, + useCallback, +} from "react"; import { ColumnItem } from "../table-settings"; import "./ColumnList.css"; +import { getColumnLabel } from "@finos/vuu-utils"; const classBase = "vuuColumnList"; const classBaseListItem = "vuuColumnListItem"; @@ -27,6 +33,7 @@ export interface ColumnListProps columnItems: ColumnItem[]; onChange: ColumnChangeHandler; onMoveListItem: ListProps["onMoveListItem"]; + onNavigateToColumn?: (columnName: string) => void; } const ColumnListItem = ({ @@ -40,12 +47,15 @@ const ColumnListItem = ({ className={cx(classNameProp, classBaseListItem)} data-name={item?.name} > + {item?.isCalculated ? ( ) : ( )} - {item?.label ?? item?.name} + + {getColumnLabel(item as ColumnDescriptor)} + { const handleChange = useCallback( @@ -83,6 +94,17 @@ export const ColumnList = ({ }, [onChange] ); + + const handleClick = useCallback((evt) => { + const targetEl = evt.target as HTMLElement; + if (targetEl.classList.contains("vuuColumnList-text")) { + const listItemEl = targetEl.closest(".vuuListItem") as HTMLElement; + if (listItemEl?.dataset.name) { + onNavigateToColumn?.(listItemEl.dataset.name); + } + } + }, []); + return (
@@ -95,8 +117,9 @@ export const ColumnList = ({ ListItem={ColumnListItem} allowDragDrop - height="100%" + height="auto" onChange={handleChange} + onClick={handleClick} onMoveListItem={onMoveListItem} selectionStrategy="none" source={columnItems} diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css index c08eb64a61..d13c7d0aa4 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css @@ -1,8 +1,13 @@ .vuuColumnNameLabel-calculated { + cursor: pointer; display: flex; gap: 2px; } +.vuuColumnNameLabel-edit { + margin-left: auto; +} + .vuuColumnNameLabel-placeholder { color: var(--vuu-color-gray-35); } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx index 6dd6259d4a..95847da2f5 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx @@ -7,30 +7,36 @@ import { } from "@finos/vuu-utils"; import "./ColumnNameLabel.css"; +import { MouseEventHandler } from "react"; const classBase = "vuuColumnNameLabel"; export interface ColumnNameLabelProps { column: ColumnDescriptor; + onClick: MouseEventHandler; } -export const ColumnNameLabel = ({ column }: ColumnNameLabelProps) => { +export const ColumnNameLabel = ({ column, onClick }: ColumnNameLabelProps) => { if (isCalculatedColumn(column.name)) { - const [name, expression, type] = getCalculatedColumnDetails(column); + const [name, type, expression] = getCalculatedColumnDetails(column); const displayName = name || "name"; - const displayExpression = "expression"; + const displayExpression = "=expression"; const nameClass = displayName === "name" ? `${classBase}-placeholder` : undefined; const expressionClass = expression === "" ? `${classBase}-placeholder` : undefined; return ( -
+
{displayName} : - {displayExpression} - : {type || "string"} + : + {displayExpression} +
); } else { diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css index 8fb584a561..f3020185e7 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.css @@ -67,6 +67,9 @@ --vuu-icon-svg: var(--vuu-svg-pin-right); } + .vuuColumnSettingsPanel-editing .vuuColumnNameLabel-edit { + display: none; + } diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx index b07ba98e8b..ec43db60c6 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnSettingsPanel.tsx @@ -1,5 +1,6 @@ import { ColumnDescriptor, TableConfig } from "@finos/vuu-datagrid-types"; import { VuuTable } from "@finos/vuu-protocol-types"; +import { VuuInput } from "@finos/vuu-ui-controls"; import { getCalculatedColumnName, getDefaultAlignment, @@ -12,14 +13,14 @@ import { ToggleButton, ToggleButtonGroup, } from "@salt-ds/core"; +import cx from "classnames"; import { HTMLAttributes } from "react"; +import { ColumnExpressionPanel } from "../column-expression-panel"; import { ColumnFormattingPanel } from "../column-formatting-settings"; +import { ColumnNameLabel } from "./ColumnNameLabel"; import { useColumnSettings } from "./useColumnSettings"; -import { ColumnExpressionPanel } from "../column-expression-panel"; -import { VuuInput } from "@finos/vuu-ui-controls"; import "./ColumnSettingsPanel.css"; -import { ColumnNameLabel } from "./ColumnNameLabel"; const classBase = "vuuColumnSettingsPanel"; @@ -35,6 +36,7 @@ const getColumnLabel = (column: ColumnDescriptor) => { export interface ColumnSettingsProps extends HTMLAttributes { column: ColumnDescriptor; onConfigChange: (config: TableConfig) => void; + onCancelCreateColumn: () => void; onCreateCalculatedColumn: (column: ColumnDescriptor) => void; tableConfig: TableConfig; vuuTable: VuuTable; @@ -42,6 +44,7 @@ export interface ColumnSettingsProps extends HTMLAttributes { export const ColumnSettingsPanel = ({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -55,14 +58,17 @@ export const ColumnSettingsPanel = ({ column, navigateNextColumn, navigatePrevColumn, + onCancel, onChange, onChangeCalculatedColumnName, onChangeFormatting, onChangeRenderer, + onEditCalculatedColumn, onInputCommit, onSave, } = useColumnSettings({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -77,16 +83,19 @@ export const ColumnSettingsPanel = ({ } = column; return ( -
+
- +
{editCalculatedColumn ? ( @@ -167,27 +176,46 @@ export const ColumnSettingsPanel = ({ onChangeRenderer={onChangeRenderer} /> -
- - + +
+ ) : ( +
- NEXT - -
+ + +
+ )}
); }; diff --git a/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts b/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts index 29f39c9ee0..7574d6c5ac 100644 --- a/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts +++ b/vuu-ui/packages/vuu-table-extras/src/column-settings/useColumnSettings.ts @@ -19,11 +19,13 @@ import { SingleSelectionHandler } from "@finos/vuu-ui-controls"; import { FormEventHandler, useCallback, + useEffect, useMemo, useRef, useState, } from "react"; import { ColumnSettingsProps } from "./ColumnSettingsPanel"; +import { ColumnExpressionSubmitHandler } from "../column-expression-input"; const integerCellRenderers: CellRendererDescriptor[] = [ { @@ -81,7 +83,7 @@ const getCellRendererDescriptor = ( } } } - // retur the appropriate default value for the column + // returm the appropriate default value for the column const typedAvailableRenderers = getAvailableCellRenderers(column); return typedAvailableRenderers[0]; }; @@ -123,6 +125,7 @@ const replaceColumn = ( export const useColumnSettings = ({ column: columnProp, + onCancelCreateColumn, onConfigChange, onCreateCalculatedColumn, tableConfig, @@ -130,9 +133,19 @@ export const useColumnSettings = ({ const [column, setColumn] = useState( getColumn(tableConfig.columns, columnProp) ); - const [editCalculatedColumn, setEditCalculatedColumn] = useState( - column.name === "::" - ); + const columnRef = useRef(column); + const [inEditMode, setEditMode] = useState(column.name === "::"); + + const handleEditCalculatedcolumn = useCallback(() => { + columnRef.current = column; + setEditMode(true); + }, [column]); + + useEffect(() => { + setColumn(columnProp); + setEditMode(columnProp.name === "::"); + }, [columnProp]); + const availableRenderers = useMemo(() => { return getAvailableCellRenderers(column); }, [column]); @@ -239,28 +252,35 @@ export const useColumnSettings = ({ navigateColumn({ moveBy: -1 }); }, [navigateColumn]); - const handleSaveCalculatedColumn = useCallback( - (calculatedColumn: ColumnDescriptor) => { - // TODO validate expression, unique name - onCreateCalculatedColumn({ - ...column, - ...calculatedColumn, - }); - }, - [column, onCreateCalculatedColumn] - ); + const handleSaveCalculatedColumn = useCallback(() => { + // TODO validate expression, unique name + onCreateCalculatedColumn(column); + }, [column, onCreateCalculatedColumn]); + + const handleCancelEdit = useCallback(() => { + if (columnProp.name === "::") { + onCancelCreateColumn(); + } else { + if (columnRef.current !== undefined && columnRef.current !== column) { + setColumn(columnRef.current); + } + setEditMode(false); + } + }, [column, columnProp.name, onCancelCreateColumn]); return { availableRenderers, - editCalculatedColumn, + editCalculatedColumn: inEditMode, selectedCellRenderer: selectedCellRendererRef.current, column, navigateNextColumn, navigatePrevColumn, + onCancel: handleCancelEdit, onChange: handleChange, onChangeCalculatedColumnName: handleChangeCalculatedColumnName, onChangeFormatting: handleChangeFormatting, onChangeRenderer: handleChangeRenderer, + onEditCalculatedColumn: handleEditCalculatedcolumn, onInputCommit: handleInputCommit, onSave: handleSaveCalculatedColumn, }; diff --git a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx index 7a41e5a31e..8db16df6f7 100644 --- a/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/table-settings/TableSettingsPanel.tsx @@ -21,6 +21,7 @@ export interface TableSettingsProps extends HTMLAttributes { onAddCalculatedColumn: () => void; onConfigChange: (config: TableConfig) => void; onDataSourceConfigChange: (dataSOurceConfig: DataSourceConfig) => void; + onNavigateToColumn?: (columnName: string) => void; tableConfig: TableConfig; } @@ -34,6 +35,7 @@ export const TableSettingsPanel = ({ onAddCalculatedColumn, onConfigChange, onDataSourceConfigChange, + onNavigateToColumn, tableConfig: tableConfigProp, ...htmlAttributes }: TableSettingsProps) => { @@ -115,6 +117,7 @@ export const TableSettingsPanel = ({ columnItems={columnItems} onChange={onColumnChange} onMoveListItem={onMoveListItem} + onNavigateToColumn={onNavigateToColumn} />
diff --git a/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts b/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts index bf3efe11ed..444e7a5f2c 100644 --- a/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts +++ b/vuu-ui/packages/vuu-table-extras/src/useTableAndColumnSettings.ts @@ -54,6 +54,12 @@ export const useTableAndColumnSettings = ({ [dispatchLayoutAction] ); + const handleCancelCreateColumn = useCallback(() => { + requestAnimationFrame(() => { + showTableSettingsRef.current?.(); + }); + }, []); + const handleCreateCalculatedColumn = useCallback( (column: ColumnDescriptor) => { const newAvailableColumns = availableColumns.concat({ @@ -74,6 +80,7 @@ export const useTableAndColumnSettings = ({ (action: ColumnActionColumnSettings) => { showContextPanel("ColumnSettings", "Column Settings", { column: action.column, + onCancelCreateColumn: handleCancelCreateColumn, onConfigChange, onCreateCalculatedColumn: handleCreateCalculatedColumn, tableConfig, @@ -81,6 +88,7 @@ export const useTableAndColumnSettings = ({ } as ColumnSettingsProps); }, [ + handleCancelCreateColumn, handleCreateCalculatedColumn, onConfigChange, showContextPanel, @@ -99,6 +107,21 @@ export const useTableAndColumnSettings = ({ }); }, [showColumnSettingsPanel]); + const handleNavigateToColumn = useCallback( + (columnName: string) => { + const column = tableConfig.columns.find((c) => c.name === columnName); + if (column) { + showColumnSettingsPanel({ + type: "columnSettings", + column, + //TODO where do we get this from + vuuTable: { module: "SIMUL", table: "instruments" }, + }); + } + }, + [showColumnSettingsPanel, tableConfig.columns] + ); + showTableSettingsRef.current = useCallback(() => { showContextPanel("TableSettings", "DataGrid Settings", { availableColumns: @@ -110,11 +133,13 @@ export const useTableAndColumnSettings = ({ onAddCalculatedColumn: handleAddCalculatedColumn, onConfigChange, onDataSourceConfigChange, + onNavigateToColumn: handleNavigateToColumn, tableConfig, } as TableSettingsProps); }, [ availableColumns, handleAddCalculatedColumn, + handleNavigateToColumn, onConfigChange, onDataSourceConfigChange, showContextPanel, diff --git a/vuu-ui/packages/vuu-table-extras/test/ColumnExpressionTreeWalker.test.ts b/vuu-ui/packages/vuu-table-extras/test/ColumnExpressionTreeWalker.test.ts index 85d0cfb2b1..f35309dc66 100644 --- a/vuu-ui/packages/vuu-table-extras/test/ColumnExpressionTreeWalker.test.ts +++ b/vuu-ui/packages/vuu-table-extras/test/ColumnExpressionTreeWalker.test.ts @@ -386,6 +386,20 @@ describe("Column Expression treeWalker", () => { }); }); }); + + it.skip("parses and expressions with boolean conditions", () => { + const str = 'and(bid > 100, currency="EUR")'; + const result = parser.parse(str); + const expression = walkTree(result, str); + expect(expression).toEqual({ + type: "callExpression", + functionName: "and", + arguments: [ + { type: "relationalExpression" }, + { type: "relationalExpression" }, + ], + }); + }); }); // expect(evaluateExpression('if(side="Sell","N","Y")')).toEqual(Ok); diff --git a/vuu-ui/packages/vuu-table-extras/test/column-parser.test.ts b/vuu-ui/packages/vuu-table-extras/test/column-parser.test.ts index bfde35d356..58f118c2f6 100644 --- a/vuu-ui/packages/vuu-table-extras/test/column-parser.test.ts +++ b/vuu-ui/packages/vuu-table-extras/test/column-parser.test.ts @@ -74,6 +74,14 @@ describe("ColumnExpressionParser", () => { ).toEqual(Ok); }); + it("parses and function", () => { + expect(evaluateExpression('and(price > 100, ccy="EUR")')).toEqual(Ok); + }); + + it("parses or function", () => { + expect(evaluateExpression('or(price > 100, ccy="EUR")')).toEqual(Ok); + }); + it("parses conditional expressions", () => { expect(evaluateExpression("if(price > 100, true, false)")).toEqual(Ok); expect(evaluateExpression('if(side="Sell","N","Y")')).toEqual(Ok); 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 9efc9efd4d..4fccc0fb9d 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/Row.css +++ b/vuu-ui/packages/vuu-table/src/table-next/Row.css @@ -54,7 +54,7 @@ } .vuuTableNextRow-selectionDecorator { - background-color: var(--vuu-selection-decorator-bg, inherit); + background-color: var(--vuu-selection-decorator-bg, white); display: inline-block; position: relative; height: var(--row-height); diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.css b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.css index af1a15e5be..2d9140740b 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.css +++ b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.css @@ -3,11 +3,12 @@ --menu-item-icon-color: black; --vuu-icon-color: white; --vuu-icon-height: 12px; - --vuu-icon-width: 13px; + --vuu-icon-width: 12px; align-items: center; background: var(--salt-taggable-background-active); color: white; border-radius: 4px; + flex: var(--vuuColumnHeaderPill-flex, none); font-size: 11px; gap: 4px; height: 16px; diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css index a42cef5d21..3c4e712ac5 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css +++ b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.css @@ -13,9 +13,11 @@ border-radius: 4px; cursor: pointer; display: inline-block; + flex: 0 0 20px; padding: 2px; - left: var(--column-menu-left, 0); + /* left: var(--column-menu-left, 0); */ margin: var(--vuuTable-columnMenu-margin, 0); + width: auto; } .vuuTable-columnMenu:hover { diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-resizing/ColumnResizer.css b/vuu-ui/packages/vuu-table/src/table-next/column-resizing/ColumnResizer.css index 360052e7b4..8c3568df21 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-resizing/ColumnResizer.css +++ b/vuu-ui/packages/vuu-table/src/table-next/column-resizing/ColumnResizer.css @@ -3,12 +3,14 @@ cursor: col-resize; height: var(--header-height); margin-left: var(--columnResizer-left, auto); - position: relative; - width: 2px; + position: absolute; + right: -5px; + width: 8px; + z-index:1; } .vuuColumnResizerNext:hover { - --columnResizer-color: var(--salt-color-blue-500); + --columnResizer-color: var(--vuu-color-purple-10); } .vuuColumnResizerNext:after { @@ -17,7 +19,7 @@ content: ''; position: absolute; top: 0; - right: 0px; + right: 3px; height: var(--columnResizer-height, 0); width: 2px; } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css index 02c9bcd826..8522a0901e 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.css @@ -1,6 +1,7 @@ .vuuTableNextHeaderCell { --cell-align: 'flex-start'; - --vuuColumnHeaderPill-margin: 0 0 0 3px; + --vuuColumnHeaderPill-margin: 0; + --vuuColumnHeaderPill-flex: 0 0 24px; align-items: center; background-color: var(--vuuTableNextHeaderCell-background, inherit); @@ -11,7 +12,10 @@ box-sizing: border-box; cursor: default; display: inline-flex; + gap: 4px; height: var(--header-height); + padding: 0 12px 0 4px; + position: relative; vertical-align: top; } @@ -20,16 +24,20 @@ --vuuTable-columnMenu-margin: 0; --vuuColumnHeaderPill-margin: 0 3px 0 0; --column-menu-left: 2px; - justify-content: flex-end; + padding: 0 3px 0 12px; } - .vuuTableNextHeaderCell .vuuColumnResizerNext:hover { - --columnResizer-color: var(--vuu-color-purple-10); -} .vuuTableNextHeaderCell-label { + flex: 0 1 auto; line-height: calc(var(--header-height) - 1px); + overflow: hidden; + text-overflow: ellipsis; +} + +.vuuTableNextHeaderCell-right .vuuTableNextHeaderCell-label { + text-align: right; } .vuuTableNextHeaderCell-resizing { diff --git a/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts b/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts index 5b669e6c53..7b3e072544 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/moving-window.ts @@ -1,5 +1,5 @@ import { DataSourceRow } from "@finos/vuu-data-types"; -import { metadataKeys, WindowRange } from "@finos/vuu-utils"; +import { isRowSelectedLast, metadataKeys, WindowRange } from "@finos/vuu-utils"; import { VuuRange } from "@finos/vuu-protocol-types"; const { SELECTED } = metadataKeys; @@ -29,6 +29,21 @@ export class MovingWindow { if (this.isWithinRange(index)) { const internalIndex = index - this.range.from; this.data[internalIndex] = data; + + // Hack until we can deal with this more elegantly. When we have a block + // select operation, first row is selected (and updated via server), then + // remaining rows are selected when we select the block-end row. We get an + // update for all rows except first. Because we're extending the select status + // on the client, we have to adjust the first row selected (its still selected + // but is no longer the 'last selected row in block') + // Maybe answer is to apply ALL the selection status code here, not in Viewport + if (data[SELECTED]) { + const previousRow = this.data[internalIndex - 1]; + if (isRowSelectedLast(previousRow)) { + this.data[internalIndex - 1] = previousRow.slice() as DataSourceRow; + this.data[internalIndex - 1][SELECTED] -= 4; + } + } } } diff --git a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.css b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.css index 9b91a2a51f..e9dde62b6e 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.css +++ b/vuu-ui/packages/vuu-table/src/table-next/table-cell/TableCell.css @@ -5,7 +5,7 @@ display: inline-block; height: 100%; overflow:hidden; - padding: 0 8px; + padding: 0 11px 0 12px; text-overflow: ellipsis; vertical-align: top; } 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 cac32bd63a..91dbea8b6f 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 @@ -22,7 +22,7 @@ export const TableCell = ({ const handleDataItemEdited = useCallback( (value: VuuColumnDataType) => { - onDataEdited?.(row[IDX], name, value); + onDataEdited?.(row, name, value); // TODO will only return false in case of server rejection return true; }, 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 92fae53647..cb901d82ad 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useDataSource.ts @@ -95,6 +95,10 @@ export const useDataSource = ({ ] ); + const getSelectedRows = useCallback(() => { + return dataWindow.getSelectedRows(); + }, [dataWindow]); + useEffect( () => () => { isMounted.current = true; @@ -142,6 +146,7 @@ export const useDataSource = ({ return { data: data.current, + getSelectedRows, range: rangeRef.current, setRange, }; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts index 2f9746f2e5..339539b616 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts @@ -1,17 +1,24 @@ -import { buildColumnMap } from "@finos/vuu-utils"; +import { DataSource } from "@finos/vuu-data"; import { DataSourceRow } from "@finos/vuu-data-types"; import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { MouseEvent, useCallback } from "react"; import { useContextMenu as usePopupContextMenu } from "@finos/vuu-popups"; +import { buildColumnMap } from "@finos/vuu-utils"; +import { MouseEvent, useCallback } from "react"; export interface TableContextMenuHookProps { columns: KeyedColumnDescriptor[]; data: DataSourceRow[]; + dataSource: DataSource; + getSelectedRows: () => DataSourceRow[]; } +const NO_ROWS = [] as const; + export const useTableContextMenu = ({ columns, data, + dataSource, + getSelectedRows, }: TableContextMenuHookProps) => { const [showContextMenu] = usePopupContextMenu(); @@ -23,7 +30,7 @@ export const useTableContextMenu = ({ const cellEl = target?.closest("div[role='cell']"); const rowEl = target?.closest("div[role='row']"); if (cellEl && rowEl /*&& currentData && currentDataSource*/) { - // const { columns, selectedRowsCount } = currentDataSource; + const { selectedRowsCount } = dataSource; const columnMap = buildColumnMap(columns); const rowIndex = parseInt(rowEl.ariaRowIndex ?? "-1"); const cellIndex = Array.from(rowEl.childNodes).indexOf(cellEl); @@ -33,12 +40,12 @@ export const useTableContextMenu = ({ columnMap, columnName, row, - // selectedRows: selectedRowsCount === 0 ? NO_ROWS : getSelectedRows(), - // viewport: dataSource?.viewport, + selectedRows: selectedRowsCount === 0 ? NO_ROWS : getSelectedRows(), + viewport: dataSource.viewport, }); } }, - [columns, data, showContextMenu] + [columns, data, dataSource, showContextMenu] ); return onContextMenu; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts index c4dd66803b..4b963d86bd 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts @@ -11,6 +11,7 @@ import { applyGroupByToColumns, applySortToColumns, getCellRenderer, + getColumnLabel, getTableHeadings, getValueFormatter, hasValidationRules, @@ -195,10 +196,8 @@ export type ColumnActionDispatch = (action: GridModelAction) => void; const columnReducer: GridModelReducer = (state, action) => { info?.(`TableModelReducer ${action.type}`); - console.log(`TableModelReducer ${action.type}`); switch (action.type) { case "init": - console.log({ init: action }); return init(action); case "moveColumn": return moveColumn(state, action); @@ -252,9 +251,11 @@ function init({ tableConfig, }: InitialConfig): InternalTableModel { const { columns, ...tableAttributes } = tableConfig; + const keyedColumns = columns .filter(subscribedOnly(dataSourceConfig?.columns)) .map(columnDescriptorToKeyedColumDescriptor(tableAttributes)); + const maybePinnedColumns = keyedColumns.some(isPinned) ? sortPinnedColumns(keyedColumns) : keyedColumns; @@ -298,7 +299,7 @@ const columnDescriptorToKeyedColumDescriptor = align = getDefaultAlignment(column.serverDataType), key, name, - label = name, + label = getColumnLabel(column), width = columnDefaultWidth, ...rest } = column; 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 3400fca925..aa8266cc1c 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -99,7 +99,7 @@ export const useTable = ({ containerRef, dataSource, headerHeight = 25, - navigationStyle, + navigationStyle = "cell", onAvailableColumnsChange, onConfigChange, onFeatureEnabled, @@ -187,21 +187,22 @@ export const useTable = ({ // tableSchema, // }); } else { - console.log("usbscription message with no schema"); + console.log("subscription message with no schema"); } }, [] ); - const { data, range, setRange } = useDataSource({ - dataSource, - onFeatureEnabled, - onFeatureInvocation, - renderBufferSize, - onSizeChange: onDataRowcountChange, - onSubscribed, - range: initialRange, - }); + const { data, getSelectedRows, onEditTableData, range, setRange } = + useDataSource({ + dataSource, + onFeatureEnabled, + onFeatureInvocation, + renderBufferSize, + onSizeChange: onDataRowcountChange, + onSubscribed, + range: initialRange, + }); const handleConfigChanged = useCallback( (tableConfig: TableConfig) => { @@ -234,6 +235,9 @@ export const useTable = ({ tableConfig: newTableConfig, dataSourceConfig: dataSource.config, }); + console.log(`dispatch onConfigChange`, { + newTableConfig, + }); onConfigChange?.(newTableConfig); }, [dataSource, dispatchColumnAction, onConfigChange, tableConfig] @@ -429,7 +433,12 @@ export const useTable = ({ [navigationKeyDown, editingKeyDown] ); - const onContextMenu = useTableContextMenuNext({ columns, data }); + const onContextMenu = useTableContextMenuNext({ + columns, + data, + dataSource, + getSelectedRows, + }); const onHeaderClick = useCallback( (evt: MouseEvent) => { diff --git a/vuu-ui/packages/vuu-table/src/table/Table.tsx b/vuu-ui/packages/vuu-table/src/table/Table.tsx index 05cd0a99a5..49be16bda4 100644 --- a/vuu-ui/packages/vuu-table/src/table/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/table/Table.tsx @@ -15,6 +15,12 @@ import { isDataLoading } from "@finos/vuu-utils"; const classBase = "vuuTable"; +export interface TablePropsDeprecated + extends Omit { + height?: number; + width?: number; +} + export const Table = ({ allowConfigEditing: showSettings = false, className: classNameProp, @@ -35,7 +41,7 @@ export const Table = ({ style: styleProp, width, ...htmlAttributes -}: TableProps) => { +}: TablePropsDeprecated) => { const id = useIdMemo(idProp); const { containerMeasurements: { containerRef, innerSize, outerSize }, diff --git a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts index a196e163f8..ec8c2be168 100644 --- a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts +++ b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts @@ -13,7 +13,8 @@ import { TableSelectionModel, } from "@finos/vuu-datagrid-types"; import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { FC, HTMLAttributes, MouseEvent } from "react"; +import { MeasuredContainerProps } from "packages/vuu-layout/src"; +import { FC, MouseEvent } from "react"; import { RowProps } from "../table-next/Row"; export type TableRowClickHandler = (row: VuuDataRow) => void; @@ -22,8 +23,7 @@ export type TableRowSelectHandler = (row: DataSourceRow) => void; export type TableNavigationStyle = "none" | "cell" | "row"; -export interface TableProps - extends Omit, "onSelect"> { +export interface TableProps extends Omit { Row?: FC; allowConfigEditing?: boolean; /** @@ -33,7 +33,6 @@ export interface TableProps config: TableConfig; dataSource: DataSource; headerHeight?: number; - height?: number; /** * Defined how focus navigation within data cells will be handled by table. * Default is cell. @@ -85,7 +84,6 @@ export interface TableProps * composite component. */ showColumnHeaders?: boolean; - width?: number; } export type TableColumnResizeHandler = ( diff --git a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts b/vuu-ui/packages/vuu-table/src/table/useDataSource.ts index 11bd8a041b..d018100e32 100644 --- a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/table/useDataSource.ts @@ -13,7 +13,13 @@ import { isVuuFeatureInvocation, } from "@finos/vuu-data-react"; import { VuuRange, VuuSortCol } from "@finos/vuu-protocol-types"; -import { getFullRange, metadataKeys, WindowRange } from "@finos/vuu-utils"; +import { + getFullRange, + isRowSelectedLast, + metadataKeys, + RowSelected, + WindowRange, +} from "@finos/vuu-utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; const { SELECTED } = metadataKeys; @@ -224,6 +230,11 @@ export class MovingWindow { this.data[internalIndex - 1][SELECTED] = 2; } else if (preSelected === 2 && !isSelected) { this.data[internalIndex - 1][SELECTED] = 0; + } else if ( + isRowSelectedLast(this.data[internalIndex - 1]) && + isSelected + ) { + this.data[internalIndex - 1][SELECTED] -= 4; } } } diff --git a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts index bbd5cea1ee..a5ad0c3281 100644 --- a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts @@ -24,6 +24,8 @@ import { stripFilterFromColumns, moveItemDeprecated, getDefaultAlignment, + isCalculatedColumn, + getCalculatedColumnName, } from "@finos/vuu-utils"; import { Reducer, useReducer } from "react"; @@ -236,6 +238,14 @@ function init({ dataSourceConfig, tableConfig }: InitialConfig): TableModel { } } +const labelFromName = (column: ColumnDescriptor) => { + if (isCalculatedColumn(column.name)) { + return getCalculatedColumnName(column); + } else { + return column.name; + } +}; + const getLabel = ( label: string, columnFormatHeader?: "uppercase" | "capitalize" @@ -264,7 +274,7 @@ const toKeyedColumWithDefaults = align = getDefaultAlignment(serverDataType), key, name, - label = name, + label = labelFromName(column), width = columnDefaultWidth, ...rest } = column; diff --git a/vuu-ui/packages/vuu-theme/css/foundations/color.css b/vuu-ui/packages/vuu-theme/css/foundations/color.css index 694551e002..ea52e17e84 100644 --- a/vuu-ui/packages/vuu-theme/css/foundations/color.css +++ b/vuu-ui/packages/vuu-theme/css/foundations/color.css @@ -17,6 +17,7 @@ --vuu-color-pink-10-fade-20: rgba(234, 120, 128, .2); /* #F37880 */ + --vuu-color-gray-03: rgb(237, 237, 237); /* #EDEDED */ --vuu-color-gray-05: rgb(222, 222, 222); /* #DEDEDE */ --vuu-color-gray-10: rgb(228, 227, 231); /* #E4E3E7 */ --vuu-color-gray-20: rgb(245, 242, 248); /* #F5F2F8 */ diff --git a/vuu-ui/packages/vuu-theme/css/foundations/typography.css b/vuu-ui/packages/vuu-theme/css/foundations/typography.css index 1c1b7430dc..251af886b3 100644 --- a/vuu-ui/packages/vuu-theme/css/foundations/typography.css +++ b/vuu-ui/packages/vuu-theme/css/foundations/typography.css @@ -1,6 +1,6 @@ .vuu-theme { --salt-typography-fontFamily: "Nunito Sans"; - --salt-typography-fontFamily-code: "PT Mono"; + --salt-typography-fontFamily-code: 'Sometype Mono'; --salt-typography-fontWeight-light: 300; --salt-typography-fontWeight-regular: 400; diff --git a/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css index 0cd0a402d4..5ede2aac80 100644 --- a/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css +++ b/vuu-ui/packages/vuu-theme/fonts/NunitoSans.css @@ -5,7 +5,7 @@ font-weight: 300; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @font-face { @@ -14,7 +14,7 @@ font-weight: 400; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @font-face { @@ -23,7 +23,7 @@ font-weight: 500; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @@ -33,7 +33,7 @@ font-weight: 600; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @@ -43,7 +43,7 @@ font-weight: 700; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* latin */ @@ -53,6 +53,6 @@ font-weight: 800; font-stretch: 100%; font-display: swap; - src: url(./NunitoSansv15.woff2) format('woff2'); + src: url(./NunitoSansv15Latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-theme/fonts/NunitoSansv15Latin.woff2 b/vuu-ui/packages/vuu-theme/fonts/NunitoSansv15Latin.woff2 new file mode 100644 index 0000000000..3a597c3dca Binary files /dev/null and b/vuu-ui/packages/vuu-theme/fonts/NunitoSansv15Latin.woff2 differ diff --git a/vuu-ui/packages/vuu-theme/fonts/SomeTypeMono.css b/vuu-ui/packages/vuu-theme/fonts/SomeTypeMono.css new file mode 100644 index 0000000000..5575785c3a --- /dev/null +++ b/vuu-ui/packages/vuu-theme/fonts/SomeTypeMono.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'Sometype Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./SomeTypeMonov1-500.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-theme/fonts/SomeTypeMonov1-500.woff2 b/vuu-ui/packages/vuu-theme/fonts/SomeTypeMonov1-500.woff2 new file mode 100644 index 0000000000..6e4afe8520 Binary files /dev/null and b/vuu-ui/packages/vuu-theme/fonts/SomeTypeMonov1-500.woff2 differ diff --git a/vuu-ui/packages/vuu-theme/index.css b/vuu-ui/packages/vuu-theme/index.css index 9222247cd5..3a0b68bd5c 100644 --- a/vuu-ui/packages/vuu-theme/index.css +++ b/vuu-ui/packages/vuu-theme/index.css @@ -1,4 +1,5 @@ @import url(fonts/NunitoSans.css); +@import url(fonts/SomeTypeMono.css); @import url(css/global.css); @import url(css/theme.css); @import url(css/components/components.css); 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 44e5d72954..59bfae0dfa 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 @@ -1,4 +1,4 @@ -import { RefObject, SyntheticEvent } from "react"; +import { MouseEventHandler, RefObject, SyntheticEvent } from "react"; export type SelectionDisallowed = "none"; export type SingleSelectionStrategy = "default" | "deselectable"; @@ -79,6 +79,7 @@ export interface SelectionHookProps extends SelectionProps { highlightedIdx: number; itemQuery: string; label?: string; + onClick?: MouseEventHandler; selectionKeys?: string[]; tabToSelect?: boolean; } 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 4f30d9c404..0678b89420 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 @@ -41,6 +41,7 @@ export const useSelection = ({ // groupSelection = GROUP_SELECTION_NONE, highlightedIdx, itemQuery, + onClick, // label, onSelect, onSelectionChange, @@ -239,18 +240,22 @@ export const useSelection = ({ lastActive.current = highlightedIdx; } } + onClick?.(evt); }, [ containerRef, highlightedIdx, disableSelection, + onClick, selectItemAtIndex, isExtendedSelect, ] ); const listHandlers = selectionIsDisallowed(selectionStrategy) - ? NO_SELECTION_HANDLERS + ? { + onClick, + } : { onClick: handleClick, onKeyDown: handleKeyDown, diff --git a/vuu-ui/packages/vuu-ui-controls/src/dropdown/DropdownBase.tsx b/vuu-ui/packages/vuu-ui-controls/src/dropdown/DropdownBase.tsx index e199f3d370..3e932f2afd 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/dropdown/DropdownBase.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/dropdown/DropdownBase.tsx @@ -110,7 +110,7 @@ export const DropdownBase = forwardRef( className: cx(className, `${classBase}-popup-component`), id, ref: popupComponentRef, - width: placement.endsWith("full-width") ? "100%" : ownWidth ?? width, + width: placement.endsWith("full-width") ? "auto" : ownWidth ?? width, }); }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdownBase.ts b/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdownBase.ts index 00b96bb53d..926e73174d 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdownBase.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/dropdown/useDropdownBase.ts @@ -118,15 +118,10 @@ export const useDropdownBase = ({ const handleBlur = useCallback( (evt: FocusEvent) => { - console.log("useDropdownBase blur", { - popperRef: popperRef.current, - relatedTarget: evt.relatedTarget, - }); if (isOpen) { if (popperRef.current?.contains(evt.relatedTarget)) { // ignore } else { - console.log("hide dropdown"); hideDropdown("blur"); } } diff --git a/vuu-ui/packages/vuu-ui-controls/src/editable-label/EditableLabel.css b/vuu-ui/packages/vuu-ui-controls/src/editable-label/EditableLabel.css index f0903f269f..7432423143 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/editable-label/EditableLabel.css +++ b/vuu-ui/packages/vuu-ui-controls/src/editable-label/EditableLabel.css @@ -12,7 +12,7 @@ font-size: var(--salt-text-fontSize); height: var(--editableLabel-height); justify-content: center; - max-width: 170px; + /* max-width: 170px; */ outline: none; overflow: hidden; padding: 0 var(--editableLabel-padding); 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 6d0562db1d..e87ce9fa35 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 @@ -5,7 +5,7 @@ import { TableNext, TableProps, TableRowSelectHandler } from "@finos/vuu-table"; import { ColumnMap } from "@finos/vuu-utils"; import { Input } from "@salt-ds/core"; import { ForwardedRef, forwardRef, HTMLAttributes, useMemo } from "react"; -import { DropdownBase } from "../dropdown"; +import { DropdownBase, OpenChangeHandler } from "../dropdown"; import "./SearchCell"; import { useInstrumentPicker } from "./useInstrumentPicker"; @@ -26,6 +26,8 @@ export interface InstrumentPickerProps * @returns string */ itemToString?: (row: DataSourceRow) => string; + onClose?: () => void; + onOpenChange?: OpenChangeHandler; onSelect: TableRowSelectHandler; schema: TableSchema; searchColumns: string[]; @@ -40,6 +42,7 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( disabled, id: idProp, itemToString, + onOpenChange: onOpenChangeProp, onSelect, schema, searchColumns, @@ -62,14 +65,21 @@ export const InstrumentPicker = forwardRef(function InstrumentPicker( columns: TableProps.config.columns, dataSource, itemToString, + onOpenChange: onOpenChangeProp, onSelect, searchColumns, }); - console.log({ value }); - const endAdornment = useMemo(() => , []); + const tableProps = { + ...TableProps, + config: { + ...TableProps.config, + showHighlightedRow: true, + }, + }; + return ( 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 ae13cc7224..1fea1733c5 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 @@ -5,12 +5,13 @@ import { TableRowSelectHandler } from "@finos/vuu-table"; import { ColumnMap } from "@finos/vuu-utils"; import { ChangeEvent, useCallback, useMemo, useState } from "react"; import { useControlled } from "../common-hooks"; +import { OpenChangeHandler } from "../dropdown"; import { InstrumentPickerProps } from "./InstrumentPicker"; export interface InstrumentPickerHookProps extends Pick< InstrumentPickerProps, - "columnMap" | "itemToString" | "onSelect" | "searchColumns" + "columnMap" | "itemToString" | "onOpenChange" | "onSelect" | "searchColumns" > { columns: ColumnDescriptor[]; dataSource: DataSource; @@ -31,6 +32,7 @@ export const useInstrumentPicker = ({ defaultIsOpen, isOpen: isOpenProp, itemToString = defaultItemToString(columns, columnMap), + onOpenChange: onOpenChangeProp, onSelect, searchColumns, }: InstrumentPickerHookProps) => { @@ -41,18 +43,18 @@ export const useInstrumentPicker = ({ name: "useDropdownList", }); - console.log({ dataSource }); const baseFilterPattern = useMemo( // TODO make this contains once server supports it () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), [searchColumns] ); - const handleOpenChange = useCallback( - (open) => { + const handleOpenChange = useCallback( + (open, closeReason) => { setIsOpen(open); + onOpenChangeProp?.(open, closeReason); }, - [setIsOpen] + [onOpenChangeProp, setIsOpen] ); const handleInputChange = useCallback( @@ -82,8 +84,9 @@ export const useInstrumentPicker = ({ setValue(value); setIsOpen(false); onSelect(row); + onOpenChangeProp?.(false, "select"); }, - [itemToString, onSelect, setIsOpen] + [itemToString, onOpenChangeProp, onSelect, setIsOpen] ); const inputProps = { diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx index 8e03994ced..18e56040e0 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx @@ -69,6 +69,7 @@ export const List = forwardRef(function List< maxWidth, minHeight, minWidth, + onClick: onClickProp, onDragStart, onDrop, onMoveListItem, @@ -164,6 +165,7 @@ export const List = forwardRef(function List< id, label: "List", listHandlers: listHandlersProp, // should this be in context ? + onClick: onClickProp, onDragStart, onDrop, onMoveListItem, diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts index 3a452f19ef..dff81b3070 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts @@ -242,6 +242,7 @@ export interface ListHookProps< | "collapsibleHeaders" | "disabled" | "id" + | "onClick" | "onDragStart" | "onDrop" | "onHighlight" 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 096153c8d4..e396d668e3 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -46,6 +46,7 @@ export const useList = ({ id, label = "", listHandlers: listHandlersProp, + onClick: onClickProp, onDragStart, onDrop, onHighlight, @@ -166,6 +167,7 @@ export const useList = ({ highlightedIdx: highlightedIndex, itemQuery: ".vuuListItem", label: `${label}:useList`, + onClick: onClickProp, onSelect: handleSelect, onSelectionChange: handleSelectionChange, selected, diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx index e0c1279c29..0eb457070d 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx @@ -122,7 +122,7 @@ export const Tab = forwardRef(function Tab( {...props} aria-controls={ariaControls} aria-selected={selected} - className={cx(classBase, { + className={cx(classBase, className, { [`${classBase}-closeable`]: closeable, "vuuDraggable-dragAway": dragging, [`${classBase}-editing`]: editing, diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabsTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabsTypes.ts index c8fb3d70bd..e75eee04c2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabsTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabsTypes.ts @@ -111,6 +111,10 @@ export interface TabstripProps extends HTMLAttributes { * not closeable, not renameable and has no tab-location , otherwise true. */ showTabMenuButton?: boolean; + /** + * An optional classname that will be added to each tab + */ + tabClassName?: string; } export type exitEditHandler = ( diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css index da1b648bb4..e0a111663b 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.css @@ -107,8 +107,10 @@ } .vuuDraggable-tabstrip-horizontal { + --item-height: var(--tabstrip-height); --tab-thumb-height: 2px; --tab-thumb-left: 0px; + --tabstrip-display: inline-flex; --tabstrip-height: 28px; line-height: var(--tabstrip-height); } diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx index b27872665f..bf7ee27c12 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tabstrip.tsx @@ -29,6 +29,7 @@ export const Tabstrip = ({ orientation = "horizontal", showTabMenuButton, style: styleProp, + tabClassName, ...htmlAttributes }: TabstripProps) => { const rootRef = useRef(null); @@ -53,7 +54,6 @@ export const Tabstrip = ({ onMoveTab, orientation, }); - const id = useId(idProp); const className = cx(classBase, `${classBase}-${orientation}`, classNameProp); const style = @@ -70,6 +70,7 @@ export const Tabstrip = ({ .map((child, index) => { const { id: tabId = `${id}-tab-${index}`, + className, closeable = allowCloseTab, editable = allowRenameTab, location: tabLocation, @@ -79,6 +80,7 @@ export const Tabstrip = ({ return React.cloneElement(child, { ...tabProps, ...tabstripHook.navigationProps, + className: cx(className, tabClassName), closeable, "data-overflow-priority": selected ? "1" : undefined, dragging: draggedItemIndex === index, @@ -111,19 +113,20 @@ export const Tabstrip = ({ ) ), [ - activeTabIndex, + children, allowAddTab, + tabstripHook.navigationProps, + onClickAddTab, + id, allowCloseTab, allowRenameTab, - children, - focusVisible, - id, - location, - onClickAddTab, showTabMenuButton, + activeTabIndex, tabProps, + tabClassName, draggedItemIndex, - tabstripHook.navigationProps, + focusVisible, + location, ] ); 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 e3d069ee57..fdf95e3d3f 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 @@ -35,7 +35,6 @@ export const VuuInput = ({ }: VuuInputProps) => { const commitValue = useCallback>( (evt, value) => { - console.log(`commit value ${value}`); if (type === "number") { const numericValue = parseFloat(value); if (isValidNumber(numericValue)) { diff --git a/vuu-ui/packages/vuu-utils/src/array-utils.ts b/vuu-ui/packages/vuu-utils/src/array-utils.ts index a58182eed2..bf083c5280 100644 --- a/vuu-ui/packages/vuu-utils/src/array-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/array-utils.ts @@ -97,3 +97,21 @@ export const moveItem = ( } } }; + +export const getAddedItems = (values: undefined | T[], newValues: T[]) => { + const isNew = (v: T) => !values?.includes(v); + if (values === undefined) { + return newValues; + } else if (newValues.some(isNew)) { + return newValues.filter(isNew); + } else { + return [] as T[]; + } +}; + +export const getMissingItems = ( + sourceItems: T[], + items: I[], + identity: (s: T) => I +) => + items.filter((i) => sourceItems.findIndex((s) => identity(s) === i) === -1); diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index f00bc3f58e..3bb469015e 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -29,6 +29,10 @@ import type { CSSProperties } from "react"; import type { CellRendererDescriptor } from "./component-registry"; import { isFilterClause, isMultiClauseFilter } from "./filter-utils"; +/** + * ColumnMap provides a lookup of the index position of a data item within a row + * by column name. + */ export interface ColumnMap { [columnName: string]: number; } @@ -583,6 +587,16 @@ export const getColumnName = (name: string) => { } }; +export const getColumnLabel = (column: ColumnDescriptor) => { + if (column.label) { + return column.label; + } else if (isCalculatedColumn(column.name)) { + return getCalculatedColumnName(column); + } else { + return column.name; + } +}; + export const findColumn = ( columns: KeyedColumnDescriptor[], columnName: string @@ -630,10 +644,7 @@ export function updateColumn( } } -export const toDataSourceColumns = (column: ColumnDescriptor) => - column.expression - ? `${column.name}:${column.serverDataType}:${column.expression}` - : column.name; +export const toDataSourceColumns = (column: ColumnDescriptor) => column.name; export const getRowRecord = ( row: DataSourceRow, @@ -877,7 +888,7 @@ export const isCalculatedColumn = (columnName?: string) => export const getCalculatedColumnDetails = (column: ColumnDescriptor) => { if (isCalculatedColumn(column.name)) { - return column.name.split(":"); + return column.name.split(/:=?/); } else { throw Error( `column-utils, getCalculatedColumnDetails column name ${column.name} is not valid calculated column` @@ -887,41 +898,41 @@ export const getCalculatedColumnDetails = (column: ColumnDescriptor) => { export const getCalculatedColumnName = (column: ColumnDescriptor) => getCalculatedColumnDetails(column)[0]; -export const getCalculatedColumnExpression = (column: ColumnDescriptor) => - getCalculatedColumnDetails(column)[1]; export const getCalculatedColumnType = (column: ColumnDescriptor) => - getCalculatedColumnDetails(column)[2] as VuuColumnDataType; + getCalculatedColumnDetails(column)[1] as VuuColumnDataType; +export const getCalculatedColumnExpression = (column: ColumnDescriptor) => + getCalculatedColumnDetails(column)[2]; export const setCalculatedColumnName = ( column: ColumnDescriptor, name: string ): ColumnDescriptor => { - const [, expression, type] = column.name.split(":"); + const [, type, expression] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:${expression}`, }; }; -// TODO should we validate the expression here ? -export const setCalculatedColumnExpression = ( +export const setCalculatedColumnType = ( column: ColumnDescriptor, - expression: string + type: string ): ColumnDescriptor => { - const [name, , type] = column.name.split(":"); + const [name, , expression] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:${expression}`, }; }; -export const setCalculatedColumnType = ( +// TODO should we validate the expression here ? +export const setCalculatedColumnExpression = ( column: ColumnDescriptor, - type: string + expression: string ): ColumnDescriptor => { - const [name, expression] = column.name.split(":"); + const [name, type] = column.name.split(":"); return { ...column, - name: `${name}:${expression}:${type}`, + name: `${name}:${type}:=${expression}`, }; }; diff --git a/vuu-ui/packages/vuu-utils/src/event-emitter.ts b/vuu-ui/packages/vuu-utils/src/event-emitter.ts index 25b8de9fa6..1cd4b6b3cd 100644 --- a/vuu-ui/packages/vuu-utils/src/event-emitter.ts +++ b/vuu-ui/packages/vuu-utils/src/event-emitter.ts @@ -89,6 +89,15 @@ export class EventEmitter { this.addListener(event, listener); } + hasListener(event: E, listener: Events[E]) { + const listeners = this.#events.get(event); + if (Array.isArray(listeners)) { + return listeners.includes(listener); + } else { + return listeners === listener; + } + } + private invokeHandler(handler: Listener | Array, args: unknown[]) { if (isArrayOfListeners(handler)) { handler.slice().forEach((listener) => this.invokeHandler(listener, args)); diff --git a/vuu-ui/packages/vuu-utils/src/filter-utils.ts b/vuu-ui/packages/vuu-utils/src/filter-utils.ts index 0213e57f56..a62ba334f2 100644 --- a/vuu-ui/packages/vuu-utils/src/filter-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/filter-utils.ts @@ -67,11 +67,14 @@ export function isMultiClauseFilter( const filterValue = (value: string | number | boolean) => typeof value === "string" ? `"${value}"` : value; +const quotedStrings = (value: string | number | boolean) => + typeof value === "string" ? `"${value}"` : value; + export const filterAsQuery = (f: Filter): string => { if (isMultiClauseFilter(f)) { return f.filters.map((filter) => filterAsQuery(filter)).join(` ${f.op} `); } else if (isMultiValueFilter(f)) { - return `${f.column} ${f.op} [${f.values.join(",")}]`; + return `${f.column} ${f.op} [${f.values.map(quotedStrings).join(",")}]`; } else { return `${f.column} ${f.op} ${filterValue(f.value)}`; } diff --git a/vuu-ui/packages/vuu-utils/src/selection-utils.ts b/vuu-ui/packages/vuu-utils/src/selection-utils.ts index 0d78efa25b..0daabf20c1 100644 --- a/vuu-ui/packages/vuu-utils/src/selection-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/selection-utils.ts @@ -20,6 +20,8 @@ export const RowSelected = { export const isRowSelected = (row: DataSourceRow): boolean => (row[SELECTED] & RowSelected.True) === RowSelected.True; +export const isRowSelectedLast = (row?: DataSourceRow): boolean => + row !== undefined && (row[SELECTED] & RowSelected.Last) === RowSelected.Last; const inAscendingOrder = (item1: SelectionItem, item2: SelectionItem) => { const n1: number = typeof item1 === "number" ? item1 : item1[0]; diff --git a/vuu-ui/packages/vuu-utils/test/filter-utils.test.ts b/vuu-ui/packages/vuu-utils/test/filter-utils.test.ts index fba68a4ed5..0f2f8068c0 100644 --- a/vuu-ui/packages/vuu-utils/test/filter-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/filter-utils.test.ts @@ -12,6 +12,15 @@ describe("filter-utils", () => { }) ).toEqual('currency = "EUR"'); }); + it("stringifies multi value filter clauses, string values", () => { + expect( + filterAsQuery({ + column: "currency", + op: "in", + values: ["EUR", "GBP"], + }) + ).toEqual('currency in ["EUR","GBP"]'); + }); it("stringifies simple filter clauses, numeric values", () => { expect( filterAsQuery({ @@ -21,6 +30,15 @@ describe("filter-utils", () => { }) ).toEqual("price > 1000"); }); + it("stringifies multi value filter clauses, numeric values", () => { + expect( + filterAsQuery({ + column: "price", + op: "in", + values: [1000, 2000, 3000], + }) + ).toEqual("price in [1000,2000,3000]"); + }); it("stringifies simple filter clauses, boolean values", () => { expect( filterAsQuery({ diff --git a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts index f845b0fd27..69344abd81 100644 --- a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts @@ -123,4 +123,77 @@ describe("jsonToDataSourceRows", () => { [5, 5, true, false, 1, 0, "$root|test6|test6.2", 0, "", "test6.2", "test 6.2 value"], ]]); }); + + it("parses a 2 level structure, mixed simple attributes and array (simple values)", () => { + // prettier-ignore + + expect( + jsonToDataSourceRows({ + test1: "value 1", + test2: 12345, + test3: 100.01, + test4: true, + test5: ["test5.1", "test5.2", "test5.3"], + }) + ).toEqual([ + [ + { name: "col 1", type: { name: "json", renderer: { name: "json" } } }, + { name: "col 2", type: { name: "json", renderer: { name: "json" } } }, + { + name: "col 3", + hidden: true, + type: { name: "json", renderer: { name: "json" } }, + }, + ], + [ + [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], + [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], + [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], + [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], + [5, 5, true, false, 1, 0, "$root|test5|0", 0, "", "0", "test5.1"], + [6, 6, true, false, 1, 0, "$root|test5|1", 0, "", "1", "test5.2"], + [7, 7, true, false, 1, 0, "$root|test5|2", 0, "", "2", "test5.3"], + ], + ]); + }); + + it("parses a 2 level structure, mixed simple attributes and array (json values)", () => { + // prettier-ignore + expect( + jsonToDataSourceRows({ + test1: "value 1", + test2: 12345, + test3: 100.01, + test4: true, + test5: [ + {"test5.1": "test 5.1 value"}, + {"test5.2": "test 5.2 value"}, + {"test5.3": "test 5.2 value"} + ], + + + }) + // prettier-ignore + ).toEqual([[ + {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, + {name: "col 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} + ], + [ + [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], + [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], + [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], + [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], + [5, 5, false, false, 1, 1, '$root|test5|0', 0, '', '0+', '' ], + [6, 6, true, false, 2, 0, '$root|test5|0|test5.1', 0, '', '', 'test5.1', 'test 5.1 value' ], + [7, 7, false, false, 1, 1, '$root|test5|1', 0, '', '1+', '' ], + [8,8, true, false, 2, 0, '$root|test5|1|test5.2', 0, '', '', 'test5.2','test 5.2 value'], + [9, 9, false, false, 1, 1, '$root|test5|2', 0, '', '2+', '' ], + [10,10, true,false, 2, 0, '$root|test5|2|test5.3', 0, '', '', 'test5.3', 'test 5.2 value'] + ] + ]); + }); }); diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css index 13aab89882..52c514dcdd 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.css @@ -53,7 +53,7 @@ body { align-items: center; display: flex; gap: 12px; - height: 36px; + height: var(--vuuToolbarProxy-height, 36px); } .vuuToolbarProxy > [data-align="end"]{ @@ -111,8 +111,8 @@ body { z-index: 1; } - .vuuShell-mainTabs > .vuuTabstrip .vuuTab { - background-color: var(--vuu-color-gray-28); + .vuuTab.MainTab { + background-color: #F1F2F4; border-color: #D6D7DA; border-radius: 6px 6px 0 0; border-width: 1px; @@ -120,13 +120,14 @@ body { position: relative; } - .vuuShell-mainTabs > .vuuTabstrip .vuuTab-selected { + .MainTab.vuuTab-selected { background-color: white; border-bottom-color: white; z-index: 1; + } - .vuuShell-mainTabs > .vuuTabstrip .vuuTab-selected:before{ + .MainTab.vuuTab-selected:before{ background-color: #6d188b;; content: ''; position: absolute; @@ -137,7 +138,7 @@ body { width: 6px; } - .vuuShell-mainTabs > .vuuTabstrip .vuuTab:hover:not(.vuuTab-selected):before{ + .MainTab.vuuTab:hover:not(.vuuTab-selected):before{ background-color: #F37880; content: ''; position: absolute; @@ -148,7 +149,7 @@ body { width: 6px; } - .vuuShell-mainTabs > .vuuTabstrip .vuuTab-main { + .vuuTab.MainTab .vuuTab-main { background-color: transparent; font-weight: 700; height: 29px; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx index 061618e8fb..332e584be8 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/src/App.tsx @@ -1,4 +1,4 @@ -import { ContextMenuProvider, Dialog } from "@finos/vuu-popups"; +import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; import { LeftNav, Shell, @@ -6,7 +6,6 @@ import { ShellProps, VuuUser, } from "@finos/vuu-shell"; -import { ReactElement, useCallback, useRef, useState } from "react"; import { getDefaultColumnConfig } from "./columnMetaData"; import { createPlaceholder } from "./createPlaceholder"; import { useFeatures } from "./useFeatures"; @@ -20,6 +19,7 @@ import { } from "@finos/vuu-layout"; import "./App.css"; +import { useRpcResponseHandler } from "./useRpcResponseHandler"; registerComponent("ColumnSettings", ColumnSettingsPanel, "view"); registerComponent("TableSettings", TableSettingsPanel, "view"); @@ -40,26 +40,14 @@ const { await vuuConfig; export const App = ({ user }: { user: VuuUser }) => { - const dialogTitleRef = useRef(""); - const [dialogContent, setDialogContent] = useState(); - const [features, tableFeatures] = useFeatures({ features: configuredFeatures, }); - console.log({ features, tableFeatures }); - - const { - buildMenuOptions, - dialogContent: saveLayoutDialog, - handleCloseDialog, - handleMenuAction, - } = useLayoutContextMenuItems(); - - const handleClose = useCallback(() => { - setDialogContent(undefined); - handleCloseDialog?.(); - }, [handleCloseDialog]); + const { dialog, setDialogState } = useDialog(); + const { handleRpcResponse } = useRpcResponseHandler(setDialogState); + const { buildMenuOptions, handleMenuAction } = + useLayoutContextMenuItems(setDialogState); // TODO get Context from Shell return ( @@ -67,7 +55,9 @@ export const App = ({ user }: { user: VuuUser }) => { menuActionHandler={handleMenuAction} menuBuilder={buildMenuOptions} > - + { serverUrl={serverUrl} user={user} > - - {dialogContent ?? saveLayoutDialog} - + {dialog} diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/defaultLayout.ts b/vuu-ui/sample-apps/app-vuu-basket-trader/src/defaultLayout.ts deleted file mode 100644 index cea78c9253..0000000000 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/defaultLayout.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { LayoutJSON } from "@finos/vuu-layout"; - -export const defaultLayout: LayoutJSON = { - id: "main-tabs", - type: "Stack", - props: { - className: "vuuShell-mainTabs", - TabstripProps: { - allowAddTab: true, - allowRenameTab: true, - animateSelectionThumb: false, - className: "vuuShellMainTabstrip", - location: "main-tab", - }, - preserve: true, - active: 0, - }, - children: [ - { - type: "Placeholder", - title: "Page 1", - }, - ], -}; diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts b/vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts index 114b378cd2..81fe3da34a 100644 --- a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/src/useFeatures.ts @@ -1,6 +1,12 @@ import { useVuuTables } from "@finos/vuu-data-react"; -import { FeatureProps, Features } from "@finos/vuu-shell"; +import { + FeatureProps, + Features, + isTableSchema, + isWildcardSchema, +} from "@finos/vuu-shell"; import { wordify } from "@finos/vuu-utils"; +import { TableSchema } from "@finos/vuu-data"; import { useMemo } from "react"; export interface FeaturesHookProps { @@ -17,13 +23,14 @@ export const useFeatures = ({ const features: FeatureProps[] = []; const tableFeatures: FeatureProps[] = []; for (const { - featureProps, + featureProps = {}, leftNavLocation = "vuu-tables", ...feature } of Object.values(featuresProp)) { + const { schema, schemas } = featureProps; const target = leftNavLocation === "vuu-tables" ? tableFeatures : features; - if (featureProps?.schema === "*" && tables) { + if (isWildcardSchema(schema) && tables) { for (const tableSchema of tables.values()) { target.push({ ...feature, @@ -35,15 +42,28 @@ export const useFeatures = ({ )}`, }); } - } else if (featureProps?.schema && tables) { + } else if (isTableSchema(schema) && tables) { //TODO set the - const tableSchema = tables.get(featureProps?.schema); + const tableSchema = tables.get(schema.table); target.push({ ...feature, ComponentProps: { tableSchema, }, }); + } else if (Array.isArray(schemas)) { + target.push({ + ...feature, + ComponentProps: schemas.reduce>( + (map, schema) => { + map[`${schema.table}Schema`] = tables?.get( + schema.table + ) as TableSchema; + return map; + }, + {} + ), + }); } else { target.push(feature); } diff --git a/vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx b/vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx new file mode 100644 index 0000000000..f21679dcd9 --- /dev/null +++ b/vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx @@ -0,0 +1,73 @@ +import { RpcResponseHandler, useVuuTables } from "@finos/vuu-data-react"; +import { hasAction, MenuRpcResponse, TableSchema } from "@finos/vuu-data"; +import { useCallback } from "react"; +import { getFormConfig } from "./session-editing"; +import { Feature, SessionEditingForm } from "packages/vuu-shell/src"; +import { VuuTable } from "@finos/vuu-protocol-types"; +import { SetDialog } from "@finos/vuu-popups"; + +const withTable = (action: unknown): action is { table: VuuTable } => + action !== null && typeof action === "object" && "table" in action; + +const vuuFilterTableFeatureUrl = "./feature-filter-table/index.js"; + +export const useRpcResponseHandler = (setDialogState: SetDialog) => { + const tables = useVuuTables(); + + const handleClose = useCallback(() => { + setDialogState(undefined); + }, [setDialogState]); + + const handleRpcResponse = useCallback( + (response) => { + if ( + hasAction(response) && + typeof response.action === "object" && + response.action !== null && + "type" in response.action && + response.action?.type === "OPEN_DIALOG_ACTION" + ) { + const { tableSchema } = response.action as unknown as { + tableSchema: TableSchema; + }; + if (tableSchema) { + const formConfig = getFormConfig(response as MenuRpcResponse); + // dialogTitleRef.current = formConfig.config.title; + setDialogState({ + content: ( + + ), + title: "Set Parameters", + }); + } else if ( + withTable(response.action) && + tables && + response.action.table + ) { + const schema = tables.get(response.action.table.table); + if (schema) { + // If we already have this table open in this viewport, ignore + setDialogState({ + content: ( + + ), + title: "", + }); + } + } + } else { + console.warn(`App, handleServiceRequest ${JSON.stringify(response)}`); + } + }, + [handleClose, setDialogState, tables] + ); + + return { + handleRpcResponse, + }; +}; 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 4a7e69fd52..91562ab062 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx @@ -1,6 +1,5 @@ import { hasAction, MenuRpcResponse, TableSchema } from "@finos/vuu-data"; import { RpcResponseHandler, useVuuTables } from "@finos/vuu-data-react"; -import { registerComponent } from "@finos/vuu-layout"; import { Dialog } from "@finos/vuu-popups"; import { Feature, @@ -13,7 +12,6 @@ import { } from "@finos/vuu-shell"; import { ReactElement, useCallback, useRef, useState } from "react"; import { AppSidePanel } from "./app-sidepanel"; -// import { Stack } from "./AppStack"; import { getDefaultColumnConfig } from "./columnMetaData"; import { getFormConfig } from "./session-editing"; import { createPlaceholder } from "./createPlaceholder"; @@ -33,8 +31,6 @@ const { websocketUrl: serverUrl = defaultWebsocketUrl, features } = const vuuBlotterUrl = "./feature-vuu-table/index.js"; // const vuuBlotterUrl = "./feature-vuu-table/index.js"; -// registerComponent("Stack", Stack, "container"); - // createNewChild is used when we add a new Tab to Stack const layoutProps: ShellProps["LayoutProps"] = { createNewChild: createPlaceholder, @@ -60,7 +56,7 @@ export const App = ({ user }: { user: VuuUser }) => { typeof response.action === "object" && response.action !== null && "type" in response.action && - response?.action?.type === "OPEN_DIALOG_ACTION" + response.action?.type === "OPEN_DIALOG_ACTION" ) { const { tableSchema } = response.action as unknown as { tableSchema: TableSchema; diff --git a/vuu-ui/sample-apps/feature-basket-trading/package.json b/vuu-ui/sample-apps/feature-basket-trading/package.json index 7b8d966809..e8c343ac7f 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/package.json +++ b/vuu-ui/sample-apps/feature-basket-trading/package.json @@ -42,7 +42,24 @@ }, "vuu": { "featureProps": { - "schema": "instrumentPrices" + "schemas": [ + { + "module": "BASKET", + "table": "basket" + }, + { + "module": "BASKET", + "table": "basketTrading" + }, + { + "module": "BASKET", + "table": "basketTradingConstituent" + }, + { + "module": "SIMUL", + "table": "instruments" + } + ] }, "leftNavLocation": "vuu-features" } 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 b999b8f37b..84d486c992 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx @@ -1,17 +1,17 @@ import { TableSchema } from "@finos/vuu-data"; import { FlexboxLayout, Stack } from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { BasketSelectorProps } from "./basket-selector"; import { BasketTableEdit } from "./basket-table-edit"; import { BasketTableLive } from "./basket-table-live"; import { BasketToolbar } from "./basket-toolbar"; import { useBasketTabMenu } from "./useBasketTabMenu"; import { useBasketTradingDataSources } from "./useBasketTradingDatasources"; -import { NewBasketPanel } from "./new-basket-panel"; import "./VuuBasketTradingFeature.css"; import { EmptyBasketsPanel } from "./empty-baskets-panel"; +import { useBasketTrading } from "./useBasketTrading"; const classBase = "VuuBasketTradingFeature"; @@ -20,89 +20,94 @@ const basketStatus: [BasketStatus, BasketStatus] = ["design", "on-market"]; export interface BasketTradingFeatureProps { basketSchema: TableSchema; - basketDefinitionsSchema: TableSchema; - basketDesignSchema: TableSchema; - basketOrdersSchema: TableSchema; + basketTradingSchema: TableSchema; + basketTradingConstituentSchema: TableSchema; instrumentsSchema: TableSchema; } -const VuuBasketTradingFeature = ({ - basketSchema, - basketDefinitionsSchema, - basketDesignSchema, - basketOrdersSchema, - instrumentsSchema, -}: BasketTradingFeatureProps) => { - const [dialog, setDialog] = useState(null); +const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { + const { + basketSchema, + basketTradingSchema, + basketTradingConstituentSchema, + instrumentsSchema, + } = props; + + const basketTradingId = "steve-00001"; + const { activeTabIndex, dataSourceBasket, - dataSourceBasketDefinitions, - dataSourceBasketDefinitionsSearch, - dataSourceBasketDesign, - dataSourceBasketOrders, + dataSourceBasketTrading, + dataSourceBasketTradingSearch, + dataSourceBasketTradingConstituent, dataSourceInstruments, onSendToMarket, onTakeOffMarket, - saveNewBasket, } = useBasketTradingDataSources({ + basketTradingId, basketSchema, - basketDefinitionsSchema, - basketDesignSchema, - basketOrdersSchema, + basketTradingSchema, + basketTradingConstituentSchema, instrumentsSchema, }); - useEffect(() => { - dataSourceBasketDesign.resume?.(); - return () => { - dataSourceBasketDesign.suspend?.(); - }; - }, [dataSourceBasketDesign]); + const [basketCount, setBasketCount] = useState(-1); + useMemo(() => { + dataSourceBasketTradingSearch.subscribe( + { + range: { from: 0, to: 100 }, + }, + (message) => { + console.log("message from dataSourceTrading", { + message, + }); + if (message.size) { + setBasketCount(message.size); + } + } + ); + + // TEMP server is notsending TABLE_ROWS if size is zero + setTimeout(() => { + setBasketCount((count) => (count === -1 ? 0 : count)); + }, 1000); + }, [dataSourceBasketTradingSearch]); + // useEffect(() => { + // dataSourceBasketDesign.resume?.(); + // return () => { + // dataSourceBasketDesign.suspend?.(); + // }; + // }, [dataSourceBasketDesign]); const [buildMenuOptions, handleMenuAction] = useBasketTabMenu({ dataSourceInstruments, }); - const handleClose = useCallback(() => { - setDialog(null); - }, []); - - const handleSaveNewBasket = useCallback( - (basketName, basketId) => { - saveNewBasket(basketName, basketId); - setDialog(null); - }, - [saveNewBasket] - ); + const { basketId, dialog, handleAddBasket } = useBasketTrading({ + basketSchema, + dataSourceBasket, + }); - const handleAddBasket = useCallback(() => { - console.log("add a basket"); - setDialog( - - ); - }, [basketSchema, dataSourceBasket, handleClose, handleSaveNewBasket]); + // useMemo(() => { + // dataSourceBasketTrading.filter = { + // filter: `basketId = "${basketId}"`, + // }; + // }, [basketId, dataSourceBasketTrading]); const basketSelectorProps = useMemo( () => ({ - basketId: "001", - dataSourceBasket: dataSourceBasketDefinitions, - dataSourceBasketSearch: dataSourceBasketDefinitionsSearch, + basketTradingId, + dataSourceBasketTradingSearch: dataSourceBasketTradingSearch, onClickAddBasket: handleAddBasket, }), - [ - dataSourceBasketDefinitions, - dataSourceBasketDefinitionsSearch, - handleAddBasket, - ] + [basketTradingId, dataSourceBasketTradingSearch, handleAddBasket] ); - if (dataSourceBasketDefinitions.size === 0) { + if (basketCount === -1) { + // TODO loading + return null; + } else if (basketCount === 0) { return ( <> @@ -123,6 +128,7 @@ const VuuBasketTradingFeature = ({ @@ -135,13 +141,13 @@ const VuuBasketTradingFeature = ({ diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx index f027468c12..19f52e1fe1 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.tsx @@ -22,9 +22,8 @@ import "./BasketSelector.css"; const classBase = "vuuBasketSelector"; export interface BasketSelectorProps extends HTMLAttributes { - basketId?: string; - dataSourceBasket: DataSource; - dataSourceBasketSearch: DataSource; + basketTradingId?: string; + dataSourceBasketTradingSearch: DataSource; label?: string; onClickAddBasket: () => void; } @@ -44,9 +43,8 @@ export class Basket { } export const BasketSelector = ({ - basketId: basketIdProp, - dataSourceBasket, - dataSourceBasketSearch, + basketTradingId: basketTradingIdProp, + dataSourceBasketTradingSearch, id: idProp, onClickAddBasket, ...htmlAttributes @@ -54,11 +52,13 @@ export const BasketSelector = ({ // const [basket, setBasket] = useState(new Basket()); const rootRef = useRef(null); const columnMap = useMemo( - () => buildColumnMap(dataSourceBasket.columns), - [dataSourceBasket.columns] + () => buildColumnMap(dataSourceBasketTradingSearch.columns), + [dataSourceBasketTradingSearch.columns] ); const [open, setOpen] = useState(false); - const [basketId, setBasketId] = useState(basketIdProp); + const [basketTradingId, setBasketTradingId] = useState( + basketTradingIdProp + ); const [basket, setBasket] = useState(); const id = useId(idProp); const handleData = useCallback( @@ -76,25 +76,27 @@ export const BasketSelector = ({ useMemo(() => { console.log("subscribe to basket"); - dataSourceBasket.subscribe( + dataSourceBasketTradingSearch.subscribe( { range: { from: 0, to: 1 }, - filter: { filter: `id = "NONE"` }, + filter: { filter: `instanceId = "${basketTradingId}"` }, }, handleData ); - }, [dataSourceBasket, handleData]); + }, [basketTradingId, dataSourceBasketTradingSearch, handleData]); useEffect(() => { - console.log(`apply filter id = ${basketId}`); - dataSourceBasket.filter = { filter: `id = "${basketId ?? "NONE"}"` }; - }, [basketId, dataSourceBasket]); + console.log(`apply filter id = ${basketTradingId}`); + dataSourceBasketTradingSearch.filter = { + filter: `id = "${basketTradingId ?? "NONE"}"`, + }; + }, [basketTradingId, dataSourceBasketTradingSearch]); const handleRowClick = useCallback( (row) => { const { id } = columnMap; const basketId = row[id] as string; - setBasketId(basketId); + setBasketTradingId(basketId); setOpen(false); }, [columnMap] @@ -176,7 +178,7 @@ export const BasketSelector = ({
diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketToolbar.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketToolbar.tsx index ec675809ae..05d7c7a219 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketToolbar.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketToolbar.tsx @@ -4,22 +4,25 @@ import { HTMLAttributes } from "react"; import { BasketSelector, BasketSelectorProps } from "../basket-selector"; import { BasketStatus } from "../VuuBasketTradingFeature"; import { BasketMenu } from "./BasketMenu"; +import { MenuActionHandler } from "@finos/vuu-data-types"; +import { DataSource } from "@finos/vuu-data"; import "./BasketToolbar.css"; -import { MenuActionHandler } from "@finos/vuu-data-types"; const classBase = "vuuBasketToolbar"; export interface BasketToolbarProps extends HTMLAttributes { basketStatus: BasketStatus; BasketSelectorProps: BasketSelectorProps; + basketTradingDataSource: DataSource; onSendToMarket: () => void; onTakeOffMarket: () => void; } export const BasketToolbar = ({ - basketStatus, BasketSelectorProps, + basketStatus, + basketTradingDataSource, onSendToMarket, onTakeOffMarket, }: BasketToolbarProps) => { diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.css b/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.css index e1f9e885be..f65ddf9fde 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.css +++ b/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.css @@ -13,4 +13,9 @@ .vuuBasketEmptyBasketsPanel-info { font-size: 18px; font-weight: 700; +} + +.vuuBasketEmptyBasketsPanel-add { + --saltButton-height: 24px; + --saltButton-padding: 16px; } \ No newline at end of file diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.tsx index 2f9c259749..d0191d06dc 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.tsx @@ -17,7 +17,11 @@ export const EmptyBasketsPanel = ({ You do not have any baskets set up - diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css index 2390fafdb4..e6116a0f9d 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css @@ -2,18 +2,19 @@ display: flex; flex-direction: column; height: 200px; + padding: 16px; width: 300px; } .vuuBasketNewBasketPanel-body { - padding: 12px; + flex: 1 1 auto; } .vuuBasketNewBasketPanel-buttonBar { --saltButton-borderRadius: 6px; --saltButton-padding: 12px; display: flex; - align-items: center; + align-items: flex-end; gap: 8px; height: 32px; justify-content: flex-end; 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 b48ac1b6f3..496f63b965 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 @@ -4,22 +4,23 @@ import { PopupComponent as Popup, Portal, } from "@finos/vuu-popups"; -import { TableRowSelectHandler } from "@finos/vuu-table"; import { - Commithandler, InstrumentPicker, InstrumentPickerProps, VuuInput, } from "@finos/vuu-ui-controls"; -import { buildColumnMap } from "@finos/vuu-utils"; import { Button, FormField, FormFieldLabel } from "@salt-ds/core"; import cx from "classnames"; -import { HTMLAttributes, useCallback, useMemo, useState } from "react"; +import { DataSourceRow } from "packages/vuu-data-types"; +import { HTMLAttributes, useMemo } from "react"; import "./NewBasketPanel.css"; +import { useNewBasketPanel } from "./useNewBasketPanel"; const classBase = "vuuBasketNewBasketPanel"; +const displayName = (key: number) => (row: DataSourceRow) => String(row[key]); + export interface NewBasketPanelProps extends HTMLAttributes { basketDataSource: DataSource; basketSchema: TableSchema; @@ -37,11 +38,25 @@ export const NewBasketPanel = ({ onSaveBasket, ...htmlAttributes }: NewBasketPanelProps) => { + const { + columnMap, + onChangeBasketName, + onOpenChangeInstrumentPicker, + onFeatureEnabled, + onSave, + onSelectBasket, + saveButtonDisabled, + } = useNewBasketPanel({ + basketDataSource, + basketSchema, + onSaveBasket, + }); + const tableProps = useMemo( () => ({ config: { columns: [ - { name: "ID", hidden: true }, + { name: "id", hidden: true }, { name: "name", width: 200, @@ -50,37 +65,12 @@ export const NewBasketPanel = ({ rowSeparators: true, }, dataSource: basketDataSource, + onFeatureEnabled, }), - [basketDataSource] - ); - const [basketName, setBasketName] = useState(""); - const [basketId, setBasketId] = useState(); - - const columnMap = buildColumnMap(basketSchema.columns); - - const handleChangeBasketName = useCallback>( - (evt, value) => { - setBasketName(value); - }, - [] - ); - - const handleSelectBasket = useCallback( - (row) => { - const { ID } = columnMap; - const basketId = row[ID] as string; - setBasketId(basketId); - }, - [columnMap] + [basketDataSource, onFeatureEnabled] ); - const saveBasket = useCallback(() => { - if (basketName && basketId) { - onSaveBasket(basketName, basketId); - } - }, [basketId, basketName, onSaveBasket]); - - const disableSave = basketName === "" || basketId === undefined; + const itemToString = displayName(columnMap.name); return ( @@ -90,27 +80,33 @@ export const NewBasketPanel = ({
Basket Name - + Basket Definition
-
-
- - +
+ + +
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 new file mode 100644 index 0000000000..c3aaa80c19 --- /dev/null +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts @@ -0,0 +1,113 @@ +import { VuuFeatureMessage } from "@finos/vuu-data"; +import { isViewportMenusAction } from "@finos/vuu-data-react"; +import { + ClientToServerMenuRPC, + VuuMenu, + VuuMenuItem, +} from "@finos/vuu-protocol-types"; +import { TableRowSelectHandler } from "@finos/vuu-table"; +import { Commithandler, OpenChangeHandler } from "@finos/vuu-ui-controls"; +import { buildColumnMap, metadataKeys } from "@finos/vuu-utils"; +import { useCallback, useRef, useState } from "react"; +import { NewBasketPanelProps } from "./NewBasketPanel"; + +const { KEY } = metadataKeys; + +type Menu = VuuMenu | VuuMenuItem; +const isMenu = (menu: Menu): menu is VuuMenu => "menus" in menu; +const flatten = (menus: Menu[], menuItems: VuuMenuItem[] = []) => { + menus.forEach((m) => + isMenu(m) ? flatten(m.menus, menuItems) : menuItems.push(m) + ); + return menuItems; +}; +const getRpcCommand = (menus: Menu[], selectRpcCommand?: string) => { + const selectionMenuItems = flatten(menus).filter( + (m) => m.context === "selected-rows" + ); + if (selectRpcCommand) { + return selectionMenuItems.find((m) => m.rpcName === selectRpcCommand); + } else if (selectionMenuItems.length === 1) { + return selectionMenuItems[0]; + } +}; + +export type NewBasketHookProps = Pick< + NewBasketPanelProps, + "basketDataSource" | "basketSchema" | "onSaveBasket" +>; + +export const useNewBasketPanel = ({ + basketDataSource, + basketSchema, + onSaveBasket, +}: NewBasketHookProps) => { + const rpcCommandRef = useRef(); + const columnMap = buildColumnMap(basketSchema.columns); + const [basketName, setBasketName] = useState(""); + const [basketId, setBasketId] = useState(); + + const saveBasket = useCallback(() => { + if (basketName && basketId) { + onSaveBasket(basketName, basketId); + + if (rpcCommandRef.current) { + basketDataSource.menuRpcCall({ + rpcName: rpcCommandRef.current, + type: "VIEW_PORT_MENUS_SELECT_RPC", + } as Omit); + + requestAnimationFrame(() => { + basketDataSource.unsubscribe(); + }); + } + } + }, [basketDataSource, basketId, basketName, onSaveBasket]); + + const handleSelectBasket = useCallback( + (row) => { + const basketId = row[KEY] as string; + console.log({ basketId, columnMap }); + setBasketId(basketId); + }, + [columnMap] + ); + + const handleChangeBasketName = useCallback>( + (evt, value) => { + setBasketName(value); + }, + [] + ); + + const handleFeatureEnabled = useCallback((message: VuuFeatureMessage) => { + if (isViewportMenusAction(message)) { + const { + menu: { menus }, + } = message; + const rpcCommand = getRpcCommand(menus, "CREATE_NEW_BASKET"); + console.log({ rpcCommand }); + rpcCommandRef.current = rpcCommand?.rpcName; + } + }, []); + + const handleOpenChangeInstrumentPicker = useCallback( + (open, closeReason) => { + if (!open) { + console.log(`instrument picker closed ${closeReason}`); + } + }, + [] + ); + + return { + columnMap, + onChangeBasketName: handleChangeBasketName, + onCloseInstrumentPicker: handleOpenChangeInstrumentPicker, + onFeatureEnabled: handleFeatureEnabled, + onOpenChangeInstrumentPicker: handleOpenChangeInstrumentPicker, + onSave: saveBasket, + onSelectBasket: handleSelectBasket, + saveButtonDisabled: basketName === "" || basketId === undefined, + }; +}; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx new file mode 100644 index 0000000000..90d2f5992d --- /dev/null +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx @@ -0,0 +1,69 @@ +import { DataSource, TableSchema } from "@finos/vuu-data"; +import { useViewContext } from "@finos/vuu-layout"; +import { useCallback, useMemo, useState } from "react"; +import { NewBasketPanel } from "./new-basket-panel"; + +export interface BasketTradingHookProsp { + basketSchema: TableSchema; + dataSourceBasket: DataSource; +} + +type BasketState = { + basketId?: string; + dialog?: JSX.Element; +}; + +const NO_STATE = { basketId: undefined } as any; + +export const useBasketTrading = ({ + basketSchema, + dataSourceBasket, +}: BasketTradingHookProsp) => { + const { load, save } = useViewContext(); + + const { basketId } = useMemo(() => { + const { basketId } = load?.("basket-state") ?? NO_STATE; + if (basketId) { + console.log(`loaded basketId ${basketId}`); + } + return { basketId }; + }, [load]); + + const [basketState, setBasketState] = useState({ + basketId, + dialog: undefined, + }); + + const handleClose = useCallback(() => { + setBasketState((state) => ({ + ...state, + dialog: undefined, + })); + }, []); + + const handleSaveNewBasket = useCallback((basketName, basketId) => { + setBasketState({ + basketId, + dialog: undefined, + }); + }, []); + + const handleAddBasket = useCallback(() => { + setBasketState((state) => ({ + ...state, + dialog: ( + + ), + })); + }, [basketSchema, dataSourceBasket, handleClose, handleSaveNewBasket]); + + return { + ...basketState, + handleAddBasket, + }; +}; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts index 506de0f418..914a1ab19c 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts @@ -2,50 +2,57 @@ import { useViewContext } from "@finos/vuu-layout"; import { DataSource, RemoteDataSource, TableSchema } from "@finos/vuu-data"; import { useCallback, useMemo, useState } from "react"; import { BasketTradingFeatureProps } from "./VuuBasketTradingFeature"; +import { VuuFilter } from "packages/vuu-protocol-types"; export type basketDataSourceKey = | "data-source-basket" - | "data-source-basket-definitions" - | "data-source-basket-definitions-search" - | "data-source-basket-design" - | "data-source-basket-orders" + | "data-source-basket-trading" + | "data-source-basket-trading-search" + | "data-source-basket-trading-constituent" | "data-source-instruments"; export const useBasketTradingDataSources = ({ basketSchema, - basketDefinitionsSchema, - basketDesignSchema, - basketOrdersSchema, + basketTradingId, + basketTradingSchema, + basketTradingConstituentSchema, instrumentsSchema, -}: BasketTradingFeatureProps) => { +}: BasketTradingFeatureProps & { basketTradingId: string }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const { id, loadSession, saveSession, title } = useViewContext(); const [ dataSourceBasket, - dataSourceBasketDefinitions, - dataSourceBasketDefinitionsSearch, - dataSourceBasketDesign, - dataSourceBasketOrders, + dataSourceBasketTrading, + dataSourceBasketTradingSearch, + dataSourceBasketTradingConstituent, dataSourceInstruments, ] = useMemo(() => { - const dataSourceConfig: [basketDataSourceKey, TableSchema][] = [ + const basketFilter: VuuFilter = { + filter: `instanceId = "${basketTradingId}"`, + }; + const dataSourceConfig: [basketDataSourceKey, TableSchema, VuuFilter?][] = [ ["data-source-basket", basketSchema], - ["data-source-basket-definitions", basketDefinitionsSchema], - ["data-source-basket-definitions-search", basketDefinitionsSchema], - ["data-source-basket-design", basketDesignSchema], - ["data-source-basket-orders", basketOrdersSchema], + ["data-source-basket-trading", basketTradingSchema, basketFilter], + ["data-source-basket-trading-search", basketTradingSchema, basketFilter], + [ + "data-source-basket-trading-constituent", + basketTradingConstituentSchema, + basketFilter, + ], ["data-source-instruments", instrumentsSchema], ]; const dataSources: DataSource[] = []; - for (const [key, schema] of dataSourceConfig) { + for (const [key, schema, filter] of dataSourceConfig) { + console.log(`filter for ${key} = ${JSON.stringify(filter)}`); let dataSource = loadSession?.(key) as RemoteDataSource; if (dataSource === undefined) { dataSource = new RemoteDataSource({ bufferSize: 200, - viewport: id, + filter, + viewport: `${id}-${key}`, table: schema.table, columns: schema.columns.map((col) => col.name), title, @@ -56,15 +63,15 @@ export const useBasketTradingDataSources = ({ } return dataSources; }, [ - basketDefinitionsSchema, - basketDesignSchema, - basketOrdersSchema, basketSchema, - id, + basketTradingSchema, + basketTradingId, + basketTradingConstituentSchema, instrumentsSchema, loadSession, - saveSession, + id, title, + saveSession, ]); const handleSendToMarket = useCallback(() => { @@ -75,20 +82,14 @@ export const useBasketTradingDataSources = ({ setActiveTabIndex(0); }, []); - const saveNewBasket = useCallback((basketName: string, basketId: string) => { - console.log(`save new baskert ${basketName}, ${basketId}`); - }, []); - return { activeTabIndex, dataSourceBasket, - dataSourceBasketDefinitions, - dataSourceBasketDefinitionsSearch, - dataSourceBasketDesign, - dataSourceBasketOrders, + dataSourceBasketTrading, + dataSourceBasketTradingSearch, + dataSourceBasketTradingConstituent, dataSourceInstruments, onSendToMarket: handleSendToMarket, onTakeOffMarket: handleTakeOffMarket, - saveNewBasket, }; }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/tsconfig.json b/vuu-ui/sample-apps/feature-basket-trading/tsconfig.json new file mode 100644 index 0000000000..db9582a162 --- /dev/null +++ b/vuu-ui/sample-apps/feature-basket-trading/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.css b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.css index e69de29bb2..d2006a3823 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.css +++ b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.css @@ -0,0 +1,10 @@ +.vuuFilterTableFeature { + --vuuMeasuredContainer-flex: 1 1 1px; + min-height: 100px; + +} + +.vuuFilterTableFeature-footer { + --vuuToolbarProxy-height: auto; + flex: 0 0 18px; +} \ No newline at end of file diff --git a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx index bf2b748403..f3a600af26 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx @@ -1,271 +1,26 @@ -import { - DataSource, - DataSourceConfig, - DataSourceVisualLinkCreatedMessage, - RemoteDataSource, - SchemaColumn, - TableSchema, - VuuFeatureInvocationMessage, - VuuFeatureMessage, -} from "@finos/vuu-data"; -import { DataSourceFilter } from "@finos/vuu-data-types"; -import { TableConfig } from "@finos/vuu-datagrid-types"; +import { TableSchema } from "@finos/vuu-data"; import { FilterTable } from "@finos/vuu-datatable"; -import { Filter } from "@finos/vuu-filter-types"; -import { FilterBarProps } from "@finos/vuu-filters"; -import { FlexboxLayout, useViewContext } from "@finos/vuu-layout"; -import { ShellContextProps, useShellContext } from "@finos/vuu-shell"; +import { FlexboxLayout } from "@finos/vuu-layout"; import { DataSourceStats } from "@finos/vuu-table-extras"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { - isViewportMenusAction, - isVisualLinksAction, - MenuActionConfig, - useVuuMenuActions, -} from "@finos/vuu-data-react"; +import cx from "classnames"; import { ContextMenuProvider } from "@finos/vuu-popups"; -import { LinkDescriptorWithLabel, VuuMenu } from "@finos/vuu-protocol-types"; +import { useFilterTable } from "./useFilterTable"; -import { Button } from "@salt-ds/core"; import "./VuuFilterTableFeature.css"; -const classBase = "VuuFilterTableFeature"; +const classBase = "vuuFilterTableFeature"; export interface FilterTableFeatureProps { tableSchema: TableSchema; } -type FilterTableConfig = { - "available-columns"?: SchemaColumn[]; - "datasource-config"?: DataSourceConfig; - "filterbar-config"?: Partial; - "table-config"?: TableConfig; -}; - -const NO_CONFIG: FilterTableConfig = {}; - -const applyDefaults = ( - { columns, table }: TableSchema, - getDefaultColumnConfig?: ShellContextProps["getDefaultColumnConfig"] -) => { - if (typeof getDefaultColumnConfig === "function") { - return columns.map((column) => { - const config = getDefaultColumnConfig(table.table, column.name); - if (config) { - return { - ...column, - ...config, - }; - } else { - return column; - } - }); - } else { - return columns; - } -}; const VuuFilterTableFeature = ({ tableSchema }: FilterTableFeatureProps) => { - const { id, dispatch, load, save, loadSession, saveSession, title } = - useViewContext(); - const { - "available-columns": availableColumnsFromState, - "datasource-config": dataSourceConfigFromState, - "filterbar-config": filterbarConfigFromState, - "table-config": tableConfigFromState, - } = useMemo(() => load?.() ?? NO_CONFIG, [load]); - - const activeRef = useRef( - filterbarConfigFromState?.activeFilterIndex ?? [] - ); - const [filters, setFilters] = useState( - filterbarConfigFromState?.filters ?? [] - ); - - const handleFiltersChanged = useCallback( - (filters: Filter[]) => { - save?.( - { activeFilterIndex: activeRef.current, filters }, - "filterbar-config" - ); - setFilters(filters); - }, - [save] - ); - - const handleDataSourceConfigChange = useCallback( - (config: DataSourceConfig | undefined, confirmed?: boolean) => { - // confirmed / unconfirmed messages are used for UI updates, not state saving - if (confirmed === undefined) { - save?.(config, "datasource-config"); - } - }, - [save] - ); - - const handleAvailableColumnsChange = useCallback( - (columns: SchemaColumn[]) => { - console.log("save new available columns"); - save?.(columns, "available-columns"); - // tableConfigRef.current = config; - }, - [save] - ); - - const handleTableConfigChange = useCallback( - (config: TableConfig) => { - console.log(`tabale config changed`); - save?.(config, "table-config"); - // tableConfigRef.current = config; - }, - [save] - ); - - const { getDefaultColumnConfig, handleRpcResponse } = useShellContext(); - - const tableConfig = useMemo( - () => ({ - ...tableConfigFromState, - columns: - tableConfigFromState?.columns || - applyDefaults(tableSchema, getDefaultColumnConfig), - }), - [getDefaultColumnConfig, tableConfigFromState, tableSchema] - ); - - const dataSource: DataSource = useMemo(() => { - let ds = loadSession?.("data-source") as RemoteDataSource; - if (ds) { - console.log( - "%cFilterTableFeature DATA SOURCE IN SESSION STATE", - "color:red;font-weight:bold;" - ); - - return ds; - } - const columns = - dataSourceConfigFromState?.columns ?? - tableSchema.columns.map((col) => col.name); - - ds = new RemoteDataSource({ - bufferSize: 200, - viewport: id, - table: tableSchema.table, - ...dataSourceConfigFromState, - columns, - title, - }); - ds.on("config", handleDataSourceConfigChange); - saveSession?.(ds, "data-source"); - return ds; - }, [ - dataSourceConfigFromState, - handleDataSourceConfigChange, - id, - loadSession, - saveSession, - tableSchema.columns, - tableSchema.table, - title, - ]); - - useEffect(() => { - dataSource.resume?.(); - return () => { - dataSource.suspend?.(); - }; - }, [dataSource]); - - const removeVisualLink = useCallback(() => { - dataSource.visualLink = undefined; - }, [dataSource]); - - const handleVuuFeatureEnabled = useCallback( - (message: VuuFeatureMessage) => { - if (isViewportMenusAction(message)) { - saveSession?.(message.menu, "vuu-menu"); - } else if (isVisualLinksAction(message)) { - saveSession?.(message.links, "vuu-links"); - } - }, - [saveSession] - ); - - const handleVuuFeatureInvoked = useCallback( - (message: VuuFeatureInvocationMessage) => { - if (message.type === "vuu-link-created") { - dispatch?.({ - type: "add-toolbar-contribution", - location: "post-title", - content: ( -