From 308ee373564d1ca6e9f09bc5f8a5991835136144 Mon Sep 17 00:00:00 2001 From: heswell Date: Tue, 1 Oct 2024 19:36:21 +0100 Subject: [PATCH] Rest data source (#1510) * fix issue in ArrayDataSource not refreshing after resume * rest datasource experiment * add pagination * add TablePicker, RestDataSource first cut, fix table issues * initial search capability in TablePicker * fix table fixed size rendering --- .../array-data-source/array-data-source.ts | 23 +- .../vuu-data-react/src/data-editing/index.ts | 1 + .../RestDataSourceProvider.tsx | 25 +++ .../src/hooks/useSessionDataSource.ts | 8 +- vuu-ui/packages/vuu-data-remote/src/index.ts | 1 + .../src/rest-data/moving-window.ts | 81 +++++++ .../src/rest-data/rest-data-source.ts | 211 ++++++++++++++++++ .../src/rest-data/rest-utils.ts | 66 ++++++ .../src/server-proxy/viewport.ts | 13 +- .../vuu-data-test/src/simul/simul-schemas.ts | 32 +-- vuu-ui/packages/vuu-data-types/index.d.ts | 8 + vuu-ui/packages/vuu-table/src/Row.css | 6 +- vuu-ui/packages/vuu-table/src/Table.css | 29 +++ vuu-ui/packages/vuu-table/src/Table.tsx | 70 ++++-- vuu-ui/packages/vuu-table/src/index.ts | 1 + .../packages/vuu-table/src/moving-window.ts | 26 ++- .../src/useControlledTableNavigation.ts | 13 +- .../packages/vuu-table/src/useDataSource.ts | 21 +- .../vuu-table/src/useKeyboardNavigation.ts | 33 +-- .../vuu-table/src/useMeasuredHeight.ts | 2 +- .../packages/vuu-table/src/usePagination.ts | 49 ++++ vuu-ui/packages/vuu-table/src/useTable.ts | 12 +- .../packages/vuu-table/src/useTableScroll.ts | 2 +- .../vuu-table/src/useTableViewport.ts | 61 +++-- .../instrument-picker/InstrumentPicker.css | 19 -- .../instrument-picker/InstrumentPicker.tsx | 129 ----------- .../src/instrument-picker/TablePicker.css | 14 ++ .../src/instrument-picker/TablePicker.tsx | 139 ++++++++++++ .../src/instrument-picker/index.ts | 2 +- .../instrument-picker/useInstrumentPicker.ts | 110 --------- .../src/instrument-picker/useTablePicker.ts | 207 +++++++++++++++++ .../useMeasuredContainer.ts | 46 ++-- vuu-ui/packages/vuu-utils/src/column-utils.ts | 14 +- .../packages/vuu-utils/src/feature-utils.ts | 46 ++-- .../sample-apps/app-vuu-example/src/App.tsx | 1 + .../src/new-basket-panel/NewBasketPanel.tsx | 40 +--- .../src/new-basket-panel/useNewBasketPanel.ts | 23 +- .../src/useBasketTrading.tsx | 9 +- .../src/useBasketTradingDatasources.ts | 5 - .../src/examples/Table/Paging.examples.tsx | 77 +++++++ .../src/examples/Table/SIMUL.examples.tsx | 6 +- .../src/examples/Table/Table.examples.tsx | 53 +++-- .../examples/Table/TableLayout.examples.tsx | 2 +- .../TableVuuLayoutCombinations.examples.tsx | 53 +++++ vuu-ui/showcase/src/examples/Table/index.ts | 2 + .../UiControls/InstrumentPicker.examples.tsx | 116 ---------- .../UiControls/TablePicker.examples.tsx | 72 ++++++ .../showcase/src/examples/UiControls/index.ts | 2 +- .../VuuFeatures/NewBasketPanel.examples.tsx | 35 ++- .../src/examples/salt/Pagination.examples.tsx | 29 +++ vuu-ui/showcase/src/examples/salt/index.ts | 1 + 51 files changed, 1447 insertions(+), 599 deletions(-) create mode 100644 vuu-ui/packages/vuu-data-react/src/datasource-provider/RestDataSourceProvider.tsx create mode 100644 vuu-ui/packages/vuu-data-remote/src/rest-data/moving-window.ts create mode 100644 vuu-ui/packages/vuu-data-remote/src/rest-data/rest-data-source.ts create mode 100644 vuu-ui/packages/vuu-data-remote/src/rest-data/rest-utils.ts create mode 100644 vuu-ui/packages/vuu-table/src/usePagination.ts delete mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.css delete mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.tsx delete mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useTablePicker.ts create mode 100644 vuu-ui/showcase/src/examples/Table/Paging.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/Table/TableVuuLayoutCombinations.examples.tsx delete mode 100644 vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/UiControls/TablePicker.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/salt/Pagination.examples.tsx diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts index 1aeacbdad..6ed080fee 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts @@ -122,7 +122,6 @@ export class ArrayDataSource #links: LinkDescriptorWithLabel[] | undefined; #range: VuuRange = NULL_RANGE; #selectedRowsCount = 0; - #size = 0; #status: DataSourceStatus = "initialising"; #title: string | undefined; @@ -163,7 +162,6 @@ export class ArrayDataSource this.rangeChangeRowset = rangeChangeRowset; this.tableSchema = buildTableSchema(columnDescriptors, keyColumn); this.viewport = viewport || uuid(); - this.#size = data.length; this.#title = title; const columns = columnDescriptors.map((col) => col.name); @@ -243,6 +241,13 @@ export class ArrayDataSource } else if (this.#range !== NULL_RANGE) { this.sendRowsToClient(); } + + if (this.range.to !== 0) { + const pageCount = Math.ceil( + this.size / (this.range.to - this.range.from), + ); + this.emit("page-count", pageCount); + } } } @@ -259,13 +264,19 @@ export class ArrayDataSource } } - resume() { + resume(callback?: SubscribeCallback) { const isSuspended = this.#status === "suspended"; info?.(`resume #${this.viewport}, current status ${this.#status}`); + if (callback) { + this.clientCallback = callback; + } + if (isSuspended) { this.#status = "subscribed"; } this.emit("resumed", this.viewport); + + this.sendRowsToClient(true); } disable() { @@ -305,6 +316,10 @@ export class ArrayDataSource } } + get pageSize() { + return this.#range.to - this.#range.from; + } + get links() { return this.#links; } @@ -472,7 +487,6 @@ export class ArrayDataSource } get size() { - // return this.#size; return this.processedData?.length ?? this.#data.length; } @@ -605,6 +619,7 @@ export class ArrayDataSource this.clientCallback?.({ clientViewportId: this.viewport, mode: "batch", + range: this.#range, rows: rowsWithinViewport, size: data.length, type: "viewport-update", diff --git a/vuu-ui/packages/vuu-data-react/src/data-editing/index.ts b/vuu-ui/packages/vuu-data-react/src/data-editing/index.ts index fdec645cc..4480e5109 100644 --- a/vuu-ui/packages/vuu-data-react/src/data-editing/index.ts +++ b/vuu-ui/packages/vuu-data-react/src/data-editing/index.ts @@ -2,3 +2,4 @@ export * from "./get-data-item-edit-control"; export * from "./EditForm"; export * from "./edit-rule-validation-checker"; export * from "./UnsavedChangesReport"; +export * from "./useEditForm"; diff --git a/vuu-ui/packages/vuu-data-react/src/datasource-provider/RestDataSourceProvider.tsx b/vuu-ui/packages/vuu-data-react/src/datasource-provider/RestDataSourceProvider.tsx new file mode 100644 index 000000000..b926b23fd --- /dev/null +++ b/vuu-ui/packages/vuu-data-react/src/datasource-provider/RestDataSourceProvider.tsx @@ -0,0 +1,25 @@ +import { RestDataSource, ConnectionManager } from "@finos/vuu-data-remote"; +import { DataSourceProvider } from "@finos/vuu-utils"; +import { ReactNode } from "react"; + +const getServerAPI = () => ConnectionManager.serverAPI; + +export const RestDataSourceProvider = ({ + children, + url, +}: { + children: ReactNode; + url: string; +}) => { + // url is a static property + RestDataSource.url = url; + return ( + + {children} + + ); +}; diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useSessionDataSource.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useSessionDataSource.ts index 799e4c5c0..2f46ea348 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useSessionDataSource.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useSessionDataSource.ts @@ -1,7 +1,7 @@ import { DataSource, DataSourceConfig, - TableSchema, + TableSchema } from "@finos/vuu-data-types"; import { isConfigChanged, resetRange, useDataSource } from "@finos/vuu-utils"; import { useViewContext } from "@finos/vuu-layout"; @@ -15,7 +15,7 @@ const NO_CONFIG: SessionDataSourceConfig = {}; export const useSessionDataSource = ({ dataSourceSessionKey = "data-source", - tableSchema, + tableSchema }: { dataSourceSessionKey?: string; tableSchema: TableSchema; @@ -70,7 +70,7 @@ export const useSessionDataSource = ({ table: tableSchema.table, ...dataSourceConfigFromState, columns, - title, + title }); ds.on("config", handleDataSourceConfigChange); saveSession?.(ds, "data-source"); @@ -85,7 +85,7 @@ export const useSessionDataSource = ({ saveSession, tableSchema.columns, tableSchema.table, - title, + title ]); return dataSource; diff --git a/vuu-ui/packages/vuu-data-remote/src/index.ts b/vuu-ui/packages/vuu-data-remote/src/index.ts index c05716877..6220a240b 100644 --- a/vuu-ui/packages/vuu-data-remote/src/index.ts +++ b/vuu-ui/packages/vuu-data-remote/src/index.ts @@ -4,5 +4,6 @@ export { default as ConnectionManager } from "./ConnectionManager"; export * from "./constants"; export * from "./data-source"; export * from "./message-utils"; +export * from "./rest-data/rest-data-source"; export * from "./vuu-data-source"; export type { WebSocketConnectionState } from "./WebSocketConnection"; diff --git a/vuu-ui/packages/vuu-data-remote/src/rest-data/moving-window.ts b/vuu-ui/packages/vuu-data-remote/src/rest-data/moving-window.ts new file mode 100644 index 000000000..76eb46ce7 --- /dev/null +++ b/vuu-ui/packages/vuu-data-remote/src/rest-data/moving-window.ts @@ -0,0 +1,81 @@ +import { DataSourceRow } from "@finos/vuu-data-types"; +import { isRowSelectedLast, metadataKeys, WindowRange } from "@finos/vuu-utils"; +import { VuuRange } from "@finos/vuu-protocol-types"; + +const { SELECTED } = metadataKeys; + +export class MovingWindow { + public data: DataSourceRow[]; + public rowCount = 0; + private range: WindowRange; + + constructor({ from, to }: VuuRange) { + this.range = new WindowRange(from, to); + //internal data is always 0 based, we add range.from to determine an offset + this.data = new Array(Math.max(0, to - from)); + this.rowCount = 0; + } + + setRowCount = (rowCount: number) => { + if (rowCount < this.data.length) { + this.data.length = rowCount; + } + + this.rowCount = rowCount; + }; + + add(data: DataSourceRow) { + const [index] = data; + 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; + } + } + } + } + + getAtIndex(index: number) { + return this.range.isWithin(index) && + this.data[index - this.range.from] != null + ? this.data[index - this.range.from] + : undefined; + } + + isWithinRange(index: number) { + return this.range.isWithin(index); + } + + setRange({ from, to }: VuuRange) { + if (from !== this.range.from || to !== this.range.to) { + const [overlapFrom, overlapTo] = this.range.overlap(from, to); + const newData = new Array(Math.max(0, to - from)); + for (let i = overlapFrom; i < overlapTo; i++) { + const data = this.getAtIndex(i); + if (data) { + const index = i - from; + newData[index] = data; + } + } + this.data = newData; + this.range.from = from; + this.range.to = to; + } + } + + getSelectedRows() { + return this.data.filter((row) => row[SELECTED] !== 0); + } +} diff --git a/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-data-source.ts b/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-data-source.ts new file mode 100644 index 000000000..1136acd82 --- /dev/null +++ b/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-data-source.ts @@ -0,0 +1,211 @@ +import { + DataSource, + DataSourceConstructorProps, + DataSourceEditHandler, + DataSourceEvents, + DataSourceFilter, + DataSourceRow, + DataSourceStatus, + SubscribeCallback, + SubscribeProps, + WithFullConfig, +} from "@finos/vuu-data-types"; +import { + VuuAggregation, + VuuTable, + VuuGroupBy, + VuuRange, + VuuSort, +} from "@finos/vuu-protocol-types"; +import { + ColumnMap, + EventEmitter, + NO_CONFIG_CHANGES, + NULL_RANGE, + buildColumnMap, + uuid, + vanillaConfig, +} from "@finos/vuu-utils"; +import { NDJsonReader, jsonToDataSourceRow } from "./rest-utils"; +import { MovingWindow } from "./moving-window"; + +export class RestDataSource + extends EventEmitter + implements DataSource +{ + private static _url = "/api"; + + private clientCallback: SubscribeCallback | undefined; + #columnMap: ColumnMap = buildColumnMap([ + "bbg", + "currency", + "description", + "exchange", + "ric", + "lotSize", + ]); + #config: WithFullConfig = vanillaConfig; + #data: DataSourceRow[] = []; + #dataWindow = new MovingWindow(NULL_RANGE); + #range: VuuRange = NULL_RANGE; + #title: string | undefined; + + aggregations: VuuAggregation[] = []; + filter: DataSourceFilter = { filter: "" }; + groupBy: VuuGroupBy = []; + selectedRowsCount = 0; + size = 0; + sort: VuuSort = { sortDefs: [] }; + status: DataSourceStatus = "initialising"; + table: VuuTable; + + viewport: string; + + constructor({ + table, + title, + viewport = uuid(), + }: DataSourceConstructorProps & { + url?: string; + }) { + super(); + + if (!table) + throw Error("RemoteDataSource constructor called without table"); + + this.table = table; + this.viewport = viewport; + + this.#config = { + ...this.#config, + columns: [ + "bbg", + "currency", + "description", + "exchange", + "ric", + "isin", + "lotSize", + ], + }; + this.#title = title; + } + + static get url() { + return this._url; + } + static set url(url: string) { + this._url = url; + } + + get title() { + return this.#title ?? `${this.table.module} ${this.table.table}`; + } + + set title(title: string) { + this.#title = title; + } + + async subscribe( + { range, ...props }: SubscribeProps, + callback: SubscribeCallback, + ) { + if (range) { + this.range = range; + } + + console.log(`subscribe ${JSON.stringify(props, null, 2)}`); + this.clientCallback = callback; + + this.fetchData(); + } + + unsubscribe() { + console.log("unsubscribe"); + } + + get columns() { + return this.#config.columns; + } + + get config() { + return this.#config; + } + + get range() { + return this.#range; + } + + set range(range: VuuRange) { + console.log(`set range ${JSON.stringify(range)}`); + if (range.from !== this.#range.from || range.to !== this.#range.to) { + this.#range = range; + this.#dataWindow.setRange(range); + this.sendRowsToClient(); + } + } + + private fetchData() { + const start = performance.now(); + const allDone = () => { + const end = performance.now(); + console.log( + `processing ${this.#dataWindow.data.length} rows took ${end - start}ms`, + ); + this.clientCallback?.({ + clientViewportId: this.viewport, + mode: "update", + rows: this.#dataWindow.data, + size: 200000, + type: "viewport-update", + }); + }; + + console.log(`base ${RestDataSource.url}`); + + const url = `${RestDataSource.url}/${this.table.table}`; + // const summaryUrl = `${url}/summary`; + + fetch(url, { + mode: "cors", + }).then( + NDJsonReader( + this.#range.from, + (index, json) => + this.#dataWindow.add( + jsonToDataSourceRow(index, json, this.#columnMap), + ), + allDone, + ), + ); + } + + private sendRowsToClient() { + console.log(`send rows to client`); + } + + applyEdit: DataSourceEditHandler = async () => { + return "Method not implemented"; + }; + + applyConfig = () => NO_CONFIG_CHANGES; + + openTreeNode = () => { + throw new Error("openTreeNode, Method not implemented."); + }; + closeTreeNode = () => { + throw new Error("closeTreeNode, Method not implemented."); + }; + + remoteProcedureCall = async () => "Method not implemented" as T; + menuRpcCall = async () => { + return "Method not supported"; + }; + rpcCall = async () => { + return "Method not supported" as T; + }; + + select = () => { + throw new Error("remoteProcedureCall, Method not implemented."); + }; +} diff --git a/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-utils.ts b/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-utils.ts new file mode 100644 index 000000000..205484aa6 --- /dev/null +++ b/vuu-ui/packages/vuu-data-remote/src/rest-data/rest-utils.ts @@ -0,0 +1,66 @@ +import { DataSourceRow } from "@finos/vuu-data-types"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; + +export type JsonPrimitive = string | number | boolean | null; +export type JsonArray = Json[]; +export type JsonObject = { [key: string]: Json }; +export type Json = JsonPrimitive | JsonArray | JsonObject; +export type JsonHandler = (rowIndex: number, json: JsonObject) => void; + +export const NDJsonReader = + (startIndex: number, jsonHandler: JsonHandler, onEnd: () => void) => + (response: Response) => { + if (response.ok && response.body) { + const stream = response.body.getReader(); + const decoder = new TextDecoder(); + const matcher = /\r?\n/; + let buf = ""; + let index = startIndex; + + const loop: () => void = () => + stream.read().then(({ done, value }) => { + if (done) { + if (buf.length > 0) jsonHandler(index, JSON.parse(buf)); + onEnd(); + } else { + const chunk = decoder.decode(value, { + stream: true + }); + buf += chunk; + + const jsonFragments = buf.split(matcher); + buf = jsonFragments.pop() ?? ""; + for (const jsonFragment of jsonFragments) { + jsonHandler(index, JSON.parse(jsonFragment)); + index += 1; + } + return loop(); + } + }); + return loop(); + } else { + throw Error(`response invalid ${response.status} ${response.statusText}`); + } + }; + +export const jsonToDataSourceRow = ( + rowIndex: number, + json: JsonObject, + columnMap: ColumnMap +): DataSourceRow => { + const dataSourceRow: DataSourceRow = [ + rowIndex, + rowIndex, + true, + false, + 0, + 0, + json.ric as string, + 0 + ]; + for (const [column, colIdx] of Object.entries(columnMap)) { + dataSourceRow[colIdx] = json[column] as VuuRowDataItemType; + } + return dataSourceRow; +}; diff --git a/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts index fd3a0b792..ae7816428 100644 --- a/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts @@ -171,8 +171,9 @@ export class Viewport { /** * clientRange is always the range requested by the client. We should assume * these are the rows visible to the user + * TODO what is clientRange needed for ? */ - private clientRange: VuuRange; + #clientRange: VuuRange; private columns: string[]; private dataWindow: ArrayBackedMovingWindow; private filter: DataSourceFilter; @@ -231,7 +232,7 @@ export class Viewport { ) { this.aggregations = aggregations; this.bufferSize = bufferSize; - this.clientRange = range; + this.#clientRange = range; this.clientViewportId = viewport; this.columns = columns; this.filter = filter; @@ -246,7 +247,7 @@ export class Viewport { `constructor #${viewport} ${table.table} bufferSize=${bufferSize}`, ); this.dataWindow = new ArrayBackedMovingWindow( - this.clientRange, + this.#clientRange, range, this.bufferSize, ); @@ -289,6 +290,10 @@ export class Viewport { return this.dataWindow.rowCount ?? 0; } + get clientRange() { + return this.#clientRange; + } + get status() { return this.#status; } @@ -304,7 +309,7 @@ export class Viewport { return { type: Message.CREATE_VP, table: this.table, - range: getFullRange(this.clientRange, this.bufferSize), + range: getFullRange(this.#clientRange, this.bufferSize), aggregations: this.aggregations, columns: this.columns, sort: this.sort, 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 index b88b1021c..2774a8452 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts @@ -23,11 +23,9 @@ export const schemas: Readonly>> = { name: "isin", serverDataType: "string" }, { name: "lotSize", serverDataType: "int" }, { name: "ric", serverDataType: "string" }, - { name: "price", serverDataType: "double" }, - { name: "date", serverDataType: "long" } ], key: "ric", - table: { module: "SIMUL", table: "instruments" } + table: { module: "SIMUL", table: "instruments" }, }, instrumentsExtended: { columns: [ @@ -40,10 +38,12 @@ export const schemas: Readonly>> = { name: "ric", serverDataType: "string" }, { name: "supported", serverDataType: "boolean" }, { name: "wishlist", serverDataType: "boolean" }, - { name: "lastUpdated", serverDataType: "long" } + { name: "lastUpdated", serverDataType: "long" }, + { name: "price", serverDataType: "double" }, + { name: "date", serverDataType: "long" }, ], key: "ric", - table: { module: "SIMUL", table: "instrumentsExtended" } + table: { module: "SIMUL", table: "instrumentsExtended" }, }, instrumentPrices: { columns: [ @@ -62,10 +62,10 @@ export const schemas: Readonly>> = { name: "open", serverDataType: "double" }, { name: "phase", serverDataType: "string" }, { name: "ric", serverDataType: "string" }, - { name: "scenario", serverDataType: "string" } + { name: "scenario", serverDataType: "string" }, ], key: "ric", - table: { module: "SIMUL", table: "instrumentPrices" } + table: { module: "SIMUL", table: "instrumentPrices" }, }, orders: { columns: [ @@ -78,10 +78,10 @@ export const schemas: Readonly>> = { name: "quantity", serverDataType: "double" }, { name: "ric", serverDataType: "string" }, { name: "side", serverDataType: "string" }, - { name: "trader", serverDataType: "string" } + { name: "trader", serverDataType: "string" }, ], key: "orderId", - table: { module: "SIMUL", table: "orders" } + table: { module: "SIMUL", table: "orders" }, }, childOrders: { columns: [ @@ -101,10 +101,10 @@ export const schemas: Readonly>> = { name: "side", serverDataType: "string" }, { name: "status", serverDataType: "string" }, { name: "strategy", serverDataType: "string" }, - { name: "volLimit", serverDataType: "int" } + { name: "volLimit", serverDataType: "int" }, ], key: "id", - table: { module: "SIMUL", table: "childOrders" } + table: { module: "SIMUL", table: "childOrders" }, }, parentOrders: { columns: [ @@ -124,10 +124,10 @@ export const schemas: Readonly>> = { name: "ric", serverDataType: "string" }, { name: "side", serverDataType: "string" }, { name: "status", serverDataType: "string" }, - { name: "volLimit", serverDataType: "int" } + { name: "volLimit", serverDataType: "int" }, ], key: "id", - table: { module: "SIMUL", table: "parentOrders" } + table: { module: "SIMUL", table: "parentOrders" }, }, prices: { columns: [ @@ -140,11 +140,11 @@ export const schemas: Readonly>> = { name: "open", serverDataType: "double" }, { name: "phase", serverDataType: "string" }, { name: "ric", serverDataType: "string" }, - { name: "scenario", serverDataType: "string" } + { name: "scenario", serverDataType: "string" }, ], key: "ric", - table: { module: "SIMUL", table: "prices" } - } + table: { module: "SIMUL", table: "prices" }, + }, }; export type SimulVuuTable = { diff --git a/vuu-ui/packages/vuu-data-types/index.d.ts b/vuu-ui/packages/vuu-data-types/index.d.ts index 2642a2626..5814f088d 100644 --- a/vuu-ui/packages/vuu-data-types/index.d.ts +++ b/vuu-ui/packages/vuu-data-types/index.d.ts @@ -223,6 +223,12 @@ export interface DataSourceClearMessage extends MessageWithClientViewportId { } export interface DataSourceDataMessage extends MessageWithClientViewportId { mode: DataUpdateMode; + /** + * this is needed by the ArrayDataSource, biut not currently used by VuuDataSource. + * Suspect it will be valuable in any DtaSOurce and should eventually be made a + * required field. + */ + range?: VuuRange; rows?: DataSourceRow[]; size?: number; type: "viewport-update"; @@ -454,6 +460,7 @@ export type DataSourceEvents = { configChanges?: DataSourceConfigChanges, ) => void; optimize: (optimize: OptimizeStrategy) => void; + "page-count": (pageCount: number) => void; range: (range: VuuRange) => void; resize: (size: number) => void; "row-selection": RowSelectionEventHandler; @@ -546,6 +553,7 @@ export interface DataSource closeTreeNode: (key: string, cascade?: boolean) => void; columns: string[]; config: DataSourceConfig; + status: DataSourceStatus; /** * diff --git a/vuu-ui/packages/vuu-table/src/Row.css b/vuu-ui/packages/vuu-table/src/Row.css index 2c8d51469..943c77a46 100644 --- a/vuu-ui/packages/vuu-table/src/Row.css +++ b/vuu-ui/packages/vuu-table/src/Row.css @@ -1,13 +1,17 @@ .vuuTableRow { background: var(--row-background, var(--table-background)); - color: var(--salt-content-secondary-foreground); border-bottom: 1px solid var(--row-borderColor, var(--table-background)); box-sizing: border-box; + color: var(--salt-content-secondary-foreground); + contain: strict; + contain-intrinsic-height: var(--row-height); + content-visibility: auto; height: var(--row-height); line-height: var(--row-height); position: absolute; top: 0; white-space: nowrap; + width: 100%; } .vuuTableRow-proxy { diff --git a/vuu-ui/packages/vuu-table/src/Table.css b/vuu-ui/packages/vuu-table/src/Table.css index c4796ead5..f6ae74efb 100644 --- a/vuu-ui/packages/vuu-table/src/Table.css +++ b/vuu-ui/packages/vuu-table/src/Table.css @@ -1,4 +1,5 @@ .vuuTable { + --vuu-table-pagination-height: 0px; --vuu-table-cell-outlineWidth: 1px; --table-height: var(--measured-px-height); --table-width: var(--measured-px-width); @@ -168,3 +169,31 @@ .vuuDraggable-vuuTable { --row-height: 25px; } + +.vuuTable-pagination { + --vuu-table-pagination-height: 32px; + .vuuTable-col-headings { + position: relative; + } + + .vuuTable-body { + height: calc(var(--content-height) - var(--total-header-height)); + position: relative; + } + + .vuuTableRow { + position: relative; + top: auto; + } +} + +.vuuTable-pagination-container { + align-items: center; + display: flex; + height: var(--vuu-table-pagination-height); + justify-content: flex-end; + position: absolute; + left: 0; + right: 0; + bottom: 0; +} diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index f1234fce2..b184b5132 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -20,9 +20,10 @@ import { MeasuredContainerProps, MeasuredSize, dragStrategy, + reduceSizeHeight, } from "@finos/vuu-ui-controls"; import { metadataKeys, useId } from "@finos/vuu-utils"; -import { useForkRef } from "@salt-ds/core"; +import { GoToInput, Pagination, Paginator, useForkRef } from "@salt-ds/core"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import cx from "clsx"; @@ -42,6 +43,7 @@ import { useTable } from "./useTable"; import { ScrollingAPI } from "./useTableScroll"; import tableCss from "./Table.css"; +import { usePagination } from "./usePagination"; const classBase = "vuuTable"; @@ -143,6 +145,10 @@ export interface TableProps * from contexct menu */ showColumnHeaderMenus?: boolean; + /** + * if true, pagination will be used to navigate data, scrollbars will not be rendered + */ + showPaginationControls?: boolean; } const TableCore = ({ @@ -166,12 +172,13 @@ const TableCore = ({ onRowClick: onRowClickProp, onSelect, onSelectionChange, - renderBufferSize = 5, + renderBufferSize = 0, rowHeight, scrollingApiRef, selectionModel = "extended", showColumnHeaders = true, showColumnHeaderMenus = true, + showPaginationControls, size, }: Omit & { containerRef: RefObject; @@ -223,10 +230,12 @@ const TableCore = ({ onRowClick: onRowClickProp, onSelect, onSelectionChange, - renderBufferSize: Math.max(5, renderBufferSize), + renderBufferSize, rowHeight, scrollingApiRef, selectionModel, + showColumnHeaders, + showPaginationControls, size, }); @@ -252,13 +261,15 @@ const TableCore = ({ menuActionHandler={handleContextMenuAction} menuBuilder={menuBuilder} > -
-
-
+ {showPaginationControls !== true ? ( +
+
+
+ ) : null}
, @@ -359,8 +373,9 @@ export const Table = forwardRef(function Table( const containerRef = useRef(null); const [size, setSize] = useState(); - + // TODO this will rerender entire table, move foter into seperate component const { rowHeight, rowRef } = useMeasuredHeight({ height: rowHeightProp }); + const { rowHeight: footerHeight, rowRef: footerRef } = useMeasuredHeight({}); if (config === undefined) { throw Error( @@ -371,12 +386,20 @@ export const Table = forwardRef(function Table( throw Error("vuu Table requires dataSource prop"); } + const { onPageChange, pageCount } = usePagination({ + dataSource, + showPaginationControls, + }); + // TODO render TableHeader here and measure before row construction begins // TODO we could have MeasuredContainer render a Provider and make size available via a context hook ? return ( 0 ? `${rowHeight}px` : undefined, } as CSSProperties } + width={width} > - - {size && rowHeight ? ( + {size && rowHeight && (footerHeight || showColumnHeaders !== true) ? ( ) : null} + {showPaginationControls ? ( +
+ + + + +
+ ) : null}
); }); diff --git a/vuu-ui/packages/vuu-table/src/index.ts b/vuu-ui/packages/vuu-table/src/index.ts index 0c4d3c3ff..c39ed39b0 100644 --- a/vuu-ui/packages/vuu-table/src/index.ts +++ b/vuu-ui/packages/vuu-table/src/index.ts @@ -7,6 +7,7 @@ export * from "./table-cell"; export * from "./table-config"; export * from "./table-header"; export * from "./useControlledTableNavigation"; +export * from "./useKeyboardNavigation"; export * from "./useTableModel"; export * from "./useTableScroll"; export * from "./useTableViewport"; diff --git a/vuu-ui/packages/vuu-table/src/moving-window.ts b/vuu-ui/packages/vuu-table/src/moving-window.ts index 76eb46ce7..098b1ca26 100644 --- a/vuu-ui/packages/vuu-table/src/moving-window.ts +++ b/vuu-ui/packages/vuu-table/src/moving-window.ts @@ -7,10 +7,10 @@ const { SELECTED } = metadataKeys; export class MovingWindow { public data: DataSourceRow[]; public rowCount = 0; - private range: WindowRange; + #range: WindowRange; constructor({ from, to }: VuuRange) { - this.range = new WindowRange(from, to); + this.#range = new WindowRange(from, to); //internal data is always 0 based, we add range.from to determine an offset this.data = new Array(Math.max(0, to - from)); this.rowCount = 0; @@ -27,7 +27,7 @@ export class MovingWindow { add(data: DataSourceRow) { const [index] = data; if (this.isWithinRange(index)) { - const internalIndex = index - this.range.from; + const internalIndex = index - this.#range.from; this.data[internalIndex] = data; // Hack until we can deal with this more elegantly. When we have a block @@ -48,19 +48,19 @@ export class MovingWindow { } getAtIndex(index: number) { - return this.range.isWithin(index) && - this.data[index - this.range.from] != null - ? this.data[index - this.range.from] + return this.#range.isWithin(index) && + this.data[index - this.#range.from] != null + ? this.data[index - this.#range.from] : undefined; } isWithinRange(index: number) { - return this.range.isWithin(index); + return this.#range.isWithin(index); } setRange({ from, to }: VuuRange) { - if (from !== this.range.from || to !== this.range.to) { - const [overlapFrom, overlapTo] = this.range.overlap(from, to); + if (from !== this.#range.from || to !== this.#range.to) { + const [overlapFrom, overlapTo] = this.#range.overlap(from, to); const newData = new Array(Math.max(0, to - from)); for (let i = overlapFrom; i < overlapTo; i++) { const data = this.getAtIndex(i); @@ -70,12 +70,16 @@ export class MovingWindow { } } this.data = newData; - this.range.from = from; - this.range.to = to; + this.#range.from = from; + this.#range.to = to; } } getSelectedRows() { return this.data.filter((row) => row[SELECTED] !== 0); } + + get range() { + return this.#range; + } } diff --git a/vuu-ui/packages/vuu-table/src/useControlledTableNavigation.ts b/vuu-ui/packages/vuu-table/src/useControlledTableNavigation.ts index 8ac1cc0c8..929fc8124 100644 --- a/vuu-ui/packages/vuu-table/src/useControlledTableNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useControlledTableNavigation.ts @@ -2,9 +2,12 @@ import { useStateRef } from "@finos/vuu-ui-controls"; import { dispatchMouseEvent } from "@finos/vuu-utils"; import { KeyboardEventHandler, useCallback, useRef } from "react"; +export const isRowSelectionKey = (key: string) => + key === "Enter" || key === " "; + export const useControlledTableNavigation = ( initialValue: number, - rowCount: number + rowCount: number, ) => { const tableRef = useRef(null); @@ -18,12 +21,12 @@ export const useControlledTableNavigation = ( setHighlightedIndex((index = -1) => Math.min(rowCount - 1, index + 1)); } else if (e.key === "ArrowUp") { setHighlightedIndex((index = -1) => Math.max(0, index - 1)); - } else if (e.key === "Enter" || e.key === " ") { + } else if (isRowSelectionKey(e.key)) { const { current: rowIdx } = highlightedIndexRef; // induce an onSelect event by 'clicking' the row if (typeof rowIdx === "number") { const rowEl = tableRef.current?.querySelector( - `[aria-rowindex="${rowIdx + 1}"]` + `[aria-rowindex="${rowIdx + 1}"]`, ) as HTMLElement; if (rowEl) { dispatchMouseEvent(rowEl, "click"); @@ -31,14 +34,14 @@ export const useControlledTableNavigation = ( } } }, - [highlightedIndexRef, rowCount, setHighlightedIndex] + [highlightedIndexRef, rowCount, setHighlightedIndex], ); const handleHighlight = useCallback( (idx: number) => { setHighlightedIndex(idx); }, - [setHighlightedIndex] + [setHighlightedIndex], ); return { diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index 40e5e59c8..ccb7c0703 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -34,6 +34,20 @@ export const useDataSource = ({ [], ); + useMemo(() => { + dataSource.on("resumed", () => { + // When we resume a dataSource (after switching tabs etc) + // client will receive rows. We may not have received any + // setRange calls at this point so dataWindow range will + //not yet be set. If the dataWindow range is already set, + // this is a no-op. + const { range } = dataSource; + if (range.to !== 0) { + dataWindow.setRange(dataSource.range); + } + }); + }, [dataSource, dataWindow]); + const setData = useCallback( (updates: DataSourceRow[]) => { for (const row of updates) { @@ -62,6 +76,11 @@ export const useDataSource = ({ } } if (message.rows) { + if (message.range) { + if (message.range.to !== dataWindow.range.to) { + dataWindow.setRange(message.range); + } + } setData(message.rows); } else if (message.size === 0) { setData([]); @@ -117,7 +136,7 @@ export const useDataSource = ({ // This isn't great, we're using the dataSource as a conduit to emit a // message that has nothing to do with the dataSource itself. Client // is the DataSourceState component. - // WHY CANT THIS BE DONE WITHIN DataSOurce ? + // WHY CANT THIS BE DONE WITHIN DataSource ? dataSource.emit("range", range); } }, diff --git a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts index 95ecc1f5c..42751da7a 100644 --- a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts @@ -35,7 +35,7 @@ cellNavigationKeys.add("ArrowRight"); export const isNavigationKey = ( key: string, - navigationStyle: TableNavigationStyle + navigationStyle: TableNavigationStyle, ): key is NavigationKey => { switch (navigationStyle) { case "cell": @@ -61,7 +61,7 @@ function nextCellPos( key: ArrowKey, [rowIdx, colIdx]: CellPos, columnCount: number, - rowCount: number + rowCount: number, ): CellPos { if (key === "ArrowUp") { if (rowIdx > -1) { @@ -150,12 +150,12 @@ NavigationHookProps) => { // lastFocus.current = idx; } }, - [onHighlight, setHighlightedIdx] + [onHighlight, setHighlightedIdx], ); const getFocusedCell = (element: HTMLElement | Element | null) => element?.closest( - "[role='columnHeader'],[role='cell']" + "[role='columnHeader'],[role='cell']", ) as HTMLDivElement | null; const getTableCellPos = (tableCell: HTMLDivElement): CellPos => { @@ -192,7 +192,7 @@ NavigationHookProps) => { }, // TODO we recreate this function whenever viewportRange changes, which will // be often whilst scrolling - store range in a a ref ? - [containerRef, requestScroll] + [containerRef, requestScroll], ); const setActiveCell = useCallback( @@ -208,13 +208,13 @@ NavigationHookProps) => { focusedCellPos.current = pos; } }, - [focusCell, navigationStyle, setHighlightedIdx] + [focusCell, navigationStyle, setHighlightedIdx], ); const nextPageItemIdx = useCallback( ( key: "PageDown" | "PageUp" | "Home" | "End", - [rowIdx, colIdx]: CellPos + [rowIdx, colIdx]: CellPos, ): Promise => new Promise((resolve) => { let newRowIdx = rowIdx; @@ -262,7 +262,7 @@ NavigationHookProps) => { resolve([newRowIdx, colIdx]); }, 35); }), - [requestScroll, rowCount, viewportRowCount] + [requestScroll, rowCount, viewportRowCount], ); const handleFocus = useCallback(() => { @@ -298,14 +298,14 @@ NavigationHookProps) => { setActiveCell(nextRowIdx, nextColIdx, true); } }, - [columnCount, nextPageItemIdx, rowCount, setActiveCell] + [columnCount, nextPageItemIdx, rowCount, setActiveCell], ); const scrollRowIntoViewIfNecessary = useCallback( (rowIndex: number) => { requestScroll?.({ type: "scroll-row", rowIndex }); }, - [requestScroll] + [requestScroll], ); const moveHighlightedRow = useCallback( @@ -326,12 +326,15 @@ NavigationHookProps) => { rowCount, scrollRowIntoViewIfNecessary, setHighlightedIndex, - ] + ], ); useEffect(() => { if (highlightedIndexProp !== undefined && highlightedIndexProp !== -1) { - scrollRowIntoViewIfNecessary(highlightedIndexProp); + requestAnimationFrame(() => { + // deferred call, ensuring table has fully rendered + scrollRowIntoViewIfNecessary(highlightedIndexProp); + }); } }, [highlightedIndexProp, scrollRowIntoViewIfNecessary]); @@ -351,7 +354,7 @@ NavigationHookProps) => { } } }, - [rowCount, navigationStyle, moveHighlightedRow, navigateChildItems] + [rowCount, navigationStyle, moveHighlightedRow, navigateChildItems], ); const handleClick = useCallback( @@ -364,7 +367,7 @@ NavigationHookProps) => { setActiveCell(rowIdx, colIdx); } }, - [setActiveCell] + [setActiveCell], ); const handleMouseLeave = useCallback(() => { @@ -378,7 +381,7 @@ NavigationHookProps) => { setHighlightedIndex(idx); } }, - [setHighlightedIndex] + [setHighlightedIndex], ); const navigate = useCallback(() => { diff --git a/vuu-ui/packages/vuu-table/src/useMeasuredHeight.ts b/vuu-ui/packages/vuu-table/src/useMeasuredHeight.ts index a6e4aaab5..e14723cfe 100644 --- a/vuu-ui/packages/vuu-table/src/useMeasuredHeight.ts +++ b/vuu-ui/packages/vuu-table/src/useMeasuredHeight.ts @@ -8,7 +8,7 @@ interface MeasuredHeightHookProps { export const useMeasuredHeight = ({ onHeightMeasured, - height: heightProp = 0, + height: heightProp = 0 }: MeasuredHeightHookProps) => { const [rowHeight, setRowHeight] = useState(heightProp); diff --git a/vuu-ui/packages/vuu-table/src/usePagination.ts b/vuu-ui/packages/vuu-table/src/usePagination.ts new file mode 100644 index 000000000..79a014ecf --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/usePagination.ts @@ -0,0 +1,49 @@ +import { SyntheticEvent, useCallback, useMemo, useState } from "react"; +import { TableProps } from "./Table"; +import { DataSource } from "@finos/vuu-data-types"; + +export type PaginationHookProps = Pick< + TableProps, + "dataSource" | "showPaginationControls" +>; + +const getPageCount = (dataSource: DataSource) => { + const { range, size } = dataSource; + const pageSize = range.to - range.from; + if (pageSize > 0) { + return Math.ceil(size / pageSize); + } else { + return 0; + } +}; + +export const usePagination = ({ + dataSource, + showPaginationControls, +}: PaginationHookProps) => { + const [pageCount, setPageCount] = useState(getPageCount(dataSource)); + + useMemo(() => { + if (showPaginationControls) { + dataSource.on("page-count", (n: number) => setPageCount(n)); + } + }, [dataSource, showPaginationControls]); + + const handlePageChange = useCallback( + (_evt: SyntheticEvent, page: number) => { + const { range } = dataSource; + const pageSize = range.to - range.from; + const firstRow = pageSize * (page - 1); + console.log( + `set range ${JSON.stringify({ from: firstRow, to: firstRow + pageSize })}`, + ); + dataSource.range = { from: firstRow, to: firstRow + pageSize }; + }, + [dataSource], + ); + + return { + onPageChange: handlePageChange, + pageCount, + }; +}; diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index a4a43875e..abe2b0e25 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -93,6 +93,8 @@ export interface TableHookProps | "onRowClick" | "renderBufferSize" | "scrollingApiRef" + | "showColumnHeaders" + | "showPaginationControls" > { containerRef: RefObject; rowHeight: number; @@ -138,6 +140,8 @@ export const useTable = ({ rowHeight = 20, scrollingApiRef, selectionModel, + showColumnHeaders, + showPaginationControls, size, }: TableHookProps) => { const tableConfigRef = useRef(config); @@ -145,7 +149,7 @@ export const useTable = ({ tableConfigRef.current = config; }, [config]); - const [headerHeight, setHeaderHeight] = useState(-1); + const [headerHeight, setHeaderHeight] = useState(showColumnHeaders ? -1 : 0); const [rowCount, setRowCount] = useState(dataSource.size); if (dataSource === undefined) { throw Error("no data source provided to Vuu Table"); @@ -240,8 +244,13 @@ export const useTable = ({ rowCount, rowHeight, size: size, + showPaginationControls, }); + // if (showPaginationControls) { + // dataSource.pageSize = viewportMeasurements.rowCount; + // } + const { data, dataRef, getSelectedRows, range, setRange } = useDataSource({ dataSource, // We need to factor this out of Table @@ -256,6 +265,7 @@ export const useTable = ({ rowHeight, scrollingApiRef, setRange, + showPaginationControls, onVerticalScroll: viewportHookSetScrollTop, onVerticalScrollInSitu: viewportHookSetInSituRowOffset, viewportMeasurements, diff --git a/vuu-ui/packages/vuu-table/src/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/useTableScroll.ts index 06bbcd868..336961f16 100644 --- a/vuu-ui/packages/vuu-table/src/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/useTableScroll.ts @@ -165,6 +165,7 @@ export interface TableScrollHookProps { rowHeight: number; scrollingApiRef?: ForwardedRef; setRange: (range: VuuRange) => void; + showPaginationControls?: boolean; viewportMeasurements: ViewportMeasurements; } @@ -303,7 +304,6 @@ export const useTableScroll = ({ const { current: contentContainer } = contentContainerRef; const { current: scrollbarContainer } = scrollbarContainerRef; const { current: scrollPos } = contentContainerPosRef; - if (contentContainer && scrollbarContainer) { const [ scrollLeft, diff --git a/vuu-ui/packages/vuu-table/src/useTableViewport.ts b/vuu-ui/packages/vuu-table/src/useTableViewport.ts index ea8652ea4..164f666ed 100644 --- a/vuu-ui/packages/vuu-table/src/useTableViewport.ts +++ b/vuu-ui/packages/vuu-table/src/useTableViewport.ts @@ -24,7 +24,8 @@ export interface TableViewportHookProps { * this is the solid left/right `border` rendered on the selection block */ selectionEndSize?: number; - size: MeasuredSize | undefined; + showPaginationControls?: boolean; + size: MeasuredSize; } export interface ViewportMeasurements { @@ -73,20 +74,44 @@ const UNMEASURED_VIEWPORT: TableViewportHookResult = { viewportWidth: 0, }; +const getViewportHeightProps = ( + rowCount: number, + rowHeight: number, + size: MeasuredSize, + showPaginationControls = false, +) => { + if (showPaginationControls) { + return { + pixelContentHeight: size.height, + virtualContentHeight: size.height, + virtualisedExtent: 0, + }; + } else { + const virtualContentHeight = rowCount * rowHeight; + const pixelContentHeight = Math.min(virtualContentHeight, MAX_PIXEL_HEIGHT); + const virtualisedExtent = virtualContentHeight - pixelContentHeight; + return { + pixelContentHeight, + virtualContentHeight, + virtualisedExtent, + }; + } +}; + export const useTableViewport = ({ columns, headerHeight, rowCount, rowHeight, selectionEndSize = 4, + showPaginationControls, size, }: TableViewportHookProps): TableViewportHookResult => { const inSituRowOffsetRef = useRef(0); const pctScrollTopRef = useRef(0); - // TODO we are limited by pixels not an arbitrary number of rows - const virtualContentHeight = rowCount * rowHeight; - const pixelContentHeight = Math.min(virtualContentHeight, MAX_PIXEL_HEIGHT); - const virtualisedExtent = virtualContentHeight - pixelContentHeight; + + const { virtualContentHeight, pixelContentHeight, virtualisedExtent } = + getViewportHeightProps(rowCount, rowHeight, size, showPaginationControls); const { pinnedWidthLeft, pinnedWidthRight, unpinnedWidth } = useMemo( () => measurePinnedColumns(columns, selectionEndSize), @@ -136,13 +161,18 @@ export const useTableViewport = ({ const horizontalScrollbarHeight = contentWidth > size.width ? scrollbarSize : 0; const measuredHeaderHeight = headerHeight === -1 ? 0 : headerHeight; - const visibleRows = (size.height - measuredHeaderHeight) / rowHeight; + const visibleRows = showPaginationControls + ? Math.floor((pixelContentHeight - headerHeight) / rowHeight) + : (size.height - headerHeight) / rowHeight; const count = Number.isInteger(visibleRows) ? visibleRows : Math.ceil(visibleRows); const viewportBodyHeight = size.height - measuredHeaderHeight; - const verticalScrollbarWidth = - pixelContentHeight > viewportBodyHeight ? scrollbarSize : 0; + const verticalScrollbarWidth = showPaginationControls + ? 0 + : pixelContentHeight > viewportBodyHeight + ? scrollbarSize + : 0; const appliedPageSize = count * rowHeight * (pixelContentHeight / virtualContentHeight); @@ -172,18 +202,19 @@ export const useTableViewport = ({ return UNMEASURED_VIEWPORT; } }, [ - getRowAtPosition, - getRowOffset, - headerHeight, - isVirtualScroll, + size, pinnedWidthLeft, unpinnedWidth, pinnedWidthRight, - pixelContentHeight, + headerHeight, rowHeight, + showPaginationControls, + pixelContentHeight, + virtualContentHeight, + getRowAtPosition, + getRowOffset, + isVirtualScroll, setInSituRowOffset, setScrollTop, - size, - virtualContentHeight, ]); }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.css b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.css deleted file mode 100644 index a26fb7f79..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.css +++ /dev/null @@ -1,19 +0,0 @@ -.vuuInstrumentPicker { - height: 100%; - padding: var(--vuuInstrumentPicker-padding, 12px); - display: flex; - flex-direction: column; -} - -.vuuInstrumentPicker-inputField { - --vuu-icon-size: 16px; - flex: 0 0 40px; -} -.vuuInstrumentPicker-list { - background-color: var(--salt-container-primary-background); - flex: 1 1 auto; -} - -.vuuInstrumentPicker .vuuTableCell { - padding: 0; -} 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 deleted file mode 100644 index 561c663eb..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { DataSourceRowObject, TableSchema } from "@finos/vuu-data-types"; -import { Table, TableProps } from "@finos/vuu-table"; -import { ColumnMap, useId } from "@finos/vuu-utils"; -import { Input } from "@salt-ds/core"; -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; -import { ForwardedRef, forwardRef, HTMLAttributes, useMemo } from "react"; -import { DropdownBase, OpenChangeHandler } from "../dropdown-base"; -import { SearchCell } from "./SearchCell"; -import { useInstrumentPicker } from "./useInstrumentPicker"; - -import instrumentPickerCss from "./InstrumentPicker.css"; - -const classBase = "vuuInstrumentPicker"; - -if (typeof SearchCell !== "function") { - console.warn("Instrument Picker: SearchCell module not loaded "); -} - -export interface InstrumentPickerProps - extends Omit, "onSelect">, - Pick { - TableProps: Pick; - columnMap: ColumnMap; - disabled?: boolean; - /** - * Used to form the display value to render in input following selection. If - * not provided, default will be the values from rendered columns. - * - * @param row DataSourceRow - * @returns string - */ - itemToString?: (row: DataSourceRowObject) => string; - onClose?: () => void; - onOpenChange?: OpenChangeHandler; - schema: TableSchema; - searchColumns: string[]; - width?: number; -} - -export const InstrumentPicker = forwardRef(function InstrumentPicker( - { - TableProps: { dataSource, ...TableProps }, - className, - columnMap, - disabled, - id: idProp, - itemToString, - onOpenChange: onOpenChangeProp, - onSelect, - schema, - searchColumns, - width, - ...htmlAttributes - }: InstrumentPickerProps, - forwardedRef: ForwardedRef -) { - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "vuu-instrument-picker", - css: instrumentPickerCss, - window: targetWindow, - }); - - const id = useId(idProp); - - const { - highlightedIndex, - inputProps, - isOpen, - onOpenChange, - tableHandlers, - tableRef, - value, - } = useInstrumentPicker({ - columnMap, - columns: TableProps.config.columns, - dataSource, - itemToString, - onOpenChange: onOpenChangeProp, - onSelect, - searchColumns, - }); - - const endAdornment = useMemo(() => , []); - - const tableProps = { - ...TableProps, - config: { - ...TableProps.config, - zebraStripes: false, - }, - }; - - return ( - - - - - - ); -}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.css b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.css new file mode 100644 index 000000000..a39e5a897 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.css @@ -0,0 +1,14 @@ +.vuuTablePicker { + border-radius: 6px; + height: var(--salt-size-base); + padding: 1px; + box-sizing: content-box; +} + +.vuuTablePicker-floating-table { + border: solid 1px var(--salt-container-secondary-borderColor); + border-radius: 4px; + overflow: auto; + padding: var(--salt-spacing-100) 0; + z-index: var(--salt-zIndex-popout); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.tsx new file mode 100644 index 000000000..0fa57906b --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/TablePicker.tsx @@ -0,0 +1,139 @@ +import type { DataSourceRowObject, TableSchema } from "@finos/vuu-data-types"; +import { Table, type TableProps } from "@finos/vuu-table"; +import { + Input, + useFloatingComponent, + useIdMemo, + type FloatingComponentProps, +} from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import cx from "clsx"; +import { forwardRef, useMemo, type HTMLAttributes } from "react"; +import { IconButton } from "../icon-button"; +import tablePickerCss from "./TablePicker.css"; +import { useTablePicker } from "./useTablePicker"; + +const classBase = "vuuTablePicker"; + +interface FloatingTableProps extends FloatingComponentProps { + collapsed?: boolean; +} + +export interface TablePickerProps + extends Omit, "onSelect">, + Pick { + TableProps?: Pick; + rowToString?: (row: DataSourceRowObject) => string; + schema: TableSchema; + searchColumns?: string[]; +} + +const FloatingTable = forwardRef( + function FloatingTable( + { children, className, collapsed, open, ...props }, + forwardedRef, + ) { + const { Component: FloatingComponent } = useFloatingComponent(); + return ( + + {children} + + ); + }, +); + +export const TablePicker = ({ + TableProps, + onSelect, + rowToString, + schema, + searchColumns, + ...htmlAttributes +}: TablePickerProps) => { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "vuu-table-picker", + css: tablePickerCss, + window: targetWindow, + }); + + const tableId = useIdMemo(); + + const { + containerRef, + dataSource, + highlightedIndex, + floatingUIProps: { x, y, strategy, floating, reference }, + inputProps, + interactionPropGetters: { getFloatingProps, getReferenceProps }, + onKeyDown, + open, + tableConfig, + tableHandlers, + tableRef, + value, + width, + } = useTablePicker({ + TableProps, + rowToString, + onSelect, + schema, + searchColumns, + }); + + const endAdornment = useMemo( + () => ( + + ), + [getReferenceProps, onKeyDown, reference], + ); + + return ( +
+ + +
+ + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/index.ts b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/index.ts index 48ee47024..0be6cdf2b 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/index.ts @@ -1 +1 @@ -export * from "./InstrumentPicker"; +export * from "./TablePicker"; 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 deleted file mode 100644 index eddd15042..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { DataSource, DataSourceRowObject } from "@finos/vuu-data-types"; -import { - ColumnDescriptor, - TableRowSelectHandler, -} from "@finos/vuu-table-types"; -import { useControlledTableNavigation } from "@finos/vuu-table"; -import { ChangeEvent, useCallback, useMemo, useState } from "react"; -import { useControlled } from "../common-hooks"; -import { OpenChangeHandler } from "../dropdown-base"; -import { InstrumentPickerProps } from "./InstrumentPicker"; - -export interface InstrumentPickerHookProps - extends Pick< - InstrumentPickerProps, - "columnMap" | "itemToString" | "onOpenChange" | "onSelect" | "searchColumns" - > { - columns: ColumnDescriptor[]; - dataSource: DataSource; - defaultIsOpen?: boolean; - isOpen?: boolean; -} - -const defaultItemToString = (row: DataSourceRowObject) => - Object.values(row.data).join(" "); - -export const useInstrumentPicker = ({ - dataSource, - defaultIsOpen, - isOpen: isOpenProp, - itemToString = defaultItemToString, - onOpenChange, - onSelect, - searchColumns, -}: InstrumentPickerHookProps) => { - const [value, setValue] = useState(""); - const [isOpen, setIsOpen] = useControlled({ - controlled: isOpenProp, - default: defaultIsOpen ?? false, - name: "useDropdownList", - }); - - const { highlightedIndexRef, onKeyDown, tableRef } = - useControlledTableNavigation(-1, dataSource.size); - - const baseFilterPattern = useMemo( - // TODO make this contains once server supports it - () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), - [searchColumns] - ); - - const handleOpenChange = useCallback( - (open, closeReason) => { - setIsOpen(open); - onOpenChange?.(open, closeReason); - }, - [onOpenChange, setIsOpen] - ); - - const handleInputChange = useCallback( - (evt: ChangeEvent) => { - const { value } = evt.target; - setValue(value); - - if (value && value.trim().length) { - const filter = baseFilterPattern.replaceAll("__VALUE__", value); - dataSource.filter = { - filter, - }; - } else { - dataSource.filter = { - filter: "", - }; - } - - setIsOpen(true); - }, - [baseFilterPattern, dataSource, setIsOpen] - ); - - const handleSelectRow = useCallback( - (row) => { - const value = row === null ? "" : itemToString(row); - setValue(value); - onSelect?.(row); - handleOpenChange?.(false, "select"); - }, - [handleOpenChange, itemToString, onSelect] - ); - - const inputProps = { - inputProps: { - autoComplete: "off", - onKeyDown, - }, - onChange: handleInputChange, - }; - const tableHandlers = { - onSelect: handleSelectRow, - }; - - return { - highlightedIndex: highlightedIndexRef.current, - inputProps, - isOpen, - onOpenChange: handleOpenChange, - tableHandlers, - tableRef, - value, - }; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useTablePicker.ts b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useTablePicker.ts new file mode 100644 index 000000000..6db8725fc --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useTablePicker.ts @@ -0,0 +1,207 @@ +import { + flip, + size, + useClick, + useDismiss, + useInteractions, +} from "@floating-ui/react"; +import { useFloatingUI } from "@salt-ds/core"; +import { + ChangeEvent, + KeyboardEventHandler, + RefCallback, + useCallback, + useMemo, + useRef, + useState, +} from "react"; + +import type { DataSourceRowObject } from "@finos/vuu-data-types"; +import type { + TableConfig, + TableRowSelectHandler, +} from "@finos/vuu-table-types"; +import { isStringColumn, toColumnName, useDataSource } from "@finos/vuu-utils"; +import type { TablePickerProps } from "./TablePicker"; +import { + isNavigationKey, + isRowSelectionKey, + useControlledTableNavigation, +} from "@finos/vuu-table"; + +export interface TablePickerHookProps + extends Pick< + TablePickerProps, + "TableProps" | "onSelect" | "rowToString" | "schema" | "searchColumns" + > { + defaultIsOpen?: boolean; + isOpen?: boolean; +} + +const defaultRowToString = (row: DataSourceRowObject) => + Object.values(row.data).join(" "); + +export const useTablePicker = ({ + TableProps, + onSelect, + rowToString = defaultRowToString, + schema, + searchColumns = schema.columns.filter(isStringColumn).map(toColumnName), +}: TablePickerHookProps) => { + const { VuuDataSource } = useDataSource(); + const [value, setValue] = useState(""); + const [open, setOpen] = useState(false); + + const widthRef = useRef(-1); + + const tableColumns = TableProps?.config.columns; + + const containerRef = useCallback>((el) => { + widthRef.current = el?.clientWidth ?? -1; + }, []); + + const dataSource = useMemo(() => { + const columns = tableColumns ?? schema.columns; + + return new VuuDataSource({ + columns: columns.map((c) => c.name), + table: schema.table, + }); + }, [tableColumns, VuuDataSource, schema]); + + const navigation = useControlledTableNavigation(-1, dataSource.size); + + const baseFilterPattern = useMemo( + // TODO make this contains once server supports it + () => searchColumns.map((col) => `${col} starts "__VALUE__"`).join(" or "), + [searchColumns], + ); + + // const handleOpenChange = useCallback( + // (open, closeReason) => { + // setIsOpen(open); + // onOpenChange?.(open, closeReason); + // }, + // [onOpenChange, setIsOpen], + // ); + + const { context, elements, ...floatingUIProps } = useFloatingUI({ + open, + onOpenChange: setOpen, + placement: "bottom-end", + strategy: "fixed", + middleware: [ + size({ + apply({ rects, elements, availableHeight }) { + Object.assign(elements.floating.style, { + minWidth: `${rects.reference.width}px`, + maxHeight: `max(calc(${availableHeight}px - var(--salt-spacing-100)), calc((var(--salt-size-base) + var(--salt-spacing-100)) * 5))`, + }); + }, + }), + flip({ fallbackStrategy: "initialPlacement" }), + ], + }); + + const interactionPropGetters = useInteractions([ + useDismiss(context), + useClick(context, { keyboardHandlers: false, toggle: false }), + ]); + + const handleInputChange = useCallback( + (evt: ChangeEvent) => { + const { value } = evt.target; + setValue(value); + + console.log(`input changed ${value}`); + + if (value && value.trim().length) { + const filter = baseFilterPattern.replaceAll("__VALUE__", value); + dataSource.filter = { + filter, + }; + } else { + dataSource.filter = { + filter: "", + }; + } + }, + [baseFilterPattern, dataSource], + ); + + const handleSelectRow = useCallback( + (row) => { + const value = row === null ? "" : rowToString(row); + setValue(value); + onSelect?.(row); + // TODO do we need to include a reason ? + requestAnimationFrame(() => { + setOpen(false); + }); + }, + [onSelect, rowToString], + ); + + const handleKeyDown = useCallback>( + (evt) => { + if (open) { + if (isNavigationKey(evt.key, "row") || isRowSelectionKey(evt.key)) { + navigation.onKeyDown(evt); + } + } else { + if (evt.key === "ArrowDown" || evt.key === "Enter") { + setOpen(true); + } + } + }, + [navigation, open], + ); + + const inputProps = { + inputProps: { + autoComplete: "off", + onKeyDown: handleKeyDown, + }, + onChange: handleInputChange, + }; + const tableHandlers = { + onSelect: handleSelectRow, + }; + + const tableConfig = useMemo(() => { + const config = TableProps?.config; + if (config) { + const { + columns = schema.columns, + columnLayout = "fit", + ...rest + } = config; + return { + columns, + columnLayout, + ...rest, + }; + } else { + return { + columnLayout: "fit", + columns: schema.columns, + }; + } + }, [TableProps, schema]); + + return { + containerRef, + dataSource, + highlightedIndex: navigation.highlightedIndexRef.current, + floatingUIProps, + inputProps, + interactionPropGetters, + onKeyDown: handleKeyDown, + open, + tableConfig, + tableHandlers, + tableRef: navigation.tableRef, + value, + width: widthRef.current, + }; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts b/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts index 3f3c6f6b1..efb2955d2 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts @@ -42,8 +42,6 @@ interface MeasuredState { const isNumber = (val: unknown): val is number => Number.isFinite(val); -const FULL_SIZE: CssSize = { height: "100%", width: "auto" }; - export interface MeasuredContainerHookResult { containerRef: RefObject; cssSize: CssSize; @@ -51,6 +49,33 @@ export interface MeasuredContainerHookResult { innerSize?: MeasuredSize; } +export const reduceSizeHeight = ( + size: MeasuredSize, + value: number, +): MeasuredSize => { + if (value === 0) { + return size; + } else { + return { + height: size.height - value, + width: size.width, + }; + } +}; + +const getInitialValue = ( + value: number | string | undefined, + defaultValue: "auto" | "100%", +) => { + if (isValidNumber(value)) { + return `${value}px`; + } else if (typeof value === "string") { + return value; + } else { + return defaultValue; + } +}; + // If (outer) height and width are known at initialisation (i.e. they // were passed as props), use as initial values for inner size. If there // is no border on Table, these values will not change. If there is a border, @@ -59,19 +84,10 @@ const getInitialCssSize = ( height?: number | string, width?: number | string, ): CssSize => { - if (isValidNumber(height) && isValidNumber(width)) { - return { - height: `${height}px`, - width: `${width}px`, - }; - } else if (typeof height === "string" || typeof width === "string") { - return { - height: height ?? "100%", - width: width ?? "auto", - }; - } else { - return FULL_SIZE; - } + return { + height: getInitialValue(height, "100%"), + width: getInitialValue(width, "auto"), + }; }; const getInitialInnerSize = ( diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index 453f5ee01..d980631ac 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -1044,10 +1044,12 @@ const measureColumns = ( ) => columns.reduce( (aggregated, column) => { - aggregated.totalMinWidth += column.minWidth ?? defaultMinWidth; - aggregated.totalMaxWidth += column.maxWidth ?? defaultMaxWidth; - aggregated.totalWidth += column.width; - aggregated.flexCount += column.flex ?? 0; + if (column.hidden !== true) { + aggregated.totalMinWidth += column.minWidth ?? defaultMinWidth; + aggregated.totalMaxWidth += column.maxWidth ?? defaultMaxWidth; + aggregated.totalWidth += column.width; + aggregated.flexCount += column.flex ?? 0; + } return aggregated; }, { totalMinWidth: 0, totalMaxWidth: 0, totalWidth: 0, flexCount: 0 }, @@ -1243,3 +1245,7 @@ export const dataColumnAndKeyUnchanged = ( p.row[KEY] === p1.row[KEY] && p.column.valueFormatter(p.row[p.columnMap[p.column.name]]) === p1.column.valueFormatter(p1.row[p1.columnMap[p1.column.name]]); + +export const toColumnName = (column: ColumnDescriptor) => column.name; +export const isStringColumn = (column: ColumnDescriptor) => + column.serverDataType === "string"; diff --git a/vuu-ui/packages/vuu-utils/src/feature-utils.ts b/vuu-ui/packages/vuu-utils/src/feature-utils.ts index f1e080793..e8bafad72 100644 --- a/vuu-ui/packages/vuu-utils/src/feature-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/feature-utils.ts @@ -61,12 +61,12 @@ export interface StaticFeatureDescriptor { } const isStaticFeature = ( - feature: unknown, + feature: unknown ): feature is StaticFeatureDescriptor => feature !== null && typeof feature === "object" && "type" in feature; export const isStaticFeatures = ( - features: unknown, + features: unknown ): features is StaticFeatureDescriptor[] => Array.isArray(features) && features.every(isStaticFeature); @@ -82,7 +82,7 @@ export function featureFromJson({ type }: { type: string }): ReactElement { const componentType = type.match(/^[a-z]/) ? type : getLayoutComponent(type); if (componentType === undefined) { throw Error( - `layoutUtils unable to create feature component from JSON, unknown type ${type}`, + `layoutUtils unable to create feature component from JSON, unknown type ${type}` ); } return React.createElement(componentType); @@ -111,7 +111,7 @@ export interface FeaturePropsWithFilterTableFeature } export const hasFilterTableFeatureProps = ( - props: DynamicFeatureProps, + props: DynamicFeatureProps ): props is FeaturePropsWithFilterTableFeature => typeof props.ComponentProps === "object" && props.ComponentProps !== null && @@ -142,19 +142,19 @@ export type GetFeaturePaths = (params: { export const getFilterTableFeatures = ( schemas: TableSchema[], - getFeaturePath: GetFeaturePaths, + getFeaturePath: GetFeaturePaths ) => schemas .sort(byModule) .map>((schema) => ({ ...getFeaturePath({ env, fileName: "FilterTable" }), ComponentProps: { - tableSchema: schema, + tableSchema: schema }, ViewProps: { - allowRename: true, + allowRename: true }, - title: `${schema.table.module} ${schema.table.table}`, + title: `${schema.table.module} ${schema.table.table}` })); export type Component = { @@ -164,11 +164,11 @@ export type Component = { export const assertComponentRegistered = ( componentName: string, - component: unknown, + component: unknown ) => { if (typeof component !== "function") { console.warn( - `${componentName} module not loaded, will be unabale to deserialize from layout JSON`, + `${componentName} module not loaded, will be unabale to deserialize from layout JSON` ); } }; @@ -181,14 +181,14 @@ export const assertComponentsRegistered = (componentList: Component[]) => { export const getCustomAndTableFeatures = ( dynamicFeatures: DynamicFeatureDescriptor[], - vuuTables: Map, + vuuTables: Map ): { dynamicFeatures: DynamicFeatureProps[]; tableFeatures: DynamicFeatureProps[]; } => { const [customFeatureConfig, tableFeaturesConfig] = partition( dynamicFeatures, - isCustomFeature, + isCustomFeature ); const customFeatures: DynamicFeatureProps[] = []; @@ -205,15 +205,15 @@ export const getCustomAndTableFeatures = ( tableFeatures.push({ ...feature, ComponentProps: { - tableSchema, + tableSchema }, title: `${tableSchema.table.module} ${wordify( - tableSchema.table.table, + tableSchema.table.table )}`, ViewProps: { ...viewProps, - allowRename: true, - }, + allowRename: true + } }); } } else if (isTableSchema(schema) && vuuTables) { @@ -222,9 +222,9 @@ export const getCustomAndTableFeatures = ( tableFeatures.push({ ...feature, ComponentProps: { - tableSchema, + tableSchema }, - ViewProps: viewProps, + ViewProps: viewProps }); } } @@ -241,9 +241,9 @@ export const getCustomAndTableFeatures = ( customFeatures.push({ ...feature, ComponentProps: { - tableSchema, + tableSchema }, - ViewProps: viewProps, + ViewProps: viewProps }); } else if (Array.isArray(schemas) && vuuTables) { customFeatures.push({ @@ -251,13 +251,13 @@ export const getCustomAndTableFeatures = ( ComponentProps: schemas.reduce>( (map, schema) => { map[`${schema.table}Schema`] = vuuTables.get( - schema.table, + schema.table ) as TableSchema; return map; }, - {}, + {} ), - ViewProps: viewProps, + ViewProps: viewProps }); } else { customFeatures.push(feature); 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 c7e1213e8..816e4a38e 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx @@ -25,6 +25,7 @@ import { getDefaultColumnConfig } from "./columnMetaData"; // import { useRpcResponseHandler } from "./useRpcResponseHandler"; import "./App.css"; +// import { RestDataSourceProvider } from "@finos/vuu-data-react/src/datasource-provider/RestDataSourceProvider"; registerComponent("ColumnSettings", ColumnSettingsPanel, "view"); registerComponent("TableSettings", TableSettingsPanel, "view"); 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 07ae2dc98..3596aa07b 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 @@ -1,16 +1,12 @@ -import { - DataSource, - DataSourceRowObject, - TableSchema, -} from "@finos/vuu-data-types"; +import { DataSourceRowObject, TableSchema } from "@finos/vuu-data-types"; import { DialogHeader, PopupComponent as Popup, Portal, } from "@finos/vuu-popups"; import { - InstrumentPicker, - InstrumentPickerProps, + TablePicker, + TablePickerProps, VuuInput, } from "@finos/vuu-ui-controls"; import { Button, FormField, FormFieldLabel } from "@salt-ds/core"; @@ -25,55 +21,40 @@ const classBase = "vuuBasketNewBasketPanel"; export type BasketCreatedHandler = ( basketName: string, basketId: string, - instanceId: string + instanceId: string, ) => void; export interface NewBasketPanelProps extends HTMLAttributes { - basketDataSource: DataSource; basketSchema: TableSchema; onClose: () => void; onBasketCreated: BasketCreatedHandler; } -const searchColumns = ["name"]; - export const NewBasketPanel = ({ className, - basketDataSource, basketSchema, onClose, onBasketCreated, ...htmlAttributes }: NewBasketPanelProps) => { const { - columnMap, onChangeBasketName, - onOpenChangeInstrumentPicker, onSave, onSelectBasket, saveButtonDisabled, saveButtonRef, } = useNewBasketPanel({ - basketDataSource, basketSchema, onBasketCreated, }); - const tableProps = useMemo( + const tableProps = useMemo( () => ({ config: { - columns: [ - { name: "id", hidden: true }, - { - name: "name", - width: 200, - }, - ], - rowSeparators: true, + columns: [{ name: "id", hidden: true }, { name: "name" }], }, - dataSource: basketDataSource, }), - [basketDataSource] + [], ); const itemToString = (row: DataSourceRowObject) => row.data.name as string; @@ -104,13 +85,10 @@ export const NewBasketPanel = ({ 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 index ef170d603..b69693fb7 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts @@ -1,18 +1,17 @@ import { ViewportRpcResponse } from "@finos/vuu-data-types"; import type { TableRowSelectHandler } from "@finos/vuu-table-types"; import { OpenChangeHandler } from "@finos/vuu-ui-controls"; -import { CommitHandler, buildColumnMap } from "@finos/vuu-utils"; -import { useCallback, useRef, useState } from "react"; +import { CommitHandler, buildColumnMap, useDataSource } from "@finos/vuu-utils"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { NewBasketPanelProps } from "./NewBasketPanel"; import { VuuRpcViewportRequest } from "@finos/vuu-protocol-types"; export type NewBasketHookProps = Pick< NewBasketPanelProps, - "basketDataSource" | "basketSchema" | "onBasketCreated" + "basketSchema" | "onBasketCreated" >; export const useNewBasketPanel = ({ - basketDataSource, basketSchema, onBasketCreated, }: NewBasketHookProps) => { @@ -20,6 +19,22 @@ export const useNewBasketPanel = ({ const [basketName, setBasketName] = useState(""); const [basketId, setBasketId] = useState(); const saveButtonRef = useRef(null); + const { VuuDataSource } = useDataSource(); + const basketDataSource = useMemo(() => { + const ds = new VuuDataSource({ table: basketSchema.table }); + ds.subscribe({}, () => { + // we don't really care about messages from this dataSource, we + // only use it as a conduit for creating a basket. + }); + return ds; + }, [VuuDataSource, basketSchema]); + + useEffect(() => { + return () => { + basketDataSource.unsubscribe(); + }; + }, [basketDataSource]); + const saveBasket = useCallback(() => { if (basketName && basketId) { basketDataSource diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx index 3e217e1ce..e9619c776 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx @@ -103,7 +103,6 @@ export const useBasketTrading = ({ }, [load]); const { - dataSourceBasket, dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, @@ -196,19 +195,13 @@ export const useBasketTrading = ({ ...state, dialog: ( ), })); - }, [ - basketSchema, - dataSourceBasket, - handleBasketCreated, - handleCloseNewBasketPanel, - ]); + }, [basketSchema, handleBasketCreated, handleCloseNewBasketPanel]); const basketSelectorProps = useMemo>( () => ({ 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 a71a0ac9b..9678516e0 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts @@ -22,7 +22,6 @@ const NO_CONFIG = {}; export const useBasketTradingDataSources = ({ basketConstituentSchema, - basketSchema, basketInstanceId, basketTradingSchema, basketTradingConstituentJoinSchema, @@ -34,7 +33,6 @@ export const useBasketTradingDataSources = ({ const { VuuDataSource } = useDataSource(); const [ - dataSourceBasket, dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, @@ -57,7 +55,6 @@ export const useBasketTradingDataSources = ({ number, DataSourceConfig?, ][] = [ - ["data-source-basket", basketSchema, 100], [ "data-source-basket-trading-control", basketTradingSchema, @@ -98,7 +95,6 @@ export const useBasketTradingDataSources = ({ return dataSources; }, [ basketInstanceId, - basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, basketConstituentSchema, @@ -159,7 +155,6 @@ export const useBasketTradingDataSources = ({ // in session state from where it will be used by the AddInstrument button in Col // Header return { - dataSourceBasket, dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, diff --git a/vuu-ui/showcase/src/examples/Table/Paging.examples.tsx b/vuu-ui/showcase/src/examples/Table/Paging.examples.tsx new file mode 100644 index 000000000..7be6581ff --- /dev/null +++ b/vuu-ui/showcase/src/examples/Table/Paging.examples.tsx @@ -0,0 +1,77 @@ +import { useVuuMenuActions } from "@finos/vuu-data-react"; +import { + SimulTableName, + simulModule, + simulSchemas +} from "@finos/vuu-data-test"; +import { ContextMenuProvider } from "@finos/vuu-popups"; +import { Table, TableProps } from "@finos/vuu-table"; +import { ColumnLayout } from "@finos/vuu-table-types"; +import { applyDefaultColumnConfig } from "@finos/vuu-utils"; +import { useCallback, useMemo } from "react"; +import { DemoTableContainer } from "./DemoTableContainer"; + +let displaySequence = 0; + +const SimulTable = ({ + columnLayout, + height = "100%", + renderBufferSize = 0, + rowClassNameGenerators, + tableName = "instruments", + ...props +}: Partial & { + columnLayout?: ColumnLayout; + rowClassNameGenerators?: string[]; + tableName?: SimulTableName; +}) => { + const schema = simulSchemas[tableName]; + + const tableProps = useMemo>( + () => ({ + config: { + columnLayout, + columns: applyDefaultColumnConfig(schema), + rowClassNameGenerators, + rowSeparators: true, + zebraStripes: true + }, + dataSource: simulModule.createDataSource(tableName) + }), + [columnLayout, rowClassNameGenerators, schema, tableName] + ); + + const handleConfigChange = useCallback(() => { + // console.log(JSON.stringify(config, null, 2)); + }, []); + + const { buildViewserverMenuOptions, handleMenuAction } = useVuuMenuActions({ + dataSource: tableProps.dataSource + }); + + return ( + <> + + +
+ + + + ); +}; +SimulTable.displaySequence = displaySequence++; + +export const DefaultPaging = () => ( + +); +DefaultPaging.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx index c406a590b..faf82519f 100644 --- a/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/SIMUL.examples.tsx @@ -18,7 +18,7 @@ let displaySequence = 1; const getDefaultColumnConfig = ( tableName: string, - columnName: string + columnName: string, ): Partial | undefined => { switch (columnName) { case "ask": @@ -64,7 +64,7 @@ export const SimulTable = ({ columnLayout, getDefaultColumnConfig, height = 625, - renderBufferSize = 0, + renderBufferSize = 10, rowClassNameGenerators, tableName = "instruments", ...props @@ -94,7 +94,7 @@ export const SimulTable = ({ rowClassNameGenerators, schema, tableName, - ] + ], ); const handleConfigChange = useCallback(() => { diff --git a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx index be8832c23..57687d559 100644 --- a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx @@ -5,7 +5,7 @@ import { SimulTableName, vuuModule, } from "@finos/vuu-data-test"; -import { DataSource } from "@finos/vuu-data-types"; +import { DataSource, TableSchema } from "@finos/vuu-data-types"; import { Flexbox, FlexboxLayout, @@ -30,6 +30,7 @@ import { applyDefaultColumnConfig, defaultValueFormatter, registerComponent, + useDataSource, } from "@finos/vuu-utils"; import { Button } from "@salt-ds/core"; import { @@ -39,10 +40,11 @@ import { useMemo, useState, } from "react"; -import { useTestDataSource } from "../utils"; +import { useAutoLoginToVuuServer, useTestDataSource } from "../utils"; import { columnGenerator, rowGenerator } from "./SimpleTableDataGenerator"; import "./Table.examples.css"; +import { VuuDataSourceProvider } from "@finos/vuu-data-react"; let displaySequence = 1; @@ -52,6 +54,12 @@ export const TestTable = ({ rowCount = 1000, rowHeight = 20, width = 1000, +}: { + height?: string | number; + renderBufferSize?: number; + rowCount?: number; + rowHeight?: number; + width?: string | number; }) => { const config = useMemo( () => ({ @@ -269,22 +277,32 @@ export const EditableTableArrayData = () => { }; EditableTableArrayData.displaySequence = displaySequence++; -export const VuuInstruments = () => { - const schemas = getAllSchemas(); - const { config, dataSource, error } = useTestDataSource({ - // bufferSize: 1000, - schemas, - }); +const VuuTableTemplate = ({ schema }: { schema: TableSchema }) => { + useAutoLoginToVuuServer(); + const { VuuDataSource } = useDataSource(); + const dataSource = useMemo(() => { + const { table } = schema; + const dataSource = new VuuDataSource({ + columns: schema.columns.map((c) => c.name), + table, + }); + return dataSource; + }, [VuuDataSource, schema]); - const [tableConfig] = useState(config); + console.log(); - if (error) { - return error; - } + const config = useMemo( + () => ({ + columns: schema.columns, + }), + [schema.columns], + ); + + console.log({ columns: schema.columns }); return (
{ /> ); }; + +export const VuuInstruments = () => { + const schema = getSchema("instruments"); + return ( + + + + ); +}; VuuInstruments.displaySequence = displaySequence++; export const FlexLayoutTables = () => { diff --git a/vuu-ui/showcase/src/examples/Table/TableLayout.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableLayout.examples.tsx index bec519b8e..547b4361b 100644 --- a/vuu-ui/showcase/src/examples/Table/TableLayout.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/TableLayout.examples.tsx @@ -13,7 +13,7 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { columnGenerator, rowGenerator } from "./SimpleTableDataGenerator"; import { VuuRpcMenuRequest } from "@finos/vuu-protocol-types"; -let displaySequence = 1; +let displaySequence = 0; type DataTableProps = Partial< Omit & { config?: Partial } diff --git a/vuu-ui/showcase/src/examples/Table/TableVuuLayoutCombinations.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableVuuLayoutCombinations.examples.tsx new file mode 100644 index 000000000..3d4e55dc4 --- /dev/null +++ b/vuu-ui/showcase/src/examples/Table/TableVuuLayoutCombinations.examples.tsx @@ -0,0 +1,53 @@ +import { LayoutProvider, Stack, View } from "@finos/vuu-layout"; +import { useState } from "react"; +import { TestTable } from "./Table.examples"; + +let displaySequence = 1; + +export const TwoTabbedTables = () => { + const [active, setActive] = useState(0); + return ( + + + + + + + + + + + ); +}; +TwoTabbedTables.displaySequence = displaySequence++; + +export const FourTabbedTables = () => { + const [active, setActive] = useState(0); + return ( + + + + + + + ); +}; +FourTabbedTables.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Table/index.ts b/vuu-ui/showcase/src/examples/Table/index.ts index 561bad45a..d86eca219 100644 --- a/vuu-ui/showcase/src/examples/Table/index.ts +++ b/vuu-ui/showcase/src/examples/Table/index.ts @@ -2,9 +2,11 @@ export * as TableList from "./TableList.examples"; export * as Table from "./Table.examples"; export * as TableSelection from "./TableSelection.examples"; export * as BASKET from "./BASKET.examples"; +export * as Paging from "./Paging.examples"; export * as SIMUL from "./SIMUL.examples"; export * as TEST from "./TEST.examples"; export * as BigData from "./BigData.examples"; export * as ColumnLayout from "./ColumnLayout.examples"; export * as TableLayout from "./TableLayout.examples"; export * as BulkEdit from "./BulkEdit.examples"; +export * as TableVuuLayoutCombinations from "./TableVuuLayoutCombinations.examples"; diff --git a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx deleted file mode 100644 index 70eee410f..000000000 --- a/vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { - getAllSchemas, - getSchema, - SimulTableName, - vuuModule, -} from "@finos/vuu-data-test"; -import type { DataSourceRowObject } from "@finos/vuu-data-types"; -import type { TableProps } from "@finos/vuu-table"; -import type { - ColumnDescriptor, - TableRowSelectHandler, -} from "@finos/vuu-table-types"; -import { InstrumentPicker } from "@finos/vuu-ui-controls"; -import { buildColumnMap, ColumnMap } from "@finos/vuu-utils"; -import { useCallback, useMemo } from "react"; -import { useTestDataSource } from "../utils"; - -let displaySequence = 0; - -export const DefaultInstrumentPicker = () => { - const tableName: SimulTableName = "instruments"; - const schema = getSchema(tableName); - - const [tableProps, columnMap, searchColumns] = useMemo< - [Pick, ColumnMap, string[]] - >(() => { - return [ - { - config: { - columns: schema.columns, - rowSeparators: true, - zebraStripes: true, - }, - dataSource: - vuuModule("SIMUL").createDataSource(tableName), - }, - buildColumnMap(schema.columns), - ["bbg", "description"], - ]; - }, [schema.columns]); - - const itemToString = useCallback((row: DataSourceRowObject) => { - return String(row.data.description); - }, []); - - const handleSelect = useCallback((row) => { - if (row) { - console.log(`row selected ${row.key}`); - } - }, []); - - return ( - - ); -}; -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) => { - if (row) { - console.log(`row selected ${Object.values(row.data).join(",")}`); - } - }, []); - - if (error) { - return error; - } - - return ( - - ); -}; -InstrumentPickerVuuInstruments.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/TablePicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/TablePicker.examples.tsx new file mode 100644 index 000000000..fe79f55b9 --- /dev/null +++ b/vuu-ui/showcase/src/examples/UiControls/TablePicker.examples.tsx @@ -0,0 +1,72 @@ +import { VuuDataSourceProvider } from "@finos/vuu-data-react"; +import { getSchema, LocalDataSourceProvider } from "@finos/vuu-data-test"; +import { TablePicker, TablePickerProps } from "@finos/vuu-ui-controls"; +import { useAutoLoginToVuuServer } from "../utils"; + +let displaySequence = 0; + +const TablePickerTemplate = ({ + TableProps, + rowToString, + schema = getSchema("instruments"), +}: Partial) => { + return ( +
+ +
+ ); +}; + +const instrumentToString: TablePickerProps["rowToString"] = (row) => + `[${row.key}] ${row.data.description}`; + +export const DefaultInstrumentPicker = () => ( + + + +); +DefaultInstrumentPicker.displaySequence = displaySequence++; + +export const VuuInstrumentPicker = () => { + useAutoLoginToVuuServer(); + return ( + + + + ); +}; +VuuInstrumentPicker.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index 4b942698f..6be466c9c 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -3,7 +3,7 @@ export * as ColumnPicker from "./ColumnPicker.examples"; export * as DatePopup from "./DatePopup.examples"; export * as DragDrop from "./DragDrop.examples"; export * as EditableLabel from "./EditableLabel.examples"; -export * as InstrumentPicker from "./InstrumentPicker.examples"; +export * as TablePicker from "./TablePicker.examples"; export * as InstrumentSearch from "./InstrumentSearch.examples"; export * as List from "./List.examples"; export * as OverflowContainer from "./OverflowContainer.examples"; diff --git a/vuu-ui/showcase/src/examples/VuuFeatures/NewBasketPanel.examples.tsx b/vuu-ui/showcase/src/examples/VuuFeatures/NewBasketPanel.examples.tsx index d41029660..01da9ddc6 100644 --- a/vuu-ui/showcase/src/examples/VuuFeatures/NewBasketPanel.examples.tsx +++ b/vuu-ui/showcase/src/examples/VuuFeatures/NewBasketPanel.examples.tsx @@ -1,32 +1,29 @@ -import { BasketsTableName, getSchema, vuuModule } from "@finos/vuu-data-test"; +import { LocalDataSourceProvider, getSchema } from "@finos/vuu-data-test"; import { NewBasketPanel } from "feature-basket-trading"; -import { useCallback, useMemo } from "react"; -import { BasketCreatedHandler } from "sample-apps/feature-basket-trading/src/new-basket-panel"; +import { useCallback } from "react"; let displaySequence = 1; export const DefaultNewBasketPanel = () => { const schema = getSchema("basket"); - const dataSource = useMemo( - () => vuuModule("BASKET").createDataSource("basket"), - [] - ); + // const dataSource = useMemo( + // () => vuuModule("BASKET").createDataSource("basket"), + // [] + // ); - const handleBasketCreated = useCallback( - (basketName, basketId, instanceId) => { - console.log(`save basket #${basketId} as ${basketName} ${instanceId}`); - }, - [] - ); + const handleBasketCreated = useCallback(() => { + console.log(`save basket`); + }, []); return ( - console.log("close")} - onBasketCreated={handleBasketCreated} - /> + + console.log("close")} + onBasketCreated={handleBasketCreated} + /> + ); }; DefaultNewBasketPanel.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/salt/Pagination.examples.tsx b/vuu-ui/showcase/src/examples/salt/Pagination.examples.tsx new file mode 100644 index 000000000..31a57477c --- /dev/null +++ b/vuu-ui/showcase/src/examples/salt/Pagination.examples.tsx @@ -0,0 +1,29 @@ +import { + GoToInput, + Pagination, + PaginationProps, + Paginator +} from "@salt-ds/core"; +import { SyntheticEvent, useCallback } from "react"; + +let displaySequence = 0; + +const PaginationTemplate = ({ count = 100 }: Partial) => { + const handlePageChanged = useCallback((_: SyntheticEvent, page: number) => { + console.log(`page changed ${page}`); + }, []); + return ( +
+ + + + +
+ ); +}; + +export const DefaultPagination = () => ; +DefaultPagination.displaySequence = displaySequence++; + +export const LargeDataset = () => ; +LargeDataset.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/salt/index.ts b/vuu-ui/showcase/src/examples/salt/index.ts index 9d91ca0a2..edd5dc5c1 100644 --- a/vuu-ui/showcase/src/examples/salt/index.ts +++ b/vuu-ui/showcase/src/examples/salt/index.ts @@ -8,6 +8,7 @@ export * as Dropdown from "./Dropdown.examples"; export * as Input from "./Input.examples"; export * as FormField from "./FormField.examples"; export * as Menu from "./Menu.examples"; +export * as Pagination from "./Pagination.examples"; export * as Progress from "./Progress.examples"; export * as Switch from "./Switch.examples"; export * as ToggleButton from "./ToggleButton.examples";