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 index 23a7c07d2..aa1edde74 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts @@ -6,6 +6,7 @@ export type BasketsTableName = | "basketConstituent" | "basketTrading" | "basketTradingConstituent" + | "basketTrdConsPrices" | "priceStrategyType"; export const schemas: Readonly< @@ -67,12 +68,41 @@ export const schemas: Readonly< { name: "description", serverDataType: "string" }, { name: "instanceId", serverDataType: "string" }, { name: "instanceIdRic", serverDataType: "string" }, + { name: "limitPrice", serverDataType: "double" }, + { name: "notionalLocal", serverDataType: "double" }, + { name: "notionalUsd", 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" }, + }, + basketTrdConsPrices: { + columns: [ + { name: "algo", serverDataType: "string" }, + { name: "algoParams", serverDataType: "string" }, + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, + { name: "basketId", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", 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: "open", serverDataType: "double" }, { name: "pctFilled", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, { name: "priceSpread", serverDataType: "int" }, { name: "priceStrategyId", serverDataType: "int" }, { name: "quantity", serverDataType: "long" }, 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 index 3d56d8134..165531d04 100644 --- 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 @@ -2,6 +2,9 @@ import { VuuDataRow } from "@finos/vuu-protocol-types"; import { ColumnMap } from "@finos/vuu-utils"; import { getSchema } from "../../schemas"; +import baskets, { BasketColumnMap } from "./basket"; +import basketConstituents from "./basketConstituent"; + const schema = getSchema("basketTrading"); export const BasketTradingColumnMap = Object.values( @@ -11,6 +14,32 @@ export const BasketTradingColumnMap = Object.values( return map; }, {}); +let instance = 1; + const data: VuuDataRow[] = []; +const createBasket = (basketId: string, basketName: string) => { + const key = BasketColumnMap.basketId; + const basketRow = baskets.find((basket) => basket[key] === basketId); + const basketTradingRow = [ + basketId, + basketName, + 0, + 0, + `steve-${instance++}`, + "OFF-MARKET", + 0, + 0, + 0, + ]; + data.push(basketTradingRow); +}; + +createBasket(".FTSE", "Steve FTSE 1"); +createBasket(".FTSE", "Steve FTSE 2"); +createBasket(".FTSE", "Steve FTSE 3"); +createBasket(".FTSE", "Steve FTSE 4"); +createBasket(".FTSE", "Steve FTSE 5"); +createBasket(".FTSE", "Steve FTSE 6"); + export default data; diff --git a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts index c919eccf2..82262beec 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 @@ -201,34 +201,41 @@ export class ArrayDataSource this.status = "subscribed"; this.lastRangeServed = { from: 0, to: 0 }; - if (aggregations || columns || filter || groupBy || sort) { + let config = this.#config; + + const hasConfigProps = aggregations || columns || filter || groupBy || sort; + if (hasConfigProps) { if (range) { this.#range = range; } - this.config = { - ...this.#config, + config = { + ...config, aggregations: aggregations || this.#config.aggregations, columns: columns || this.#config.columns, filter: filter || this.#config.filter, groupBy: groupBy || this.#config.groupBy, sort: sort || this.#config.sort, }; - } else { - this.clientCallback?.({ - ...this.#config, - type: "subscribed", - clientViewportId: this.viewport, - range: this.#range, - tableSchema: this.tableSchema, - }); + } + this.clientCallback?.({ + ...config, + type: "subscribed", + clientViewportId: this.viewport, + range: this.#range, + tableSchema: this.tableSchema, + }); + + if (hasConfigProps) { + // invoke setter to action config + this.config = config; + } else { this.clientCallback({ clientViewportId: this.viewport, mode: "size-only", type: "viewport-update", size: this.#data.length, }); - if (range) { // set range and trigger dispatch of initial rows this.range = range; @@ -570,8 +577,8 @@ export class ArrayDataSource console.log({ row, colName, value }); } - 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[0]} ${columnName} ${value}`); return true; } 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 aaa8245fa..1d81b5807 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -129,7 +129,6 @@ export class RemoteDataSource }: SubscribeProps, callback: SubscribeCallback ) { - console.log(`%csubscribe`, "color:red;font-weight:bold;"); this.clientCallback = callback; if (aggregations || columns || filter || groupBy || sort) { @@ -213,16 +212,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.server = null; this.removeAllListeners(); this.status = "unsubscribed"; this.viewport = undefined; + this.range = { from: 0, to: 0 }; } suspend() { @@ -618,10 +617,6 @@ export class RemoteDataSource } applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { - console.log( - `ArrayDataSource applyEdit ${row.join(",")} ${columnName} ${value}` - ); - this.menuRpcCall({ rowKey: row[KEY], field: columnName, 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 5f824d06d..34a8f09f3 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 @@ -506,8 +506,6 @@ export class ServerProxy { | WithRequestId | WithRequestId ) { - debug?.(`handleMessageFromClient: ${message.type}`); - if (isViewportMessage(message)) { if (message.type === "disable") { // Viewport may already have been unsubscribed diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts index 47986f693..70a93683b 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -116,10 +116,8 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { return new Promise((resolve) => { const applicationLayout = getLocalEntity(this.#urlKey); if (applicationLayout) { - console.log(applicationLayout); resolve(applicationLayout); } else { - console.log(defaultLayout); resolve(defaultLayout); } }); diff --git a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx index 118436779..893b7273e 100644 --- a/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx +++ b/vuu-ui/packages/vuu-popups/src/popup/Popup.tsx @@ -1,5 +1,4 @@ import cx from "classnames"; -import { useThemeAttributes } from "@finos/vuu-shell"; import { HTMLAttributes, RefObject } from "react"; import { Position, useAnchoredPosition } from "./useAnchoredPosition"; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index e16aa8806..99a9b9a22 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -28,9 +28,9 @@ export const LayoutManagementContext = React.createContext<{ loadLayoutById: (id: string) => void; }>({ layoutMetadata: [], - saveLayout: () => null, + saveLayout: () => undefined, applicationLayout: defaultLayout, - saveApplicationLayout: () => null, + saveApplicationLayout: () => undefined, loadLayoutById: () => defaultLayout, }); diff --git a/vuu-ui/packages/vuu-shell/src/shell.tsx b/vuu-ui/packages/vuu-shell/src/shell.tsx index bde16aa6f..ac903cf4b 100644 --- a/vuu-ui/packages/vuu-shell/src/shell.tsx +++ b/vuu-ui/packages/vuu-shell/src/shell.tsx @@ -6,7 +6,6 @@ import { ReactNode, useCallback, useEffect, - useMemo, useRef, } from "react"; import { diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx index 7fd0bd51a..cc3c58c5a 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/BackgroundCell.tsx @@ -70,8 +70,6 @@ export const BackgroundCell = ({ column, row }: TableCellProps) => { ); }; -console.log("register BackgroundCellNext"); - registerComponent("background-next", BackgroundCell, "cell-renderer", { description: "Change background color of cell when value changes", label: "Background Flash", diff --git a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.tsx b/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.tsx index 9b06a6ef5..9418ea6ec 100644 --- a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.tsx @@ -51,7 +51,6 @@ const JsonCell = ({ column, row }: TableCellProps) => { } }; -console.log("register JsonCell"); registerComponent("json", JsonCell, "cell-renderer", { description: "JSON formatter", label: "JSON formatter", diff --git a/vuu-ui/packages/vuu-theme/css/components/button.css b/vuu-ui/packages/vuu-theme/css/components/button.css index 3978c8744..d201a23da 100644 --- a/vuu-ui/packages/vuu-theme/css/components/button.css +++ b/vuu-ui/packages/vuu-theme/css/components/button.css @@ -14,6 +14,10 @@ --saltButton-borderColor: var(--salt-actionable-primary-background-hover) } +.saltButton-secondary { + --saltButton-borderStyle: none; +} + .saltButton:focus-visible { outline-color: var(--vuuToolbarItem-outlineColor, var(--vuu-color-purple-10)); outline-style: dashed; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.tsx index e9d2896c5..af4a06eb2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.tsx @@ -24,8 +24,6 @@ export const SearchCell = ({ ); }; -console.log("register SearchCell"); - registerComponent("search-cell", SearchCell, "cell-renderer", { serverDataType: "private", }); 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 1fea1733c..575cba04d 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 @@ -32,7 +32,7 @@ export const useInstrumentPicker = ({ defaultIsOpen, isOpen: isOpenProp, itemToString = defaultItemToString(columns, columnMap), - onOpenChange: onOpenChangeProp, + onOpenChange, onSelect, searchColumns, }: InstrumentPickerHookProps) => { @@ -52,9 +52,12 @@ export const useInstrumentPicker = ({ const handleOpenChange = useCallback( (open, closeReason) => { setIsOpen(open); - onOpenChangeProp?.(open, closeReason); + onOpenChange?.(open, closeReason); + if (open === false) { + dataSource.unsubscribe(); + } }, - [onOpenChangeProp, setIsOpen] + [dataSource, onOpenChange, setIsOpen] ); const handleInputChange = useCallback( @@ -82,11 +85,10 @@ export const useInstrumentPicker = ({ (row) => { const value = itemToString(row); setValue(value); - setIsOpen(false); onSelect(row); - onOpenChangeProp?.(false, "select"); + handleOpenChange?.(false, "select"); }, - [itemToString, onOpenChangeProp, onSelect, setIsOpen] + [handleOpenChange, itemToString, onSelect] ); const inputProps = { diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx index 34fc2e1a0..298c038dd 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-search/InstrumentSearch.tsx @@ -4,7 +4,13 @@ import { registerComponent } from "@finos/vuu-layout"; import { TableNext, TableProps } from "@finos/vuu-table"; import { FormField, FormFieldLabel, Input } from "@salt-ds/core"; import cx from "classnames"; -import { FormEvent, HTMLAttributes, useCallback, useState } from "react"; +import { + FormEvent, + HTMLAttributes, + useCallback, + useMemo, + useState, +} from "react"; import "./SearchCell"; import "./InstrumentSearch.css"; @@ -31,7 +37,8 @@ const defaultTableConfig: TableConfig = { export interface InstrumentSearchProps extends HTMLAttributes { TableProps?: Partial; dataSource: DataSource; - searchColumn?: string; + placeHolder?: string; + searchColumns?: string[]; } const searchIcon = ; @@ -40,9 +47,16 @@ export const InstrumentSearch = ({ TableProps, className, dataSource, - searchColumn = "description", + placeHolder, + searchColumns = ["description"], ...htmlAttributes }: InstrumentSearchProps) => { + const baseFilterPattern = useMemo( + // TODO make this contains once server supports it + () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), + [searchColumns] + ); + const [searchState, setSearchState] = useState<{ searchText: string; filter: string; @@ -51,21 +65,16 @@ export const InstrumentSearch = ({ const handleChange = useCallback( (evt: FormEvent) => { const { value } = evt.target as HTMLInputElement; - const filter = `name starts "${value}"`; + const filter = baseFilterPattern.replaceAll("__VALUE__", value); setSearchState({ searchText: value, filter, }); dataSource.filter = { filter, - filterStruct: { - op: "starts", - column: searchColumn, - value, - }, }; }, - [dataSource, searchColumn] + [baseFilterPattern, dataSource] ); return ( @@ -74,12 +83,14 @@ export const InstrumentSearch = ({ { const { buildMenuOptions, handleMenuAction } = useLayoutContextMenuItems(setDialogState); - const { - buildMenuOptions, - dialogContent: saveLayoutDialog, - handleCloseDialog, - handleMenuAction, - } = useLayoutContextMenuItems(); - - const handleClose = useCallback(() => { - setDialogContent(undefined); - handleCloseDialog?.(); - }, [handleCloseDialog]); - // TODO get Context from Shell return ( { instrumentsSchema, } = props; - const basketTradingId = "steve-00001"; + const basketInstanceId = "steve-00001"; const { activeTabIndex, dataSourceBasket, dataSourceBasketTrading, + dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituent, dataSourceInstruments, onSendToMarket, onTakeOffMarket, } = useBasketTradingDataSources({ - basketTradingId, + basketInstanceId, basketSchema, basketTradingSchema, basketTradingConstituentSchema, @@ -54,17 +55,20 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { const [basketCount, setBasketCount] = useState(-1); useMemo(() => { - dataSourceBasketTradingSearch.subscribe( + dataSourceBasketTradingControl.subscribe( { range: { from: 0, to: 100 }, }, (message) => { - console.log("message from dataSourceTrading", { + console.log("message from dataSourceTradingControl", { message, }); if (message.size) { setBasketCount(message.size); } + if (message.rows) { + console.table(message.rows); + } } ); @@ -72,19 +76,19 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { setTimeout(() => { setBasketCount((count) => (count === -1 ? 0 : count)); }, 1000); - }, [dataSourceBasketTradingSearch]); - // useEffect(() => { - // dataSourceBasketDesign.resume?.(); - // return () => { - // dataSourceBasketDesign.suspend?.(); - // }; - // }, [dataSourceBasketDesign]); + }, [dataSourceBasketTradingControl]); + useEffect(() => { + // dataSourceBasketDesign.resume?.(); + return () => { + dataSourceBasketTradingControl.unsubscribe?.(); + }; + }, [dataSourceBasketTradingControl]); const [buildMenuOptions, handleMenuAction] = useBasketTabMenu({ dataSourceInstruments, }); - const { basketId, dialog, handleAddBasket } = useBasketTrading({ + const { dialog, handleAddBasket } = useBasketTrading({ basketSchema, dataSourceBasket, }); @@ -97,11 +101,12 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { const basketSelectorProps = useMemo( () => ({ - basketTradingId, + basketInstanceId, + dataSourceBasketTrading, dataSourceBasketTradingSearch: dataSourceBasketTradingSearch, onClickAddBasket: handleAddBasket, }), - [basketTradingId, dataSourceBasketTradingSearch, handleAddBasket] + [dataSourceBasketTrading, dataSourceBasketTradingSearch, handleAddBasket] ); if (basketCount === -1) { diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.css b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.css index 9bb173a94..75c2e2dc7 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.css +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelector.css @@ -2,7 +2,7 @@ --col-gap: 32px; --basket-selector-height: 61px; align-items: center; - border: solid 1px var(--salt-editable-borderColor); + border: solid 1px var(--vuu-color-gray-45); border-radius: 6px; display: inline-flex; height: var(--basket-selector-height); @@ -13,11 +13,12 @@ display: grid; flex: 1 1 auto; gap: 0px var(--col-gap); - grid-template-columns: max-content min-content min-content; + grid-template-columns: max-content min-content min-content 24px; grid-template-rows: 18px 1fr; } .vuuBasketSelector-label { color: var(--vuu-color-gray-50); + line-height: 1.5; position: relative; } @@ -35,6 +36,7 @@ font-size: 20px; font-weight: 700; line-height: normal; + min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -45,6 +47,7 @@ font-size: 20px; font-weight: 400; line-height: normal; + min-width: 50px; overflow: hidden; position: relative; text-overflow: ellipsis; @@ -59,23 +62,26 @@ --vuuPriceTicker-fontSize: 20px; --vuuPriceTicker-fontWeight: 400; line-height: 1.5; + min-width: 80px; } .vuuBasketSelector-trigger { --saltButton-height: 24px; --saltButton-width: 24px; + --vuu-icon-color: var(--vuu-color-gray-50); --vuu-icon-height: 24px; --vuu-icon-size: 24px; --vuu-icon-width: 24px; - flex: 0 0 auto; - margin-left: 16px; + align-self: center; + grid-column: 4; + grid-row: 1 / span 2; } .vuuBasketSelector-searchContainer { + --vuuMeasuredContainer-flex: 1 1 1px; display: flex; flex-direction: column; height: 400px; - width: 377px; } .vuuBasketSelector-instrumentSearch { 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 19f52e1fe..81b5381a3 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 @@ -1,141 +1,58 @@ -import { PriceTicker } from "@finos/vuu-ui-controls"; +import { DropdownBase, PriceTicker } from "@finos/vuu-ui-controls"; import { Button } from "@salt-ds/core"; -import { DataSource, SubscribeCallback } from "@finos/vuu-data"; +import { DataSource } from "@finos/vuu-data"; import { useId } from "@finos/vuu-layout"; -import { PopupComponent as Popup, Portal } from "@finos/vuu-popups"; -import { VuuDataRow } from "@finos/vuu-protocol-types"; -import { TableProps, TableRowClickHandler } from "@finos/vuu-table"; -import { InstrumentSearch } from "@finos/vuu-ui-controls"; -import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; -import { - HTMLAttributes, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { BasketSelectorRow } from "./BasketSelectorRow"; +import { DropdownBaseProps, InstrumentSearch } from "@finos/vuu-ui-controls"; +import { HTMLAttributes, useCallback, useRef } from "react"; import "./BasketSelector.css"; +import { useBasketSelector } from "./useBasketSelector"; const classBase = "vuuBasketSelector"; -export interface BasketSelectorProps extends HTMLAttributes { - basketTradingId?: string; +export interface BasketSelectorProps + extends Pick, + HTMLAttributes { + basketInstanceId?: string; + dataSourceBasketTrading: DataSource; dataSourceBasketTradingSearch: DataSource; label?: string; onClickAddBasket: () => void; } -export class Basket { - currency: string; - exchangeRateToUSD: number; - name: string; - symbolName: string; - - constructor(data: VuuDataRow, columnMap: ColumnMap) { - this.currency = data[columnMap.currency] as string; - this.exchangeRateToUSD = data[columnMap.exchangeRateToUSD] as number; - this.name = data[columnMap.name] as string; - this.symbolName = data[columnMap.symbolName] as string; - } -} - export const BasketSelector = ({ - basketTradingId: basketTradingIdProp, + basketInstanceId, + dataSourceBasketTrading, dataSourceBasketTradingSearch, id: idProp, + isOpen: isOpenProp, onClickAddBasket, + onOpenChange: onOpenChangeProp, ...htmlAttributes }: BasketSelectorProps) => { - // const [basket, setBasket] = useState(new Basket()); const rootRef = useRef(null); - const columnMap = useMemo( - () => buildColumnMap(dataSourceBasketTradingSearch.columns), - [dataSourceBasketTradingSearch.columns] - ); - const [open, setOpen] = useState(false); - const [basketTradingId, setBasketTradingId] = useState( - basketTradingIdProp - ); - const [basket, setBasket] = useState(); const id = useId(idProp); - const handleData = useCallback( - (message) => { - if (message.type === "viewport-update" && message.rows?.length === 1) { - setBasket(new Basket(message.rows[0], columnMap)); - } - }, - [columnMap] - ); - - const toggleSearch = useCallback(() => { - setOpen((open) => !open); - }, []); - - useMemo(() => { - console.log("subscribe to basket"); - dataSourceBasketTradingSearch.subscribe( - { - range: { from: 0, to: 1 }, - filter: { filter: `instanceId = "${basketTradingId}"` }, - }, - handleData - ); - }, [basketTradingId, dataSourceBasketTradingSearch, handleData]); - useEffect(() => { - 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; - setBasketTradingId(basketId); - setOpen(false); - }, - [columnMap] - ); - - const tableProps: Partial = useMemo( - () => ({ - Row: BasketSelectorRow, - config: { - columns: [ - { name: "id", width: 300 }, - { - hidden: true, - name: "name", - width: 200, - }, - { - hidden: true, - name: "symbolName", - width: 100, - type: { - name: "string", - }, - }, - ], - }, - onRowClick: handleRowClick, - rowHeight: 47, - }), - [handleRowClick] - ); + const { basket, isOpen, onOpenChange, tableProps } = useBasketSelector({ + basketInstanceId, + dataSourceBasketTrading, + dataSourceBasketTradingSearch, + isOpen: isOpenProp, + onOpenChange: onOpenChangeProp, + }); const handleClickAddBasket = useCallback(() => { - setOpen(false); onClickAddBasket(); }, [onClickAddBasket]); return ( -
+
+
+ +
+ +
- -
- - - - + ); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.css b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.css index a32cdea53..56b09c4b1 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.css +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.css @@ -4,6 +4,10 @@ width: 100%; } +.vuuBasketSelectorRow:hover { + background-color: var(--vuu-color-gray-10); +} + .vuuBasketSelectorRow .vuuTableNextCell { display: grid; grid-template-columns: 1fr 1fr; @@ -16,8 +20,20 @@ .vuuBasketSelectorRow-name { font-size: 16px; } +.vuuBasketSelectorRow-status { + text-align: right; +} -.vuuBasketSelectorRow-symbol { +.vuuBasketSelectorRow-symbolContainer { + display: flex; + gap: 8px; +} + +.vuuBasketSelectorRow-symbolLabel { + color: var(--vuu-color-gray-50); +} +.vuuBasketSelectorRow-basketId { + color: var(--vuu-color-gray-80); display: flex; gap: 3px; } diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx index 02caada4f..a897f1121 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx @@ -15,11 +15,14 @@ export const BasketSelectorRow = ({ onClick, onDataEdited, onToggleGroup, - zebraStripes = false, + zebraStripes: _, ...htmlAttributes }: RowProps) => { - const name = row[columnMap.name]; - const symbolName = row[columnMap.symbolName]; + const { + [columnMap.basketId]: basketId, + [columnMap.basketName]: basketName, + [columnMap.status]: status, + } = row; const style = { transform: `translate3d(0px, ${offset}px, 0px)` }; const handleRowClick = useCallback( @@ -41,12 +44,14 @@ export const BasketSelectorRow = ({ style={style} >
- {name} - -
- - {symbolName} + {basketName} + +
+ + {basketId}
+ +
); diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts new file mode 100644 index 000000000..e1734e666 --- /dev/null +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts @@ -0,0 +1,141 @@ +import { TableProps, TableRowClickHandler } from "@finos/vuu-table"; +import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; +import { SubscribeCallback } from "@finos/vuu-data"; +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { OpenChangeHandler, useControlled } from "@finos/vuu-ui-controls"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { BasketSelectorProps } from "./BasketSelector"; +import { BasketSelectorRow } from "./BasketSelectorRow"; + +export class Basket { + basketId: string; + basketName: string; + fxRateToUsd: number; + + constructor(data: VuuDataRow, columnMap: ColumnMap) { + this.basketId = data[columnMap.basketId] as string; + this.basketName = data[columnMap.basketName] as string; + this.fxRateToUsd = data[columnMap.fxRateToUsd] as number; + } +} + +export type BasketSelectorHookProps = Pick< + BasketSelectorProps, + | "basketInstanceId" + | "dataSourceBasketTrading" + | "dataSourceBasketTradingSearch" + | "defaultIsOpen" + | "isOpen" + | "onOpenChange" +>; + +export const useBasketSelector = ({ + basketInstanceId: basketInstanceIdProp, + dataSourceBasketTrading, + dataSourceBasketTradingSearch, + defaultIsOpen, + isOpen: isOpenProp, + onOpenChange, +}: BasketSelectorHookProps) => { + const [isOpen, setIsOpen] = useControlled({ + controlled: isOpenProp, + default: defaultIsOpen ?? false, + name: "useDropdownList", + }); + + const [basketInstanceId, setBasketInstanceId] = useState( + basketInstanceIdProp + ); + const [basket, setBasket] = useState(); + + const columnMap = useMemo( + () => buildColumnMap(dataSourceBasketTrading.columns), + [dataSourceBasketTrading.columns] + ); + + const handleOpenChange = useCallback( + (open, closeReason) => { + setIsOpen(open); + onOpenChange?.(open, closeReason); + if (open === false) { + dataSourceBasketTradingSearch.unsubscribe(); + } + }, + [dataSourceBasketTradingSearch, onOpenChange, setIsOpen] + ); + + const handleRowClick = useCallback( + (row) => { + const instanceIdId = row[columnMap.instanceId] as string; + setBasketInstanceId(instanceIdId); + setIsOpen(false); + dataSourceBasketTrading.filter = { + filter: `instanceId = "${basketInstanceId}"`, + }; + }, + [basketInstanceId, columnMap.instanceId, dataSourceBasketTrading, setIsOpen] + ); + + const handleData = useCallback( + (message) => { + if (message.type === "viewport-update" && message.rows?.length === 1) { + setBasket(new Basket(message.rows[0], columnMap)); + } + }, + [columnMap] + ); + + useMemo(() => { + dataSourceBasketTrading.subscribe( + { + range: { from: 0, to: 1 }, + filter: { filter: `instanceId = "${basketInstanceId}"` }, + }, + handleData + ); + }, [dataSourceBasketTrading, basketInstanceId, handleData]); + + const tableProps: Partial = useMemo( + () => ({ + height: "auto", + Row: BasketSelectorRow, + config: { + columns: [ + { name: "instanceId", width: 365 }, + { name: "basketId", width: 100, hidden: true }, + { + name: "name", + width: 200, + hidden: true, + }, + { + name: "basketName", + width: 100, + type: { + name: "string", + }, + hidden: true, + }, + { + name: "status", + width: 100, + type: { + name: "string", + }, + hidden: true, + }, + ], + }, + onRowClick: handleRowClick, + rowHeight: 47, + }), + [handleRowClick] + ); + + return { + basket, + isOpen, + onOpenChange: handleOpenChange, + tableProps, + }; +}; 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 914a1ab19..0b739d355 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts @@ -7,17 +7,18 @@ import { VuuFilter } from "packages/vuu-protocol-types"; export type basketDataSourceKey = | "data-source-basket" | "data-source-basket-trading" + | "data-source-basket-trading-control" | "data-source-basket-trading-search" | "data-source-basket-trading-constituent" | "data-source-instruments"; export const useBasketTradingDataSources = ({ basketSchema, - basketTradingId, + basketInstanceId, basketTradingSchema, basketTradingConstituentSchema, instrumentsSchema, -}: BasketTradingFeatureProps & { basketTradingId: string }) => { +}: BasketTradingFeatureProps & { basketInstanceId: string }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const { id, loadSession, saveSession, title } = useViewContext(); @@ -25,16 +26,18 @@ export const useBasketTradingDataSources = ({ const [ dataSourceBasket, dataSourceBasketTrading, + dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituent, dataSourceInstruments, ] = useMemo(() => { const basketFilter: VuuFilter = { - filter: `instanceId = "${basketTradingId}"`, + filter: `instanceId = "${basketInstanceId}"`, }; const dataSourceConfig: [basketDataSourceKey, TableSchema, VuuFilter?][] = [ ["data-source-basket", basketSchema], ["data-source-basket-trading", basketTradingSchema, basketFilter], + ["data-source-basket-trading-control", basketTradingSchema], ["data-source-basket-trading-search", basketTradingSchema, basketFilter], [ "data-source-basket-trading-constituent", @@ -50,7 +53,7 @@ export const useBasketTradingDataSources = ({ let dataSource = loadSession?.(key) as RemoteDataSource; if (dataSource === undefined) { dataSource = new RemoteDataSource({ - bufferSize: 200, + bufferSize: 100, filter, viewport: `${id}-${key}`, table: schema.table, @@ -65,7 +68,7 @@ export const useBasketTradingDataSources = ({ }, [ basketSchema, basketTradingSchema, - basketTradingId, + basketInstanceId, basketTradingConstituentSchema, instrumentsSchema, loadSession, @@ -86,6 +89,7 @@ export const useBasketTradingDataSources = ({ activeTabIndex, dataSourceBasket, dataSourceBasketTrading, + dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituent, dataSourceInstruments, diff --git a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx index 6446a2ea4..7c5c87e7b 100644 --- a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx @@ -1,10 +1,11 @@ import { InstrumentPicker } from "@finos/vuu-ui-controls"; -import { getSchema } from "@finos/vuu-data-test"; +import { getAllSchemas, getSchema } from "@finos/vuu-data-test"; import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; import { useCallback, useMemo } from "react"; import { TableProps, TableRowSelectHandler } from "@finos/vuu-table"; import { createArrayDataSource } from "../utils/createArrayDataSource"; import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { useTestDataSource } from "../utils"; let displaySequence = 0; @@ -47,3 +48,53 @@ export const DefaultInstrumentPicker = () => { ); }; DefaultInstrumentPicker.displaySequence = displaySequence++; + +export const InstrumentPickerVuuInstruments = () => { + const schemas = getAllSchemas(); + const { dataSource, error } = useTestDataSource({ + // bufferSize: 1000, + schemas, + }); + + const columnMap = buildColumnMap(dataSource.columns); + + const [searchColumns, tableProps] = useMemo< + [string[], Pick] + >( + () => [ + ["bbg", "description"], + { + config: { + // TODO need to inject this value + showHighlightedRow: true, + columns: [ + { name: "bbg", serverDataType: "string" }, + { name: "description", serverDataType: "string", width: 280 }, + ] as ColumnDescriptor[], + }, + dataSource, + }, + ], + [dataSource] + ); + + const handleSelect = useCallback((row) => { + console.log(`row selected ${row.join(",")}`); + }, []); + + if (error) { + return error; + } + + return ( + + ); +}; +InstrumentPickerVuuInstruments.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/InstrumentSearch.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/InstrumentSearch.examples.tsx index 9903ae46f..ef61a7200 100644 --- a/vuu-ui/showcase/src/examples/UiControls/InstrumentSearch.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/InstrumentSearch.examples.tsx @@ -1,5 +1,6 @@ import { InstrumentSearch } from "@finos/vuu-ui-controls"; -import { useTableConfig } from "../utils"; +import { getAllSchemas } from "@finos/vuu-data-test"; +import { useTableConfig, useTestDataSource } from "../utils"; let displaySequence = 1; @@ -19,3 +20,25 @@ export const DefaultInstrumentSearch = () => { }; DefaultInstrumentSearch.displaySequence = displaySequence++; + +export const InstrumentSearchVuuInstruments = () => { + const schemas = getAllSchemas(); + const { dataSource, error } = useTestDataSource({ + // bufferSize: 1000, + schemas, + }); + + if (error) { + return error; + } + + return ( + + ); +}; + +InstrumentSearchVuuInstruments.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx b/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx index 1814234e3..9c596000e 100644 --- a/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx +++ b/vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx @@ -6,22 +6,20 @@ import { getSchema } from "@finos/vuu-data-test"; let displaySequence = 1; export const DefaultBasketSelector = () => { - const schema = getSchema("basketDefinitions"); + const schema = getSchema("basketTrading"); - const { dataSource: dataSourceBasket } = useTableConfig({ - count: 5, + const { dataSource: datasourceBasketTrading } = useTableConfig({ dataSourceConfig: { columns: schema.columns.map((col) => col.name), }, - table: { module: "SIMUL", table: "basketDefinitions" }, + table: { module: "BASKET", table: "basketTrading" }, }); - const { dataSource: datasourceBasketSearch } = useTableConfig({ - count: 5, + const { dataSource: datasourceBasketTradingSearch } = useTableConfig({ dataSourceConfig: { columns: schema.columns.map((col) => col.name), }, - table: { module: "SIMUL", table: "basketDefinitions" }, + table: { module: "BASKET", table: "basketTrading" }, }); const handleClickAddBasket = useCallback(() => { @@ -30,9 +28,9 @@ export const DefaultBasketSelector = () => { return ( );