diff --git a/docs/getting_started/adding.md b/docs/getting_started/adding.md index 0849b8fdd..f4e94dc66 100644 --- a/docs/getting_started/adding.md +++ b/docs/getting_started/adding.md @@ -10,13 +10,13 @@ You can add them to your pom by referencing the parent pom directly. ``` - org.finos + org.finos.vuu vuu-parent {check the latest version} - org.finos + org.finos.vuu vuu-ui {check the latest version} diff --git a/docs/getting_started/using_vuu_from_java.md b/docs/getting_started/using_vuu_from_java.md index 198465936..dfb2fb71c 100644 --- a/docs/getting_started/using_vuu_from_java.md +++ b/docs/getting_started/using_vuu_from_java.md @@ -4,6 +4,6 @@ import { SvgDottySeparator } from "@site/src/components/SvgDottySeparator"; -We have a sample Maven project on the Vuu Github site: +We have a sample Maven module within the code repo: -[Getting Started in Java](https://github.com/venuu-io/vuu-getting-started) +[Getting Started in Java](https://github.com/finos/vuu/tree/main/example/main-java) diff --git a/docs/introduction/diagrams-server-internals.svg b/docs/introduction/diagrams-server-internals.svg new file mode 100644 index 000000000..6ff739170 --- /dev/null +++ b/docs/introduction/diagrams-server-internals.svg @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/introduction/how_does_it_work.md b/docs/introduction/how_does_it_work.md index 53dcef2df..c165974e0 100644 --- a/docs/introduction/how_does_it_work.md +++ b/docs/introduction/how_does_it_work.md @@ -2,7 +2,7 @@ Let's start with a diagram. -![](./diagrams-server-internals.png) +![](./diagrams-server-internals.svg) This shows the basic flow through the system, there are components which have been omitted for clarity. @@ -20,7 +20,7 @@ out of band on a separate thread to the update path (Filter and Sort Thread.) Th table you have asked for in your viewport and applies the filters and sorts you've requested. The resulting array of primary keys is then pushed into your viewport. -THe update path, by comparison, is different. If you have a particular key in the visible range of your viewport already +The update path, by comparison, is different. If you have a particular key in the visible range of your viewport already and it is updated, it follows the tick() path, so it will progress through the system as an event, on the same thread as the provider itself. diff --git a/docs/perf/indices.md b/docs/perf/indices.md index bb416a1ec..ef3104bad 100644 --- a/docs/perf/indices.md +++ b/docs/perf/indices.md @@ -1,6 +1,6 @@ # Indices -Indices are a mechansim for filter a tables keys quickly. They are defined as part of a table and under the hood they are implemented as skip lists. +Indices are a mechanism for filter a tables keys quickly. They are defined as part of a table and under the hood they are implemented as skip lists. Currently, there is no query planner for indices, as a more advanced SQL database might have. They simply will shortcut the filtering process if an index exists on a field. Adding indices to a field dramatically reduces the cost of filter on that field, at the memory expense of maintaining an extra data structure and slight processing overhead. diff --git a/docs/providers_tables_viewports/lifecycle-startup.svg b/docs/providers_tables_viewports/lifecycle-startup.svg new file mode 100644 index 000000000..91dc75c9d --- /dev/null +++ b/docs/providers_tables_viewports/lifecycle-startup.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/providers_tables_viewports/lifecycle.md b/docs/providers_tables_viewports/lifecycle.md index 0b55972f9..a051edaf1 100644 --- a/docs/providers_tables_viewports/lifecycle.md +++ b/docs/providers_tables_viewports/lifecycle.md @@ -56,6 +56,6 @@ When Vuuu starts up it evaluates components from the furthest node out back to t ## What does the Vui lifecycle look like on startup? -![Lifecycle Vuu](./vuu.svg) +![Lifecycle Vuu](./lifecycle-startup.svg) As you can see from this graph. The server has as well defined startup and shutdown sequencer, controlled by its lifecycle. diff --git a/docs/providers_tables_viewports/providers.md b/docs/providers_tables_viewports/providers.md index b8a4d0b78..a1a267394 100644 --- a/docs/providers_tables_viewports/providers.md +++ b/docs/providers_tables_viewports/providers.md @@ -5,7 +5,7 @@ import { SvgDottySeparator } from "@site/src/components/SvgDottySeparator"; Providers are classes which receive data from a particlar location (network, file, in-process lib) and format that data into a map which matches the shape of the table -that the provider is populating. THey have a very simple interface: +that the provider is populating. They have a very simple interface: Included below is an example of the metrics provider. diff --git a/docs/providers_tables_viewports/server-internals.svg b/docs/providers_tables_viewports/server-internals.svg new file mode 100644 index 000000000..f1e5c8b23 --- /dev/null +++ b/docs/providers_tables_viewports/server-internals.svg @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/providers_tables_viewports/tables.md b/docs/providers_tables_viewports/tables.md index 66cb5fec9..b7b921434 100644 --- a/docs/providers_tables_viewports/tables.md +++ b/docs/providers_tables_viewports/tables.md @@ -44,14 +44,14 @@ processUpdate functions like an upsert in a SQL data (i.e. it is used for both a # (Simple) Table Simple Tables (the default table type, defined by using the TableDef() class) are sinks for data. They are wrappers around -a concurrent map and are mechanisms to propogate update or delete events to join tables and view ports. +a concurrent map and are mechanisms to propagate update or delete events to join tables and view ports. Currently simple tables are limited to having strings as the key. This is likely to change in future. # Join Tables Join tables represent the logical joining of two separate tables into a single merged table. In practice they are mappings of -keys from one table to keys from one or more other tables. When data is realised (i.e. sent down to a user's ui via the websocket) +keys from one table to keys from one or more other tables. When data is realized (i.e. sent down to a user's ui via the websocket) the relevant rows are realized by dragging the data from the underlying simple tables. # AutoSubscribe Table @@ -69,7 +69,7 @@ Session tables are specific types of tables that live only during the users conn ### Tree Session Tables -Tree Session tables are created dyanmically whenever there is a request to tree an underlying flat table. THe reason for this is that Tree's are a view ontop of +Tree Session tables are created dynamically whenever there is a request to tree an underlying flat table. THe reason for this is that Tree's are a view on top of and underlying raw table. When we create a tree, we are generating a tree data structure in memory whose leaves are keys that point back to the original rows in the underlying table. When your session is closed, the server cleans up these tree tables, freeing up resources. diff --git a/docs/providers_tables_viewports/viewports.md b/docs/providers_tables_viewports/viewports.md index 5dee135c1..893bcd93a 100644 --- a/docs/providers_tables_viewports/viewports.md +++ b/docs/providers_tables_viewports/viewports.md @@ -8,7 +8,7 @@ A Viewport is a specific client's view onto an underlying table. It has knowledg the window of data that a client currently has displayed on her screen. It contains information on any sorts, or filters that a specific client has requested on the data, on columns that the client has asked to display as well as information such as which rows are currently selected on the clients grid. -As well as this viewports contain references to immutable arrays of the keys of the underlying table. These arrays are sorted and filtered based on the clients +As well as the above, viewports contain references to immutable arrays of the keys of the underlying table. These arrays are sorted and filtered based on the clients requested sort of the data. When a user opens a viewport on a table from the client, a thread in the server will asynchronously populate the keys based on the viewports parameters (sorts, filters, etc..) into an immutable array @@ -17,8 +17,8 @@ and will pass that array to the viewport. This thread will then continuously rec The row that is sent to a user is only realized in the viewport at the point the row becomes visible in the client (or part of the pre-post fetch.) This occurs by dragging the fields from the underlying tables when an update needs to be sent to the client. -![Viewport](./diagrams-view-ports.png) +![Viewport](./viewports.svg) And in the context of the wider Vuu server. -![Viewport](./diagrams-server-internals.png) +![Viewport](./server-internals.svg) diff --git a/docs/providers_tables_viewports/viewports.svg b/docs/providers_tables_viewports/viewports.svg new file mode 100644 index 000000000..210a66672 --- /dev/null +++ b/docs/providers_tables_viewports/viewports.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/ui/calculated_columns.md b/docs/ui/calculated_columns.md index 68b532efe..497aa12d8 100644 --- a/docs/ui/calculated_columns.md +++ b/docs/ui/calculated_columns.md @@ -46,13 +46,13 @@ The calculated column would at minimum need to contain: though datatype might be surplus or could be inferred. **Question:** How would we infer datatype? How would we cater for nasty expressions like "= price _ clientName"? possible we could error. We could use -precendence in the datatypes, i.e. first column sets return value.? Or we could look for widest type in the event it was int _ double or int \* long. -It would likely be porr UX to ask the user to define the return type. +precedence in the datatypes, i.e. first column sets return value.? Or we could look for widest type in the event it was int _ double or int \* long. +It would likely be poor UX to ask the user to define the return type. ### Use in Tree'd Viewports By default calculated columns would work the same in tree'd viewports as in non-tree'd viewports. THe only caveat to that -would be when the calculated column would be a branch in the tree. In that case the column values would have to be calcuated in the +would be when the calculated column would be a branch in the tree. In that case the column values would have to be calculated in the tree building function, which may slow down tree generation for specific viewports. ### Implementation diff --git a/docs/ui/vuu_ui_features.md b/docs/ui/vuu_ui_features.md new file mode 100644 index 000000000..2e87e951b --- /dev/null +++ b/docs/ui/vuu_ui_features.md @@ -0,0 +1,61 @@ +# How Features currently work in Vuu + +# Vuu Application = Vuu Shell + Feature(s) + +A Vuu ui is an instance of the Vuu Shell, which loads content dynamically at runtime. The Shell renders the outermost chrome of the application: + +- the App Header +- the left nav +- the Context panel +- a container for the main content + +The shell creates the WebWorker from which all communication with the server will be handled. It provides the layout system, a persistence service and other application level features. COmponents implementing the business functionality of a Vuu application will be loaded dymanically (by the Shell) and rendered within the main content area. + +Feature is the term used in Vuu to describe a UI component that can be loaded into the application to provide some specific functionality. This can be as simple as a single data table that displays data from a remote Vuu server. Equally, it can be a complex component with multiple tabbed pages that orchestrates data from multiple Vuu data tables. The Feature might occupy the full content area of the app, or it may be one of multiple features assembled within a `layout`. These are decisions that can be enforced by the application developer or made available to the end user. + +## Loading Features at runtime + +Technically, a feature is an ES module with a default export which must be a React component. The module will be loaded at runtime, using standard ES module loading. In other words, the module is known to the application by a url, which will be used to load it via a dynamic import statement. + +The component that actually implements the dynamic loading is `Feature`, which can be found in the `vuu-shell` package. + +Here are props expected/supported by `Feature` + +```TypeScript +export interface FeatureProps

{ + ComponentProps?: P; + css?: string; + height?: number; + title?: string; + url: string; + width?: number; +} +``` + +The only required prop is `url`. That is the url for the JavaScript bundle that exports the featured component. Most features will also ship a css bundle. By default, features will be rendered within the Vuu UI with a header. This will dsplay the feature `title`, if provided. `ComponentProps` will be passed to the component actually rendered (i.e. the component exported by the feature bundle), so these should appropriate to that component. Vuu provide a mechanism for features to persist state between sessions. This can include props, which will be injected back into a loaded feature at construction time. + +## How are Feature bundles built - current state + +Right now, both the bundle exporting the Vuu Shell and all feature bundles are built together in a single build task. ESBuild is used for this and the code splitting features of ESBuild determine the exact breakdown of code into multiple bundles. The main application will output a bundle. Each feature is defined as an entrypoint to the build, so each feature will also output a bundle. These are the feature bundles that will be loaded dynamically. Any number of additional bundles may be created as dependencies, as ESBuild identifies opportunities for code sharing across bundles. + +## How will Feature bundles be built - future state + +It is not ideal that all bundles must be created, together with the runtime shell, in a single build. If one feature gets an update, the entire app must be rebuilt and redeployed to make this update available. Vuu is going to move to a more dynamic module system, whereby feature bundles can be built and published independently. The challenge here is managing shared depedencies. The current plan is to use Vite based `Module Federation` to achieve this. It is a system designed for exactly this scenario. This will be implemented alongside a runtime discovery mechanism, so that newly published or republished feature bundles can be indentified and surfaced in a running application. + +## What is a Vuu `View` and what is the relationship between a `View` and a `Feature` + +## How does a Feature manage data communication with the Vuu Server ? + +## How does a Vuu app know which Feature(s) to load ? + +## Getting started - how do I create a Feature ? + +## Does the Vuu Showcase support Features ? + +Yes it does. The Vuu showcase is a developer tool that allows components to be rendered in isolation, with hot module reloading for a convenient developer experience. Features are a little more complex to render here because of the injection of props by the Vuu Shell +and the fact that many features will create Vuu datasources. When running in the Showcase, we might want to use local datasources with no requirement to have a Vuu server instance running. All of this can be achieved and existing Showcase examples demonstrate how this is done. + +### dataSource creation/injection in Showcase features + +Most features will create Vuu dataSource(s) internally. However there is a pattern for dataSource creation, descibed above. Features should use the session state service provided by the Vuu shell to manage any dataSources created. The Showcase can take advantage of this pattern by pre-populating the session store with dataSources. The Feature, when loaded will then use these dataSources rather than creating. +In the existing Showcase examples, there is a `features` folder. The components in here are wrappers round actual VuuFeatures implemented in sample-apps. These render the actual underlying feature but only after creating local versions of the dataSource(s) required by those features and storing them in session state. WHen these features are rendered in SHowcase examples, they will be using the local test data. diff --git a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.css b/docs/ui/vuu_ui_shell.md similarity index 100% rename from vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.css rename to docs/ui/vuu_ui_shell.md diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index e939ed9ae..32677f3bf 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -11,6 +11,7 @@ import { DataSourceRow } from "@finos/vuu-data-types"; import { ClientToServerEditRpc, ClientToServerMenuRPC, + ClientToServerViewportRpcCall, VuuMenu, VuuRange, VuuRowDataItemType, @@ -28,18 +29,21 @@ export interface TickingArrayDataSourceConstructorProps extends Omit { data?: Array; menu?: VuuMenu; + menuRpcServices?: RpcService[]; rpcServices?: RpcService[]; table?: Table; updateGenerator?: UpdateGenerator; } export class TickingArrayDataSource extends ArrayDataSource { + #menuRpcServices: RpcService[] | undefined; #rpcServices: RpcService[] | undefined; #updateGenerator: UpdateGenerator | undefined; #table?: Table; constructor({ data, + menuRpcServices, rpcServices, table, updateGenerator, @@ -54,6 +58,7 @@ export class TickingArrayDataSource extends ArrayDataSource { data: data ?? table?.data ?? [], }); this._menu = menu; + this.#menuRpcServices = menuRpcServices; this.#rpcServices = rpcServices; this.#updateGenerator = updateGenerator; this.#table = table; @@ -153,6 +158,23 @@ export class TickingArrayDataSource extends ArrayDataSource { return Promise.resolve(true); } + async rpcCall( + rpcRequest: Omit + ) { + const rpcService = this.#rpcServices?.find( + (service) => + service.rpcName === + (rpcRequest as ClientToServerViewportRpcCall).rpcName + ); + if (rpcService) { + return rpcService.service({ + ...rpcRequest, + }); + } else { + console.log(`no implementation for PRC service ${rpcRequest.rpcName}`); + } + } + async menuRpcCall( rpcRequest: Omit | ClientToServerEditRpc ): Promise< diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts index d92ce20ce..383d17617 100644 --- a/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-module.ts @@ -7,7 +7,11 @@ import ftse from "./reference-data/ftse100"; import nasdaq from "./reference-data/nasdaq100"; import sp500 from "./reference-data/sp500"; import hsi from "./reference-data/hsi"; -import { VuuMenu, VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { + ClientToServerViewportRpcCall, + VuuMenu, + VuuRowDataItemType, +} from "@finos/vuu-protocol-types"; import { Table } from "../Table"; // This is a 'local' columnMap @@ -220,13 +224,11 @@ function createTradingBasket(basketId: string, basketName: string) { }); } -async function createNewBasket(rpcRequest: any) { - const { basketName, selectedRows } = rpcRequest; - if (selectedRows.length === 1) { - const [row] = selectedRows; - const basketId = row[KEY]; - createTradingBasket(basketId, basketName); - } +async function createNewBasket(rpcRequest: ClientToServerViewportRpcCall) { + const { + params: [basketId, basketName], + } = rpcRequest; + createTradingBasket(basketId, basketName); } //------------------- @@ -298,7 +300,7 @@ const services: Record = { algoType: undefined, basket: [ { - rpcName: "CREATE_NEW_BASKET", + rpcName: "createBasket", service: createNewBasket, }, ], 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 a22887943..b6e6d6274 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 @@ -34,7 +34,6 @@ export const schemas: Readonly< columns: [ { name: "basketId", serverDataType: "string" }, { name: "change", serverDataType: "string" }, - // this column doesn't exist on Vuu server { name: "description", serverDataType: "string" }, { name: "lastTrade", serverDataType: "string" }, { name: "ric", serverDataType: "string" }, diff --git a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts index eca7f5a2f..7c4401de8 100644 --- a/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-module.ts @@ -52,7 +52,11 @@ export const populateArray = (tableName: SimulTableName, count: number) => { const getColumnDescriptors = (tableName: SimulTableName) => { const schema = schemas[tableName]; - return schema.columns; + if (schema) { + return schema.columns; + } else { + console.error(`simul-module no schema found for table SIMUL ${tableName}`); + } }; const createDataSource = (tableName: SimulTableName) => { 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 4bf28e993..3aa67fe47 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 @@ -101,7 +101,6 @@ export class ArrayDataSource private groupMap: undefined | GroupMap; /** the index of key field within raw data row */ private key: number; - private tableSchema: TableSchema; private lastRangeServed: VuuRange = { from: 0, to: 0 }; private rangeChangeRowset: "delta" | "full"; private openTreeNodes: string[] = []; @@ -120,6 +119,7 @@ export class ArrayDataSource protected _menu: VuuMenu | undefined; protected selectedRows: Selection = []; + public tableSchema: TableSchema; public viewport: string; private keys = new KeySet(this.#range); diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index f5e3f8c0b..423db1258 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -24,6 +24,7 @@ import { EventEmitter } from "@finos/vuu-utils"; import { TableSchema } from "./message-utils"; import { MenuRpcResponse, + ViewportRpcResponse, VuuUIMessageInRPCEditReject, VuuUIMessageInRPCEditResponse, } from "./vuuUIMessageTypes"; @@ -495,7 +496,8 @@ export type DataSourceInsertHandler = ( export type RpcResponse = | MenuRpcResponse | VuuUIMessageInRPCEditReject - | VuuUIMessageInRPCEditResponse; + | VuuUIMessageInRPCEditResponse + | ViewportRpcResponse; export type RpcResponseHandler = (response: RpcResponse) => boolean; @@ -556,9 +558,9 @@ export interface DataSource extends EventEmitter { menuRpcCall: ( rpcRequest: Omit | ClientToServerEditRpc ) => Promise; - rpcCall?: ( + rpcCall?: ( message: Omit - ) => Promise; + ) => Promise; openTreeNode: (key: string) => void; range: VuuRange; select: SelectionChangeHandler; diff --git a/vuu-ui/packages/vuu-data/src/inlined-worker.js b/vuu-ui/packages/vuu-data/src/inlined-worker.js index 490689184..bb98726d6 100644 --- a/vuu-ui/packages/vuu-data/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data/src/inlined-worker.js @@ -1,2562 +1,8 @@ export const workerSourceCode = ` -var __accessCheck = (obj, member, msg) => { - if (!member.has(obj)) - throw TypeError("Cannot " + msg); -}; -var __privateGet = (obj, member, getter) => { - __accessCheck(obj, member, "read from private field"); - return getter ? getter.call(obj) : member.get(obj); -}; -var __privateAdd = (obj, member, value) => { - if (member.has(obj)) - throw TypeError("Cannot add the same private member more than once"); - member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -}; -var __privateSet = (obj, member, value, setter) => { - __accessCheck(obj, member, "write to private field"); - setter ? setter.call(obj, value) : member.set(obj, value); - return value; -}; - -// ../vuu-utils/src/array-utils.ts -function partition(array, test, pass = [], fail = []) { - for (let i = 0, len = array.length; i < len; i++) { - (test(array[i], i) ? pass : fail).push(array[i]); - } - return [pass, fail]; -} - -// ../vuu-utils/src/column-utils.ts -var metadataKeys = { - IDX: 0, - RENDER_IDX: 1, - IS_LEAF: 2, - IS_EXPANDED: 3, - DEPTH: 4, - COUNT: 5, - KEY: 6, - SELECTED: 7, - count: 8, - // TODO following only used in datamodel - PARENT_IDX: "parent_idx", - IDX_POINTER: "idx_pointer", - FILTER_COUNT: "filter_count", - NEXT_FILTER_IDX: "next_filter_idx" -}; -var { DEPTH, IS_LEAF } = metadataKeys; - -// ../vuu-utils/src/cookie-utils.ts -var getCookieValue = (name) => { - var _a, _b; - if (((_a = globalThis.document) == null ? void 0 : _a.cookie) !== void 0) { - return (_b = globalThis.document.cookie.split("; ").find((row) => row.startsWith(\`\${name}=\`))) == null ? void 0 : _b.split("=")[1]; - } -}; - -// ../vuu-utils/src/range-utils.ts -function getFullRange({ from, to }, bufferSize = 0, rowCount = Number.MAX_SAFE_INTEGER) { - if (bufferSize === 0) { - if (rowCount < from) { - return { from: 0, to: 0 }; - } else { - return { from, to: Math.min(to, rowCount) }; - } - } else if (from === 0) { - return { from, to: Math.min(to + bufferSize, rowCount) }; - } else { - const rangeSize = to - from; - const buff = Math.round(bufferSize / 2); - const shortfallBefore = from - buff < 0; - const shortFallAfter = rowCount - (to + buff) < 0; - if (shortfallBefore && shortFallAfter) { - return { from: 0, to: rowCount }; - } else if (shortfallBefore) { - return { from: 0, to: rangeSize + bufferSize }; - } else if (shortFallAfter) { - return { - from: Math.max(0, rowCount - (rangeSize + bufferSize)), - to: rowCount - }; - } else { - return { from: from - buff, to: to + buff }; - } - } -} -var withinRange = (value, { from, to }) => value >= from && value < to; -var WindowRange = class { - constructor(from, to) { - this.from = from; - this.to = to; - } - isWithin(index) { - return withinRange(index, this); - } - //find the overlap of this range and a new one - overlap(from, to) { - return from >= this.to || to < this.from ? [0, 0] : [Math.max(from, this.from), Math.min(to, this.to)]; - } - copy() { - return new WindowRange(this.from, this.to); - } -}; - -// ../vuu-utils/src/DataWindow.ts -var { KEY } = metadataKeys; - -// ../vuu-utils/src/logging-utils.ts -var logLevels = ["error", "warn", "info", "debug"]; -var isValidLogLevel = (value) => typeof value === "string" && logLevels.includes(value); -var DEFAULT_LOG_LEVEL = "error"; -var NO_OP = () => void 0; -var DEFAULT_DEBUG_LEVEL = false ? "error" : "info"; -var { loggingLevel = DEFAULT_DEBUG_LEVEL } = getLoggingSettings(); -var logger = (category) => { - const debugEnabled5 = loggingLevel === "debug"; - const infoEnabled5 = debugEnabled5 || loggingLevel === "info"; - const warnEnabled = infoEnabled5 || loggingLevel === "warn"; - const errorEnabled = warnEnabled || loggingLevel === "error"; - const info5 = infoEnabled5 ? (message) => console.info(\`[\${category}] \${message}\`) : NO_OP; - const warn4 = warnEnabled ? (message) => console.warn(\`[\${category}] \${message}\`) : NO_OP; - const debug5 = debugEnabled5 ? (message) => console.debug(\`[\${category}] \${message}\`) : NO_OP; - const error4 = errorEnabled ? (message) => console.error(\`[\${category}] \${message}\`) : NO_OP; - if (false) { - return { - errorEnabled, - error: error4 - }; - } else { - return { - debugEnabled: debugEnabled5, - infoEnabled: infoEnabled5, - warnEnabled, - errorEnabled, - info: info5, - warn: warn4, - debug: debug5, - error: error4 - }; - } -}; -function getLoggingSettings() { - if (typeof loggingSettings !== "undefined") { - return loggingSettings; - } else { - return { - loggingLevel: getLoggingLevelFromCookie() - }; - } -} -function getLoggingLevelFromCookie() { - const value = getCookieValue("vuu-logging-level"); - if (isValidLogLevel(value)) { - return value; - } else { - return DEFAULT_LOG_LEVEL; - } -} - -// ../vuu-utils/src/debug-utils.ts -var { debug, debugEnabled } = logger("range-monitor"); -var RangeMonitor = class { - constructor(source) { - this.source = source; - this.range = { from: 0, to: 0 }; - this.timestamp = 0; - } - isSet() { - return this.timestamp !== 0; - } - set({ from, to }) { - const { timestamp } = this; - this.range.from = from; - this.range.to = to; - this.timestamp = performance.now(); - if (timestamp) { - debugEnabled && debug( - \`<\${this.source}> [\${from}-\${to}], \${(this.timestamp - timestamp).toFixed(0)} ms elapsed\` - ); - } else { - return 0; - } - } -}; - -// ../vuu-utils/src/event-emitter.ts -function isArrayOfListeners(listeners) { - return Array.isArray(listeners); -} -function isOnlyListener(listeners) { - return !Array.isArray(listeners); -} -var _events; -var EventEmitter = class { - constructor() { - __privateAdd(this, _events, /* @__PURE__ */ new Map()); - } - addListener(event, listener) { - const listeners = __privateGet(this, _events).get(event); - if (!listeners) { - __privateGet(this, _events).set(event, listener); - } else if (isArrayOfListeners(listeners)) { - listeners.push(listener); - } else if (isOnlyListener(listeners)) { - __privateGet(this, _events).set(event, [listeners, listener]); - } - } - removeListener(event, listener) { - if (!__privateGet(this, _events).has(event)) { - return; - } - const listenerOrListeners = __privateGet(this, _events).get(event); - let position = -1; - if (listenerOrListeners === listener) { - __privateGet(this, _events).delete(event); - } else if (Array.isArray(listenerOrListeners)) { - for (let i = length; i-- > 0; ) { - if (listenerOrListeners[i] === listener) { - position = i; - break; - } - } - if (position < 0) { - return; - } - if (listenerOrListeners.length === 1) { - listenerOrListeners.length = 0; - __privateGet(this, _events).delete(event); - } else { - listenerOrListeners.splice(position, 1); - } - } - } - removeAllListeners(event) { - if (event && __privateGet(this, _events).has(event)) { - __privateGet(this, _events).delete(event); - } else if (event === void 0) { - __privateGet(this, _events).clear(); - } - } - emit(event, ...args) { - if (__privateGet(this, _events)) { - const handler = __privateGet(this, _events).get(event); - if (handler) { - this.invokeHandler(handler, args); - } - } - } - once(event, listener) { - const handler = (...args) => { - this.removeListener(event, handler); - listener(...args); - }; - this.on(event, handler); - } - on(event, listener) { - this.addListener(event, listener); - } - hasListener(event, listener) { - const listeners = __privateGet(this, _events).get(event); - if (Array.isArray(listeners)) { - return listeners.includes(listener); - } else { - return listeners === listener; - } - } - invokeHandler(handler, args) { - if (isArrayOfListeners(handler)) { - handler.slice().forEach((listener) => this.invokeHandler(listener, args)); - } else { - switch (args.length) { - case 0: - handler(); - break; - case 1: - handler(args[0]); - break; - case 2: - handler(args[0], args[1]); - break; - default: - handler.call(null, ...args); - } - } - } -}; -_events = new WeakMap(); - -// ../vuu-utils/src/round-decimal.ts -var PUNCTUATION_STR = String.fromCharCode(8200); -var DIGIT_STR = String.fromCharCode(8199); -var Space = { - DIGIT: DIGIT_STR, - TWO_DIGITS: DIGIT_STR + DIGIT_STR, - THREE_DIGITS: DIGIT_STR + DIGIT_STR + DIGIT_STR, - FULL_PADDING: [ - null, - PUNCTUATION_STR + DIGIT_STR, - PUNCTUATION_STR + DIGIT_STR + DIGIT_STR, - PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR, - PUNCTUATION_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR - ] -}; -var LEADING_FILL = DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR + DIGIT_STR; - -// ../vuu-utils/src/json-utils.ts -var { COUNT } = metadataKeys; - -// ../vuu-utils/src/keyset.ts -var KeySet = class { - constructor(range) { - this.keys = /* @__PURE__ */ new Map(); - this.free = []; - this.nextKeyValue = 0; - this.reset(range); - } - next() { - if (this.free.length > 0) { - return this.free.pop(); - } else { - return this.nextKeyValue++; - } - } - reset({ from, to }) { - this.keys.forEach((keyValue, rowIndex) => { - if (rowIndex < from || rowIndex >= to) { - this.free.push(keyValue); - this.keys.delete(rowIndex); - } - }); - const size = to - from; - if (this.keys.size + this.free.length > size) { - this.free.length = Math.max(0, size - this.keys.size); - } - for (let rowIndex = from; rowIndex < to; rowIndex++) { - if (!this.keys.has(rowIndex)) { - const nextKeyValue = this.next(); - this.keys.set(rowIndex, nextKeyValue); - } - } - if (this.nextKeyValue > this.keys.size) { - this.nextKeyValue = this.keys.size; - } - } - keyFor(rowIndex) { - const key = this.keys.get(rowIndex); - if (key === void 0) { - console.log(\`key not found +var pe=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var d=(s,e,t)=>(pe(s,e,"read from private field"),t?t.call(s):e.get(s)),O=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},de=(s,e,t,n)=>(pe(s,e,"write to private field"),n?n.call(s,t):e.set(s,t),t);function ge(s,e,t=[],n=[]){for(let r=0,o=s.length;r{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(n=>n.startsWith(\`\${s}=\`)))==null?void 0:t.split("=")[1]};function z({from:s,to:e},t=0,n=Number.MAX_SAFE_INTEGER){if(t===0)return ns>=e&&s=this.to||ttypeof s=="string"&&ot.includes(s),at="error",k=()=>{},ut="error",{loggingLevel:A=ut}=lt(),w=s=>{let e=A==="debug",t=e||A==="info",n=t||A==="warn",r=n||A==="error",o=t?p=>console.info(\`[\${s}] \${p}\`):k,i=n?p=>console.warn(\`[\${s}] \${p}\`):k,u=e?p=>console.debug(\`[\${s}] \${p}\`):k;return{errorEnabled:r,error:r?p=>console.error(\`[\${s}] \${p}\`):k}};function lt(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:ct()}}function ct(){let s=fe("vuu-logging-level");return it(s)?s:at}var{debug:pt,debugEnabled:dt}=w("range-monitor"),U=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:n}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),n)dt&&pt(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-n).toFixed(0)} ms elapsed\`);else return 0}};function me(s){return Array.isArray(s)}function gt(s){return!Array.isArray(s)}var y,he=class{constructor(){O(this,y,new Map)}addListener(e,t){let n=d(this,y).get(e);n?me(n)?n.push(t):gt(n)&&d(this,y).set(e,[n,t]):d(this,y).set(e,t)}removeListener(e,t){if(!d(this,y).has(e))return;let n=d(this,y).get(e),r=-1;if(n===t)d(this,y).delete(e);else if(Array.isArray(n)){for(let o=length;o-- >0;)if(n[o]===t){r=o;break}if(r<0)return;n.length===1?(n.length=0,d(this,y).delete(e)):n.splice(r,1)}}removeAllListeners(e){e&&d(this,y).has(e)?d(this,y).delete(e):e===void 0&&d(this,y).clear()}emit(e,...t){if(d(this,y)){let n=d(this,y).get(e);n&&this.invokeHandler(n,t)}}once(e,t){let n=(...r)=>{this.removeListener(e,n),t(...r)};this.on(e,n)}on(e,t){this.addListener(e,t)}hasListener(e,t){let n=d(this,y).get(e);return Array.isArray(n)?n.includes(t):n===t}invokeHandler(e,t){if(me(e))e.slice().forEach(n=>this.invokeHandler(n,t));else switch(t.length){case 0:e();break;case 1:e(t[0]);break;case 2:e(t[0],t[1]);break;default:e.call(null,...t)}}};y=new WeakMap;var F=String.fromCharCode(8200),f=String.fromCharCode(8199);var Sn={DIGIT:f,TWO_DIGITS:f+f,THREE_DIGITS:f+f+f,FULL_PADDING:[null,F+f,F+f+f,F+f+f+f,F+f+f+f+f]};var wn=f+f+f+f+f+f+f+f+f;var{COUNT:\$n}=V;var N=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((r,o)=>{(o=t)&&(this.free.push(r),this.keys.delete(o))});let n=t-e;this.keys.size+this.free.length>n&&(this.free.length=Math.max(0,n-this.keys.size));for(let r=e;rthis.keys.size&&(this.nextKeyValue=this.keys.size)}keyFor(e){let t=this.keys.get(e);if(t===void 0)throw console.log(\`key not found keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`); - throw Error(\`KeySet, no key found for rowIndex \${rowIndex}\`); - } - return key; - } - toDebugString() { - return Array.from(this.keys.entries()).map((k, v) => \`\${k}=>\${v}\`).join(","); - } -}; - -// ../vuu-utils/src/row-utils.ts -var { IDX } = metadataKeys; - -// ../vuu-utils/src/selection-utils.ts -var { SELECTED } = metadataKeys; -var RowSelected = { - False: 0, - True: 1, - First: 2, - Last: 4 -}; -var rangeIncludes = (range, index) => index >= range[0] && index <= range[1]; -var SINGLE_SELECTED_ROW = RowSelected.True + RowSelected.First + RowSelected.Last; -var FIRST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.First; -var LAST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.Last; -var getSelectionStatus = (selected, itemIndex) => { - for (const item of selected) { - if (typeof item === "number") { - if (item === itemIndex) { - return SINGLE_SELECTED_ROW; - } - } else if (rangeIncludes(item, itemIndex)) { - if (itemIndex === item[0]) { - return FIRST_SELECTED_ROW_OF_BLOCK; - } else if (itemIndex === item[1]) { - return LAST_SELECTED_ROW_OF_BLOCK; - } else { - return RowSelected.True; - } - } - } - return RowSelected.False; -}; -var expandSelection = (selected) => { - if (selected.every((selectedItem) => typeof selectedItem === "number")) { - return selected; - } - const expandedSelected = []; - for (const selectedItem of selected) { - if (typeof selectedItem === "number") { - expandedSelected.push(selectedItem); - } else { - for (let i = selectedItem[0]; i <= selectedItem[1]; i++) { - expandedSelected.push(i); - } - } - } - return expandedSelected; -}; - -// ../../node_modules/html-to-image/es/util.js -var uuid = (() => { - let counter = 0; - const random = () => ( - // eslint-disable-next-line no-bitwise - \`0000\${(Math.random() * 36 ** 4 << 0).toString(36)}\`.slice(-4) - ); - return () => { - counter += 1; - return \`u\${random()}\${counter}\`; - }; -})(); - -// src/websocket-connection.ts -var { debug: debug2, debugEnabled: debugEnabled2, error, info, infoEnabled, warn } = logger( - "websocket-connection" -); -var WS = "ws"; -var isWebsocketUrl = (url) => url.startsWith(WS + "://") || url.startsWith(WS + "s://"); -var connectionAttemptStatus = {}; -var setWebsocket = Symbol("setWebsocket"); -var connectionCallback = Symbol("connectionCallback"); -async function connect(connectionString, protocol, callback, retryLimitDisconnect = 10, retryLimitStartup = 5) { - connectionAttemptStatus[connectionString] = { - status: "connecting", - connect: { - allowed: retryLimitStartup, - remaining: retryLimitStartup - }, - reconnect: { - allowed: retryLimitDisconnect, - remaining: retryLimitDisconnect - } - }; - return makeConnection(connectionString, protocol, callback); -} -async function reconnect(connection) { - throw Error("connection broken"); -} -async function makeConnection(url, protocol, callback, connection) { - const { - status: currentStatus, - connect: connectStatus, - reconnect: reconnectStatus - } = connectionAttemptStatus[url]; - const trackedStatus = currentStatus === "connecting" ? connectStatus : reconnectStatus; - try { - callback({ type: "connection-status", status: "connecting" }); - const reconnecting = typeof connection !== "undefined"; - const ws = await createWebsocket(url, protocol); - console.info( - "%c\u26A1 %cconnected", - "font-size: 24px;color: green;font-weight: bold;", - "color:green; font-size: 14px;" - ); - if (connection !== void 0) { - connection[setWebsocket](ws); - } - const websocketConnection = connection != null ? connection : new WebsocketConnection(ws, url, protocol, callback); - const status = reconnecting ? "reconnected" : "connection-open-awaiting-session"; - callback({ type: "connection-status", status }); - websocketConnection.status = status; - trackedStatus.remaining = trackedStatus.allowed; - return websocketConnection; - } catch (err) { - const retry = --trackedStatus.remaining > 0; - callback({ - type: "connection-status", - status: "disconnected", - reason: "failed to connect", - retry - }); - if (retry) { - return makeConnectionIn(url, protocol, callback, connection, 2e3); - } else { - throw Error("Failed to establish connection"); - } - } -} -var makeConnectionIn = (url, protocol, callback, connection, delay) => new Promise((resolve) => { - setTimeout(() => { - resolve(makeConnection(url, protocol, callback, connection)); - }, delay); -}); -var createWebsocket = (connectionString, protocol) => new Promise((resolve, reject) => { - const websocketUrl = isWebsocketUrl(connectionString) ? connectionString : \`wss://\${connectionString}\`; - if (infoEnabled && protocol !== void 0) { - info(\`WebSocket Protocol \${protocol == null ? void 0 : protocol.toString()}\`); - } - const ws = new WebSocket(websocketUrl, protocol); - ws.onopen = () => resolve(ws); - ws.onerror = (evt) => reject(evt); -}); -var closeWarn = () => { - warn == null ? void 0 : warn(\`Connection cannot be closed, socket not yet opened\`); -}; -var sendWarn = (msg) => { - warn == null ? void 0 : warn(\`Message cannot be sent, socket closed \${msg.body.type}\`); -}; -var parseMessage = (message) => { - try { - return JSON.parse(message); - } catch (e) { - throw Error(\`Error parsing JSON response from server \${message}\`); - } -}; -var WebsocketConnection = class { - constructor(ws, url, protocol, callback) { - this.close = closeWarn; - this.requiresLogin = true; - this.send = sendWarn; - this.status = "ready"; - this.messagesCount = 0; - this.connectionMetricsInterval = null; - this.handleWebsocketMessage = (evt) => { - const vuuMessageFromServer = parseMessage(evt.data); - this.messagesCount += 1; - if (true) { - if (debugEnabled2 && vuuMessageFromServer.body.type !== "HB") { - debug2 == null ? void 0 : debug2(\`<<< \${vuuMessageFromServer.body.type}\`); - } - } - this[connectionCallback](vuuMessageFromServer); - }; - this.url = url; - this.protocol = protocol; - this[connectionCallback] = callback; - this[setWebsocket](ws); - } - reconnect() { - reconnect(this); - } - [(connectionCallback, setWebsocket)](ws) { - const callback = this[connectionCallback]; - ws.onmessage = (evt) => { - this.status = "connected"; - ws.onmessage = this.handleWebsocketMessage; - this.handleWebsocketMessage(evt); - }; - this.connectionMetricsInterval = setInterval(() => { - callback({ - type: "connection-metrics", - messagesLength: this.messagesCount - }); - this.messagesCount = 0; - }, 2e3); - ws.onerror = () => { - error(\`\u26A1 connection error\`); - callback({ - type: "connection-status", - status: "disconnected", - reason: "error" - }); - if (this.connectionMetricsInterval) { - clearInterval(this.connectionMetricsInterval); - this.connectionMetricsInterval = null; - } - if (this.status === "connection-open-awaiting-session") { - error( - \`Websocket connection lost before Vuu session established, check websocket configuration\` - ); - } else if (this.status !== "closed") { - reconnect(this); - this.send = queue; - } - }; - ws.onclose = () => { - info == null ? void 0 : info(\`\u26A1 connection close\`); - callback({ - type: "connection-status", - status: "disconnected", - reason: "close" - }); - if (this.connectionMetricsInterval) { - clearInterval(this.connectionMetricsInterval); - this.connectionMetricsInterval = null; - } - if (this.status !== "closed") { - reconnect(this); - this.send = queue; - } - }; - const send = (msg) => { - if (true) { - if (debugEnabled2 && msg.body.type !== "HB_RESP") { - debug2 == null ? void 0 : debug2(\`>>> \${msg.body.type}\`); - } - } - ws.send(JSON.stringify(msg)); - }; - const queue = (msg) => { - info == null ? void 0 : info(\`TODO queue message until websocket reconnected \${msg.body.type}\`); - }; - this.send = send; - this.close = () => { - this.status = "closed"; - ws.close(); - this.close = closeWarn; - this.send = sendWarn; - info == null ? void 0 : info("close websocket"); - }; - } -}; - -// src/message-utils.ts -var MENU_RPC_TYPES = [ - "VIEW_PORT_MENUS_SELECT_RPC", - "VIEW_PORT_MENU_TABLE_RPC", - "VIEW_PORT_MENU_ROW_RPC", - "VIEW_PORT_MENU_CELL_RPC", - "VP_EDIT_CELL_RPC", - "VP_EDIT_ROW_RPC", - "VP_EDIT_ADD_ROW_RPC", - "VP_EDIT_DELETE_CELL_RPC", - "VP_EDIT_DELETE_ROW_RPC", - "VP_EDIT_SUBMIT_FORM_RPC" -]; -var isVuuMenuRpcRequest = (message) => MENU_RPC_TYPES.includes(message["type"]); -var isVuuRpcRequest = (message) => message["type"] === "VIEW_PORT_RPC_CALL"; -var stripRequestId = ({ - requestId, - ...rest -}) => [requestId, rest]; -var getFirstAndLastRows = (rows) => { - let firstRow = rows.at(0); - if (firstRow.updateType === "SIZE") { - if (rows.length === 1) { - return rows; - } else { - firstRow = rows.at(1); - } - } - const lastRow = rows.at(-1); - return [firstRow, lastRow]; -}; -var groupRowsByViewport = (rows) => { - const result = {}; - for (const row of rows) { - const rowsForViewport = result[row.viewPortId] || (result[row.viewPortId] = []); - rowsForViewport.push(row); - } - return result; -}; -var createSchemaFromTableMetadata = ({ - columns, - dataTypes, - key, - table -}) => { - return { - table, - columns: columns.map((col, idx) => ({ - name: col, - serverDataType: dataTypes[idx] - })), - key - }; -}; - -// src/vuuUIMessageTypes.ts -var isConnectionStatusMessage = (msg) => msg.type === "connection-status"; -var isConnectionQualityMetrics = (msg) => msg.type === "connection-metrics"; -var isViewporttMessage = (msg) => "viewport" in msg; -var isSessionTableActionMessage = (messageBody) => messageBody.type === "VIEW_PORT_MENU_RESP" && messageBody.action !== null && isSessionTable(messageBody.action.table); -var isSessionTable = (table) => { - if (table !== null && typeof table === "object" && "table" in table && "module" in table) { - return table.table.startsWith("session"); - } - return false; -}; - -// src/server-proxy/messages.ts -var CHANGE_VP_SUCCESS = "CHANGE_VP_SUCCESS"; -var CHANGE_VP_RANGE_SUCCESS = "CHANGE_VP_RANGE_SUCCESS"; -var CLOSE_TREE_NODE = "CLOSE_TREE_NODE"; -var CLOSE_TREE_SUCCESS = "CLOSE_TREE_SUCCESS"; -var CREATE_VP = "CREATE_VP"; -var DISABLE_VP = "DISABLE_VP"; -var DISABLE_VP_SUCCESS = "DISABLE_VP_SUCCESS"; -var ENABLE_VP = "ENABLE_VP"; -var ENABLE_VP_SUCCESS = "ENABLE_VP_SUCCESS"; -var GET_VP_VISUAL_LINKS = "GET_VP_VISUAL_LINKS"; -var GET_VIEW_PORT_MENUS = "GET_VIEW_PORT_MENUS"; -var HB = "HB"; -var HB_RESP = "HB_RESP"; -var LOGIN = "LOGIN"; -var OPEN_TREE_NODE = "OPEN_TREE_NODE"; -var OPEN_TREE_SUCCESS = "OPEN_TREE_SUCCESS"; -var REMOVE_VP = "REMOVE_VP"; -var RPC_RESP = "RPC_RESP"; -var SET_SELECTION_SUCCESS = "SET_SELECTION_SUCCESS"; -var TABLE_ROW = "TABLE_ROW"; - -// src/server-proxy/rpc-services.ts -var getRpcServiceModule = (service) => { - switch (service) { - case "TypeAheadRpcHandler": - return "TYPEAHEAD"; - default: - return "SIMUL"; - } -}; - -// src/server-proxy/array-backed-moving-window.ts -var EMPTY_ARRAY = []; -var log = logger("array-backed-moving-window"); -function dataIsUnchanged(newRow, existingRow) { - if (!existingRow) { - return false; - } - if (existingRow.data.length !== newRow.data.length) { - return false; - } - if (existingRow.sel !== newRow.sel) { - return false; - } - for (let i = 0; i < existingRow.data.length; i++) { - if (existingRow.data[i] !== newRow.data[i]) { - return false; - } - } - return true; -} -var _range; -var ArrayBackedMovingWindow = class { - // Note, the buffer is already accounted for in the range passed in here - constructor({ from: clientFrom, to: clientTo }, { from, to }, bufferSize) { - __privateAdd(this, _range, void 0); - this.setRowCount = (rowCount) => { - var _a; - (_a = log.info) == null ? void 0 : _a.call(log, \`setRowCount \${rowCount}\`); - if (rowCount < this.internalData.length) { - this.internalData.length = rowCount; - } - if (rowCount < this.rowCount) { - this.rowsWithinRange = 0; - const end = Math.min(rowCount, this.clientRange.to); - for (let i = this.clientRange.from; i < end; i++) { - const rowIndex = i - __privateGet(this, _range).from; - if (this.internalData[rowIndex] !== void 0) { - this.rowsWithinRange += 1; - } - } - } - this.rowCount = rowCount; - }; - this.bufferBreakout = (from, to) => { - const bufferPerimeter = this.bufferSize * 0.25; - if (__privateGet(this, _range).to - to < bufferPerimeter) { - return true; - } else if (__privateGet(this, _range).from > 0 && from - __privateGet(this, _range).from < bufferPerimeter) { - return true; - } else { - return false; - } - }; - this.bufferSize = bufferSize; - this.clientRange = new WindowRange(clientFrom, clientTo); - __privateSet(this, _range, new WindowRange(from, to)); - this.internalData = new Array(bufferSize); - this.rowsWithinRange = 0; - this.rowCount = 0; - } - get range() { - return __privateGet(this, _range); - } - // TODO we shpuld probably have a hasAllClientRowsWithinRange - get hasAllRowsWithinRange() { - return this.rowsWithinRange === this.clientRange.to - this.clientRange.from || // this.rowsWithinRange === this.range.to - this.range.from || - this.rowCount > 0 && this.clientRange.from + this.rowsWithinRange === this.rowCount; - } - // Check to see if set of rows is outside the current viewport range, indicating - // that veiwport is being scrolled quickly and server is not able to keep up. - outOfRange(firstIndex, lastIndex) { - const { from, to } = this.range; - if (lastIndex < from) { - return true; - } - if (firstIndex >= to) { - return true; - } - } - setAtIndex(row) { - const { rowIndex: index } = row; - const internalIndex = index - __privateGet(this, _range).from; - if (dataIsUnchanged(row, this.internalData[internalIndex])) { - return false; - } - const isWithinClientRange = this.isWithinClientRange(index); - if (isWithinClientRange || this.isWithinRange(index)) { - if (!this.internalData[internalIndex] && isWithinClientRange) { - this.rowsWithinRange += 1; - } - this.internalData[internalIndex] = row; - } - return isWithinClientRange; - } - getAtIndex(index) { - return __privateGet(this, _range).isWithin(index) && this.internalData[index - __privateGet(this, _range).from] != null ? this.internalData[index - __privateGet(this, _range).from] : void 0; - } - isWithinRange(index) { - return __privateGet(this, _range).isWithin(index); - } - isWithinClientRange(index) { - return this.clientRange.isWithin(index); - } - // Returns [false] or [serverDataRequired, clientRows, holdingRows] - setClientRange(from, to) { - var _a; - (_a = log.debug) == null ? void 0 : _a.call(log, \`setClientRange \${from} - \${to}\`); - const currentFrom = this.clientRange.from; - const currentTo = Math.min(this.clientRange.to, this.rowCount); - if (from === currentFrom && to === currentTo) { - return [ - false, - EMPTY_ARRAY - /*, EMPTY_ARRAY*/ - ]; - } - const originalRange = this.clientRange.copy(); - this.clientRange.from = from; - this.clientRange.to = to; - this.rowsWithinRange = 0; - for (let i = from; i < to; i++) { - const internalIndex = i - __privateGet(this, _range).from; - if (this.internalData[internalIndex]) { - this.rowsWithinRange += 1; - } - } - let clientRows = EMPTY_ARRAY; - const offset = __privateGet(this, _range).from; - if (this.hasAllRowsWithinRange) { - if (to > originalRange.to) { - const start = Math.max(from, originalRange.to); - clientRows = this.internalData.slice(start - offset, to - offset); - } else { - const end = Math.min(originalRange.from, to); - clientRows = this.internalData.slice(from - offset, end - offset); - } - } - const serverDataRequired = this.bufferBreakout(from, to); - return [serverDataRequired, clientRows]; - } - setRange(from, to) { - var _a, _b; - if (from !== __privateGet(this, _range).from || to !== __privateGet(this, _range).to) { - (_a = log.debug) == null ? void 0 : _a.call(log, \`setRange \${from} - \${to}\`); - const [overlapFrom, overlapTo] = __privateGet(this, _range).overlap(from, to); - const newData = new Array(to - from); - this.rowsWithinRange = 0; - for (let i = overlapFrom; i < overlapTo; i++) { - const data = this.getAtIndex(i); - if (data) { - const index = i - from; - newData[index] = data; - if (this.isWithinClientRange(i)) { - this.rowsWithinRange += 1; - } - } - } - this.internalData = newData; - __privateGet(this, _range).from = from; - __privateGet(this, _range).to = to; - } else { - (_b = log.debug) == null ? void 0 : _b.call(log, \`setRange \${from} - \${to} IGNORED because not changed\`); - } - } - //TODO temp - get data() { - return this.internalData; - } - getData() { - var _a; - const { from, to } = __privateGet(this, _range); - const { from: clientFrom, to: clientTo } = this.clientRange; - const startOffset = Math.max(0, clientFrom - from); - const endOffset = Math.min( - to - from, - to, - clientTo - from, - (_a = this.rowCount) != null ? _a : to - ); - return this.internalData.slice(startOffset, endOffset); - } - clear() { - var _a; - (_a = log.debug) == null ? void 0 : _a.call(log, "clear"); - this.internalData.length = 0; - this.rowsWithinRange = 0; - this.setRowCount(0); - } - // used only for debugging - getCurrentDataRange() { - const rows = this.internalData; - const len = rows.length; - let [firstRow] = this.internalData; - let lastRow = this.internalData[len - 1]; - if (firstRow && lastRow) { - return [firstRow.rowIndex, lastRow.rowIndex]; - } else { - for (let i = 0; i < len; i++) { - if (rows[i] !== void 0) { - firstRow = rows[i]; - break; - } - } - for (let i = len - 1; i >= 0; i--) { - if (rows[i] !== void 0) { - lastRow = rows[i]; - break; - } - } - if (firstRow && lastRow) { - return [firstRow.rowIndex, lastRow.rowIndex]; - } else { - return [-1, -1]; - } - } - } -}; -_range = new WeakMap(); - -// src/server-proxy/viewport.ts -var EMPTY_GROUPBY = []; -var { debug: debug3, debugEnabled: debugEnabled3, error: error2, info: info2, infoEnabled: infoEnabled2, warn: warn2 } = logger("viewport"); -var isLeafUpdate = ({ rowKey, updateType }) => updateType === "U" && !rowKey.startsWith("\$root"); -var NO_DATA_UPDATE = [ - void 0, - void 0 -]; -var NO_UPDATE_STATUS = { - count: 0, - mode: void 0, - size: 0, - ts: 0 -}; -var Viewport = class { - constructor({ - aggregations, - bufferSize = 50, - columns, - filter, - groupBy = [], - table, - range, - sort, - title, - viewport, - visualLink - }, postMessageToClient) { - /** batchMode is irrelevant for Vuu Table, it was introduced to try and improve rendering performance of AgGrid */ - this.batchMode = true; - this.hasUpdates = false; - this.pendingUpdates = []; - this.pendingOperations = /* @__PURE__ */ new Map(); - this.pendingRangeRequests = []; - this.rowCountChanged = false; - this.selectedRows = []; - this.useBatchMode = true; - this.lastUpdateStatus = NO_UPDATE_STATUS; - this.updateThrottleTimer = void 0; - this.rangeMonitor = new RangeMonitor("ViewPort"); - this.disabled = false; - this.isTree = false; - // TODO roll disabled/suspended into status - this.status = ""; - this.suspended = false; - this.suspendTimer = null; - // Records SIZE only updates - this.setLastSizeOnlyUpdateSize = (size) => { - this.lastUpdateStatus.size = size; - }; - this.setLastUpdate = (mode) => { - const { ts: lastTS, mode: lastMode } = this.lastUpdateStatus; - let elapsedTime = 0; - if (lastMode === mode) { - const ts = Date.now(); - this.lastUpdateStatus.count += 1; - this.lastUpdateStatus.ts = ts; - elapsedTime = lastTS === 0 ? 0 : ts - lastTS; - } else { - this.lastUpdateStatus.count = 1; - this.lastUpdateStatus.ts = 0; - elapsedTime = 0; - } - this.lastUpdateStatus.mode = mode; - return elapsedTime; - }; - this.rangeRequestAlreadyPending = (range) => { - const { bufferSize } = this; - const bufferThreshold = bufferSize * 0.25; - let { from: stillPendingFrom } = range; - for (const { from, to } of this.pendingRangeRequests) { - if (stillPendingFrom >= from && stillPendingFrom < to) { - if (range.to + bufferThreshold <= to) { - return true; - } else { - stillPendingFrom = to; - } - } - } - return false; - }; - this.sendThrottledSizeMessage = () => { - this.updateThrottleTimer = void 0; - this.lastUpdateStatus.count = 3; - this.postMessageToClient({ - clientViewportId: this.clientViewportId, - mode: "size-only", - size: this.lastUpdateStatus.size, - type: "viewport-update" - }); - }; - // If we are receiving multiple SIZE updates but no data, table is loading rows - // outside of our viewport. We can safely throttle these requests. Doing so will - // alleviate pressure on UI DataTable. - this.shouldThrottleMessage = (mode) => { - const elapsedTime = this.setLastUpdate(mode); - return mode === "size-only" && elapsedTime > 0 && elapsedTime < 500 && this.lastUpdateStatus.count > 3; - }; - this.throttleMessage = (mode) => { - if (this.shouldThrottleMessage(mode)) { - info2 == null ? void 0 : info2("throttling updates setTimeout to 2000"); - if (this.updateThrottleTimer === void 0) { - this.updateThrottleTimer = setTimeout( - this.sendThrottledSizeMessage, - 2e3 - ); - } - return true; - } else if (this.updateThrottleTimer !== void 0) { - clearTimeout(this.updateThrottleTimer); - this.updateThrottleTimer = void 0; - } - return false; - }; - this.getNewRowCount = () => { - if (this.rowCountChanged && this.dataWindow) { - this.rowCountChanged = false; - return this.dataWindow.rowCount; - } - }; - this.aggregations = aggregations; - this.bufferSize = bufferSize; - this.clientRange = range; - this.clientViewportId = viewport; - this.columns = columns; - this.filter = filter; - this.groupBy = groupBy; - this.keys = new KeySet(range); - this.pendingLinkedParent = visualLink; - this.table = table; - this.sort = sort; - this.title = title; - infoEnabled2 && (info2 == null ? void 0 : info2( - \`constructor #\${viewport} \${table.table} bufferSize=\${bufferSize}\` - )); - this.dataWindow = new ArrayBackedMovingWindow( - this.clientRange, - range, - this.bufferSize - ); - this.postMessageToClient = postMessageToClient; - } - get hasUpdatesToProcess() { - if (this.suspended) { - return false; - } - return this.rowCountChanged || this.hasUpdates; - } - get size() { - var _a; - return (_a = this.dataWindow.rowCount) != null ? _a : 0; - } - subscribe() { - const { filter } = this.filter; - this.status = this.status === "subscribed" ? "resubscribing" : "subscribing"; - return { - type: CREATE_VP, - table: this.table, - range: getFullRange(this.clientRange, this.bufferSize), - aggregations: this.aggregations, - columns: this.columns, - sort: this.sort, - groupBy: this.groupBy, - filterSpec: { filter } - }; - } - handleSubscribed({ - viewPortId, - aggregations, - columns, - filterSpec: filter, - range, - sort, - groupBy - }, tableSchema) { - this.serverViewportId = viewPortId; - this.status = "subscribed"; - this.aggregations = aggregations; - this.columns = columns; - this.groupBy = groupBy; - this.isTree = groupBy && groupBy.length > 0; - this.dataWindow.setRange(range.from, range.to); - return { - aggregations, - type: "subscribed", - clientViewportId: this.clientViewportId, - columns, - filter, - groupBy, - range, - sort, - tableSchema - }; - } - awaitOperation(requestId, msg) { - this.pendingOperations.set(requestId, msg); - } - // Return a message if we need to communicate this to client UI - completeOperation(requestId, ...params) { - var _a; - const { clientViewportId, pendingOperations } = this; - const pendingOperation = pendingOperations.get(requestId); - if (!pendingOperation) { - error2( - \`no matching operation found to complete for requestId \${requestId}\` - ); - return; - } - const { type } = pendingOperation; - info2 == null ? void 0 : info2(\`completeOperation \${type}\`); - pendingOperations.delete(requestId); - if (type === "CHANGE_VP_RANGE") { - const [from, to] = params; - (_a = this.dataWindow) == null ? void 0 : _a.setRange(from, to); - for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { - const pendingRangeRequest = this.pendingRangeRequests[i]; - if (pendingRangeRequest.requestId === requestId) { - pendingRangeRequest.acked = true; - break; - } else { - warn2 == null ? void 0 : warn2("range requests sent faster than they are being ACKed"); - } - } - } else if (type === "config") { - const { aggregations, columns, filter, groupBy, sort } = pendingOperation.data; - this.aggregations = aggregations; - this.columns = columns; - this.filter = filter; - this.groupBy = groupBy; - this.sort = sort; - if (groupBy.length > 0) { - this.isTree = true; - } else if (this.isTree) { - this.isTree = false; - } - debug3 == null ? void 0 : debug3(\`config change confirmed, isTree : \${this.isTree}\`); - return { - clientViewportId, - type, - config: pendingOperation.data - }; - } else if (type === "groupBy") { - this.isTree = pendingOperation.data.length > 0; - this.groupBy = pendingOperation.data; - debug3 == null ? void 0 : debug3(\`groupBy change confirmed, isTree : \${this.isTree}\`); - return { - clientViewportId, - type, - groupBy: pendingOperation.data - }; - } else if (type === "columns") { - this.columns = pendingOperation.data; - return { - clientViewportId, - type, - columns: pendingOperation.data - }; - } else if (type === "filter") { - this.filter = pendingOperation.data; - return { - clientViewportId, - type, - filter: pendingOperation.data - }; - } else if (type === "aggregate") { - this.aggregations = pendingOperation.data; - return { - clientViewportId, - type: "aggregate", - aggregations: this.aggregations - }; - } else if (type === "sort") { - this.sort = pendingOperation.data; - return { - clientViewportId, - type, - sort: this.sort - }; - } else if (type === "selection") { - } else if (type === "disable") { - this.disabled = true; - return { - type: "disabled", - clientViewportId - }; - } else if (type === "enable") { - this.disabled = false; - return { - type: "enabled", - clientViewportId - }; - } else if (type === "CREATE_VISUAL_LINK") { - const [colName, parentViewportId, parentColName] = params; - this.linkedParent = { - colName, - parentViewportId, - parentColName - }; - this.pendingLinkedParent = void 0; - return { - type: "vuu-link-created", - clientViewportId, - colName, - parentViewportId, - parentColName - }; - } else if (type === "REMOVE_VISUAL_LINK") { - this.linkedParent = void 0; - return { - type: "vuu-link-removed", - clientViewportId - }; - } - } - // TODO when a range request arrives, consider the viewport to be scrolling - // until data arrives and we have the full range. - // When not scrolling, any server data is an update - // When scrolling, we are in batch mode - rangeRequest(requestId, range) { - if (debugEnabled3) { - this.rangeMonitor.set(range); - } - const type = "CHANGE_VP_RANGE"; - if (this.dataWindow) { - const [serverDataRequired, clientRows] = this.dataWindow.setClientRange( - range.from, - range.to - ); - let debounceRequest; - const maxRange = this.dataWindow.rowCount || void 0; - const serverRequest = serverDataRequired && !this.rangeRequestAlreadyPending(range) ? { - type, - viewPortId: this.serverViewportId, - ...getFullRange(range, this.bufferSize, maxRange) - } : null; - if (serverRequest) { - debugEnabled3 && (debug3 == null ? void 0 : debug3( - \`create CHANGE_VP_RANGE: [\${serverRequest.from} - \${serverRequest.to}]\` - )); - this.awaitOperation(requestId, { type }); - const pendingRequest = this.pendingRangeRequests.at(-1); - if (pendingRequest) { - if (pendingRequest.acked) { - console.warn("Range Request before previous request is filled"); - } else { - const { from, to } = pendingRequest; - if (this.dataWindow.outOfRange(from, to)) { - debounceRequest = { - clientViewportId: this.clientViewportId, - type: "debounce-begin" - }; - } else { - warn2 == null ? void 0 : warn2("Range Request before previous request is acked"); - } - } - } - this.pendingRangeRequests.push({ ...serverRequest, requestId }); - if (this.useBatchMode) { - this.batchMode = true; - } - } else if (clientRows.length > 0) { - this.batchMode = false; - } - this.keys.reset(this.dataWindow.clientRange); - const toClient = this.isTree ? toClientRowTree : toClientRow; - if (clientRows.length) { - return [ - serverRequest, - clientRows.map((row) => { - return toClient(row, this.keys, this.selectedRows); - }) - ]; - } else if (debounceRequest) { - return [serverRequest, void 0, debounceRequest]; - } else { - return [serverRequest]; - } - } else { - return [null]; - } - } - setLinks(links) { - this.links = links; - return [ - { - type: "vuu-links", - links, - clientViewportId: this.clientViewportId - }, - this.pendingLinkedParent - ]; - } - setMenu(menu) { - return { - type: "vuu-menu", - menu, - clientViewportId: this.clientViewportId - }; - } - openTreeNode(requestId, message) { - if (this.useBatchMode) { - this.batchMode = true; - } - return { - type: OPEN_TREE_NODE, - vpId: this.serverViewportId, - treeKey: message.key - }; - } - closeTreeNode(requestId, message) { - if (this.useBatchMode) { - this.batchMode = true; - } - return { - type: CLOSE_TREE_NODE, - vpId: this.serverViewportId, - treeKey: message.key - }; - } - createLink(requestId, colName, parentVpId, parentColumnName) { - const message = { - type: "CREATE_VISUAL_LINK", - parentVpId, - childVpId: this.serverViewportId, - parentColumnName, - childColumnName: colName - }; - this.awaitOperation(requestId, message); - if (this.useBatchMode) { - this.batchMode = true; - } - return message; - } - removeLink(requestId) { - const message = { - type: "REMOVE_VISUAL_LINK", - childVpId: this.serverViewportId - }; - this.awaitOperation(requestId, message); - return message; - } - suspend() { - this.suspended = true; - info2 == null ? void 0 : info2("suspend"); - } - resume() { - this.suspended = false; - if (debugEnabled3) { - debug3 == null ? void 0 : debug3(\`resume: \${this.currentData()}\`); - } - return [this.size, this.currentData()]; - } - currentData() { - const out = []; - if (this.dataWindow) { - const records = this.dataWindow.getData(); - const { keys } = this; - const toClient = this.isTree ? toClientRowTree : toClientRow; - for (const row of records) { - if (row) { - out.push(toClient(row, keys, this.selectedRows)); - } - } - } - return out; - } - enable(requestId) { - this.awaitOperation(requestId, { type: "enable" }); - info2 == null ? void 0 : info2(\`enable: \${this.serverViewportId}\`); - return { - type: ENABLE_VP, - viewPortId: this.serverViewportId - }; - } - disable(requestId) { - this.awaitOperation(requestId, { type: "disable" }); - info2 == null ? void 0 : info2(\`disable: \${this.serverViewportId}\`); - this.suspended = false; - return { - type: DISABLE_VP, - viewPortId: this.serverViewportId - }; - } - columnRequest(requestId, columns) { - this.awaitOperation(requestId, { - type: "columns", - data: columns - }); - debug3 == null ? void 0 : debug3(\`columnRequest: \${columns}\`); - return this.createRequest({ columns }); - } - filterRequest(requestId, dataSourceFilter) { - this.awaitOperation(requestId, { - type: "filter", - data: dataSourceFilter - }); - if (this.useBatchMode) { - this.batchMode = true; - } - const { filter } = dataSourceFilter; - info2 == null ? void 0 : info2(\`filterRequest: \${filter}\`); - return this.createRequest({ filterSpec: { filter } }); - } - setConfig(requestId, config) { - this.awaitOperation(requestId, { type: "config", data: config }); - const { filter, ...remainingConfig } = config; - if (this.useBatchMode) { - this.batchMode = true; - } - debugEnabled3 ? debug3 == null ? void 0 : debug3(\`setConfig \${JSON.stringify(config)}\`) : info2 == null ? void 0 : info2(\`setConfig\`); - return this.createRequest( - { - ...remainingConfig, - filterSpec: typeof (filter == null ? void 0 : filter.filter) === "string" ? { - filter: filter.filter - } : { - filter: "" - } - }, - true - ); - } - aggregateRequest(requestId, aggregations) { - this.awaitOperation(requestId, { type: "aggregate", data: aggregations }); - info2 == null ? void 0 : info2(\`aggregateRequest: \${aggregations}\`); - return this.createRequest({ aggregations }); - } - sortRequest(requestId, sort) { - this.awaitOperation(requestId, { type: "sort", data: sort }); - info2 == null ? void 0 : info2(\`sortRequest: \${JSON.stringify(sort.sortDefs)}\`); - return this.createRequest({ sort }); - } - groupByRequest(requestId, groupBy = EMPTY_GROUPBY) { - var _a; - this.awaitOperation(requestId, { type: "groupBy", data: groupBy }); - if (this.useBatchMode) { - this.batchMode = true; - } - if (!this.isTree) { - (_a = this.dataWindow) == null ? void 0 : _a.clear(); - } - return this.createRequest({ groupBy }); - } - selectRequest(requestId, selected) { - this.selectedRows = selected; - this.awaitOperation(requestId, { type: "selection", data: selected }); - info2 == null ? void 0 : info2(\`selectRequest: \${selected}\`); - return { - type: "SET_SELECTION", - vpId: this.serverViewportId, - selection: expandSelection(selected) - }; - } - removePendingRangeRequest(firstIndex, lastIndex) { - for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { - const { from, to } = this.pendingRangeRequests[i]; - let isLast = true; - if (firstIndex >= from && firstIndex < to || lastIndex > from && lastIndex < to) { - if (!isLast) { - console.warn( - "removePendingRangeRequest TABLE_ROWS are not for latest request" - ); - } - this.pendingRangeRequests.splice(i, 1); - break; - } else { - isLast = false; - } - } - } - updateRows(rows) { - var _a, _b, _c; - const [firstRow, lastRow] = getFirstAndLastRows(rows); - if (firstRow && lastRow) { - this.removePendingRangeRequest(firstRow.rowIndex, lastRow.rowIndex); - } - if (rows.length === 1) { - if (firstRow.vpSize === 0 && this.disabled) { - debug3 == null ? void 0 : debug3( - \`ignore a SIZE=0 message on disabled viewport (\${rows.length} rows)\` - ); - return; - } else if (firstRow.updateType === "SIZE") { - this.setLastSizeOnlyUpdateSize(firstRow.vpSize); - } - } - for (const row of rows) { - if (this.isTree && isLeafUpdate(row)) { - continue; - } else { - if (row.updateType === "SIZE" || ((_a = this.dataWindow) == null ? void 0 : _a.rowCount) !== row.vpSize) { - (_b = this.dataWindow) == null ? void 0 : _b.setRowCount(row.vpSize); - this.rowCountChanged = true; - } - if (row.updateType === "U") { - if ((_c = this.dataWindow) == null ? void 0 : _c.setAtIndex(row)) { - this.hasUpdates = true; - if (!this.batchMode) { - this.pendingUpdates.push(row); - } - } - } - } - } - } - // This is called only after new data has been received from server - data - // returned direcly from buffer does not use this. - getClientRows() { - let out = void 0; - let mode = "size-only"; - if (!this.hasUpdates && !this.rowCountChanged) { - return NO_DATA_UPDATE; - } - if (this.hasUpdates) { - const { keys, selectedRows } = this; - const toClient = this.isTree ? toClientRowTree : toClientRow; - if (this.updateThrottleTimer) { - self.clearTimeout(this.updateThrottleTimer); - this.updateThrottleTimer = void 0; - } - if (this.pendingUpdates.length > 0) { - out = []; - mode = "update"; - for (const row of this.pendingUpdates) { - out.push(toClient(row, keys, selectedRows)); - } - this.pendingUpdates.length = 0; - } else { - const records = this.dataWindow.getData(); - if (this.dataWindow.hasAllRowsWithinRange) { - out = []; - mode = "batch"; - for (const row of records) { - out.push(toClient(row, keys, selectedRows)); - } - this.batchMode = false; - } - } - this.hasUpdates = false; - } - if (this.throttleMessage(mode)) { - return NO_DATA_UPDATE; - } else { - return [out, mode]; - } - } - createRequest(params, overWrite = false) { - if (overWrite) { - return { - type: "CHANGE_VP", - viewPortId: this.serverViewportId, - ...params - }; - } else { - return { - type: "CHANGE_VP", - viewPortId: this.serverViewportId, - aggregations: this.aggregations, - columns: this.columns, - sort: this.sort, - groupBy: this.groupBy, - filterSpec: { - filter: this.filter.filter - }, - ...params - }; - } - } -}; -var toClientRow = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { - return [ - rowIndex, - keys.keyFor(rowIndex), - true, - false, - 0, - 0, - rowKey, - isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 - ].concat(data); -}; -var toClientRowTree = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { - const [depth, isExpanded, , isLeaf, , count, ...rest] = data; - return [ - rowIndex, - keys.keyFor(rowIndex), - isLeaf, - isExpanded, - depth, - count, - rowKey, - isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 - ].concat(rest); -}; - -// src/server-proxy/server-proxy.ts -var _requestId = 1; -var { debug: debug4, debugEnabled: debugEnabled4, error: error3, info: info3, infoEnabled: infoEnabled3, warn: warn3 } = logger("server-proxy"); -var nextRequestId = () => \`\${_requestId++}\`; -var DEFAULT_OPTIONS = {}; -var isActiveViewport = (viewPort) => viewPort.disabled !== true && viewPort.suspended !== true; -var NO_ACTION = { - type: "NO_ACTION" -}; -var addTitleToLinks = (links, serverViewportId, label) => links.map( - (link) => link.parentVpId === serverViewportId ? { ...link, label } : link -); -function addLabelsToLinks(links, viewports) { - return links.map((linkDescriptor) => { - const { parentVpId } = linkDescriptor; - const viewport = viewports.get(parentVpId); - if (viewport) { - return { - ...linkDescriptor, - parentClientVpId: viewport.clientViewportId, - label: viewport.title - }; - } else { - throw Error("addLabelsToLinks viewport not found"); - } - }); -} -var ServerProxy = class { - constructor(connection, callback) { - this.authToken = ""; - this.user = "user"; - this.pendingRequests = /* @__PURE__ */ new Map(); - this.queuedRequests = []; - this.cachedTableMetaRequests = /* @__PURE__ */ new Map(); - this.cachedTableSchemas = /* @__PURE__ */ new Map(); - this.connection = connection; - this.postMessageToClient = callback; - this.viewports = /* @__PURE__ */ new Map(); - this.mapClientToServerViewport = /* @__PURE__ */ new Map(); - } - async reconnect() { - await this.login(this.authToken); - const [activeViewports, inactiveViewports] = partition( - Array.from(this.viewports.values()), - isActiveViewport - ); - this.viewports.clear(); - this.mapClientToServerViewport.clear(); - const reconnectViewports = (viewports) => { - viewports.forEach((viewport) => { - const { clientViewportId } = viewport; - this.viewports.set(clientViewportId, viewport); - this.sendMessageToServer(viewport.subscribe(), clientViewportId); - }); - }; - reconnectViewports(activeViewports); - setTimeout(() => { - reconnectViewports(inactiveViewports); - }, 2e3); - } - async login(authToken, user = "user") { - if (authToken) { - this.authToken = authToken; - this.user = user; - return new Promise((resolve, reject) => { - this.sendMessageToServer( - { type: LOGIN, token: this.authToken, user }, - "" - ); - this.pendingLogin = { resolve, reject }; - }); - } else if (this.authToken === "") { - error3("login, cannot login until auth token has been obtained"); - } - } - subscribe(message) { - if (!this.mapClientToServerViewport.has(message.viewport)) { - const pendingTableSchema = this.getTableMeta(message.table); - const viewport = new Viewport(message, this.postMessageToClient); - this.viewports.set(message.viewport, viewport); - const pendingSubscription = this.awaitResponseToMessage( - viewport.subscribe(), - message.viewport - ); - const awaitPendingReponses = Promise.all([ - pendingSubscription, - pendingTableSchema - ]); - awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { - const { viewPortId: serverViewportId } = subscribeResponse; - const { status: viewportStatus } = viewport; - if (message.viewport !== serverViewportId) { - this.viewports.delete(message.viewport); - this.viewports.set(serverViewportId, viewport); - } - this.mapClientToServerViewport.set(message.viewport, serverViewportId); - const clientResponse = viewport.handleSubscribed( - subscribeResponse, - tableSchema - ); - if (clientResponse) { - this.postMessageToClient(clientResponse); - if (debugEnabled4) { - debug4( - \`post DataSourceSubscribedMessage to client: \${JSON.stringify( - clientResponse - )}\` - ); - } - } - if (viewport.disabled) { - this.disableViewport(viewport); - } - if (viewportStatus === "subscribing" && // A session table will never have Visual Links, nor Context Menus - !isSessionTable(viewport.table)) { - this.sendMessageToServer({ - type: GET_VP_VISUAL_LINKS, - vpId: serverViewportId - }); - this.sendMessageToServer({ - type: GET_VIEW_PORT_MENUS, - vpId: serverViewportId - }); - Array.from(this.viewports.entries()).filter( - ([id, { disabled }]) => id !== serverViewportId && !disabled - ).forEach(([vpId]) => { - this.sendMessageToServer({ - type: GET_VP_VISUAL_LINKS, - vpId - }); - }); - } - }); - } else { - error3(\`spurious subscribe call \${message.viewport}\`); - } - } - unsubscribe(clientViewportId) { - const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); - if (serverViewportId) { - info3 == null ? void 0 : info3( - \`Unsubscribe Message (Client to Server): - \${serverViewportId}\` - ); - this.sendMessageToServer({ - type: REMOVE_VP, - viewPortId: serverViewportId - }); - } else { - error3( - \`failed to unsubscribe client viewport \${clientViewportId}, viewport not found\` - ); - } - } - getViewportForClient(clientViewportId, throws = true) { - const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); - if (serverViewportId) { - const viewport = this.viewports.get(serverViewportId); - if (viewport) { - return viewport; - } else if (throws) { - throw Error( - \`Viewport not found for client viewport \${clientViewportId}\` - ); - } else { - return null; - } - } else if (this.viewports.has(clientViewportId)) { - return this.viewports.get(clientViewportId); - } else if (throws) { - throw Error( - \`Viewport server id not found for client viewport \${clientViewportId}\` - ); - } else { - return null; - } - } - /**********************************************************************/ - /* Handle messages from client */ - /**********************************************************************/ - setViewRange(viewport, message) { - const requestId = nextRequestId(); - const [serverRequest, rows, debounceRequest] = viewport.rangeRequest( - requestId, - message.range - ); - info3 == null ? void 0 : info3(\`setViewRange \${message.range.from} - \${message.range.to}\`); - if (serverRequest) { - if (true) { - info3 == null ? void 0 : info3( - \`CHANGE_VP_RANGE [\${message.range.from}-\${message.range.to}] => [\${serverRequest.from}-\${serverRequest.to}]\` - ); - } - this.sendIfReady( - serverRequest, - requestId, - viewport.status === "subscribed" - ); - } - if (rows) { - info3 == null ? void 0 : info3(\`setViewRange \${rows.length} rows returned from cache\`); - this.postMessageToClient({ - mode: "batch", - type: "viewport-update", - clientViewportId: viewport.clientViewportId, - rows - }); - } else if (debounceRequest) { - this.postMessageToClient(debounceRequest); - } - } - setConfig(viewport, message) { - const requestId = nextRequestId(); - const request = viewport.setConfig(requestId, message.config); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - aggregate(viewport, message) { - const requestId = nextRequestId(); - const request = viewport.aggregateRequest(requestId, message.aggregations); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - sort(viewport, message) { - const requestId = nextRequestId(); - const request = viewport.sortRequest(requestId, message.sort); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - groupBy(viewport, message) { - const requestId = nextRequestId(); - const request = viewport.groupByRequest(requestId, message.groupBy); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - filter(viewport, message) { - const requestId = nextRequestId(); - const { filter } = message; - const request = viewport.filterRequest(requestId, filter); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - setColumns(viewport, message) { - const requestId = nextRequestId(); - const { columns } = message; - const request = viewport.columnRequest(requestId, columns); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - setTitle(viewport, message) { - if (viewport) { - viewport.title = message.title; - this.updateTitleOnVisualLinks(viewport); - } - } - select(viewport, message) { - const requestId = nextRequestId(); - const { selected } = message; - const request = viewport.selectRequest(requestId, selected); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - disableViewport(viewport) { - const requestId = nextRequestId(); - const request = viewport.disable(requestId); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - enableViewport(viewport) { - if (viewport.disabled) { - const requestId = nextRequestId(); - const request = viewport.enable(requestId); - this.sendIfReady(request, requestId, viewport.status === "subscribed"); - } - } - suspendViewport(viewport) { - viewport.suspend(); - viewport.suspendTimer = setTimeout(() => { - info3 == null ? void 0 : info3("suspendTimer expired, escalate suspend to disable"); - this.disableViewport(viewport); - }, 3e3); - } - resumeViewport(viewport) { - if (viewport.suspendTimer) { - debug4 == null ? void 0 : debug4("clear suspend timer"); - clearTimeout(viewport.suspendTimer); - viewport.suspendTimer = null; - } - const [size, rows] = viewport.resume(); - debug4 == null ? void 0 : debug4(\`resumeViewport size \${size}, \${rows.length} rows sent to client\`); - this.postMessageToClient({ - clientViewportId: viewport.clientViewportId, - mode: "batch", - rows, - size, - type: "viewport-update" - }); - } - openTreeNode(viewport, message) { - if (viewport.serverViewportId) { - const requestId = nextRequestId(); - this.sendIfReady( - viewport.openTreeNode(requestId, message), - requestId, - viewport.status === "subscribed" - ); - } - } - closeTreeNode(viewport, message) { - if (viewport.serverViewportId) { - const requestId = nextRequestId(); - this.sendIfReady( - viewport.closeTreeNode(requestId, message), - requestId, - viewport.status === "subscribed" - ); - } - } - createLink(viewport, message) { - const { parentClientVpId, parentColumnName, childColumnName } = message; - const requestId = nextRequestId(); - const parentVpId = this.mapClientToServerViewport.get(parentClientVpId); - if (parentVpId) { - const request = viewport.createLink( - requestId, - childColumnName, - parentVpId, - parentColumnName - ); - this.sendMessageToServer(request, requestId); - } else { - error3("ServerProxy unable to create link, viewport not found"); - } - } - removeLink(viewport) { - const requestId = nextRequestId(); - const request = viewport.removeLink(requestId); - this.sendMessageToServer(request, requestId); - } - updateTitleOnVisualLinks(viewport) { - var _a; - const { serverViewportId, title } = viewport; - for (const vp of this.viewports.values()) { - if (vp !== viewport && vp.links && serverViewportId && title) { - if ((_a = vp.links) == null ? void 0 : _a.some((link) => link.parentVpId === serverViewportId)) { - const [messageToClient] = vp.setLinks( - addTitleToLinks(vp.links, serverViewportId, title) - ); - this.postMessageToClient(messageToClient); - } - } - } - } - removeViewportFromVisualLinks(serverViewportId) { - var _a; - for (const vp of this.viewports.values()) { - if ((_a = vp.links) == null ? void 0 : _a.some(({ parentVpId }) => parentVpId === serverViewportId)) { - const [messageToClient] = vp.setLinks( - vp.links.filter(({ parentVpId }) => parentVpId !== serverViewportId) - ); - this.postMessageToClient(messageToClient); - } - } - } - menuRpcCall(message) { - const viewport = this.getViewportForClient(message.vpId, false); - if (viewport == null ? void 0 : viewport.serverViewportId) { - const [requestId, rpcRequest] = stripRequestId(message); - this.sendMessageToServer( - { - ...rpcRequest, - vpId: viewport.serverViewportId - }, - requestId - ); - } - } - viewportRpcCall(message) { - const viewport = this.getViewportForClient(message.vpId, false); - if (viewport == null ? void 0 : viewport.serverViewportId) { - const [requestId, rpcRequest] = stripRequestId(message); - this.sendMessageToServer( - { - ...rpcRequest, - vpId: viewport.serverViewportId, - namedParams: {} - }, - requestId - ); - } - } - rpcCall(message) { - const [requestId, rpcRequest] = stripRequestId(message); - const module = getRpcServiceModule(rpcRequest.service); - this.sendMessageToServer(rpcRequest, requestId, { module }); - } - handleMessageFromClient(message) { - var _a; - if (isViewporttMessage(message)) { - if (message.type === "disable") { - const viewport = this.getViewportForClient(message.viewport, false); - if (viewport !== null) { - return this.disableViewport(viewport); - } else { - return; - } - } else { - const viewport = this.getViewportForClient(message.viewport); - switch (message.type) { - case "setViewRange": - return this.setViewRange(viewport, message); - case "config": - return this.setConfig(viewport, message); - case "aggregate": - return this.aggregate(viewport, message); - case "sort": - return this.sort(viewport, message); - case "groupBy": - return this.groupBy(viewport, message); - case "filter": - return this.filter(viewport, message); - case "select": - return this.select(viewport, message); - case "suspend": - return this.suspendViewport(viewport); - case "resume": - return this.resumeViewport(viewport); - case "enable": - return this.enableViewport(viewport); - case "openTreeNode": - return this.openTreeNode(viewport, message); - case "closeTreeNode": - return this.closeTreeNode(viewport, message); - case "createLink": - return this.createLink(viewport, message); - case "removeLink": - return this.removeLink(viewport); - case "setColumns": - return this.setColumns(viewport, message); - case "setTitle": - return this.setTitle(viewport, message); - default: - } - } - } else if (isVuuRpcRequest(message)) { - return this.viewportRpcCall(message); - } else if (isVuuMenuRpcRequest(message)) { - return this.menuRpcCall(message); - } else { - const { type, requestId } = message; - switch (type) { - case "GET_TABLE_LIST": { - (_a = this.tableList) != null ? _a : this.tableList = this.awaitResponseToMessage( - { type }, - requestId - ); - this.tableList.then((response) => { - this.postMessageToClient({ - type: "TABLE_LIST_RESP", - tables: response.tables, - requestId - }); - }); - return; - } - case "GET_TABLE_META": { - this.getTableMeta(message.table, requestId).then((tableSchema) => { - if (tableSchema) { - this.postMessageToClient({ - type: "TABLE_META_RESP", - tableSchema, - requestId - }); - } - }); - return; - } - case "RPC_CALL": - return this.rpcCall(message); - default: - } - } - error3( - \`Vuu ServerProxy Unexpected message from client \${JSON.stringify( - message - )}\` - ); - } - getTableMeta(table, requestId = nextRequestId()) { - if (isSessionTable(table)) { - return Promise.resolve(void 0); - } - const key = \`\${table.module}:\${table.table}\`; - let tableMetaRequest = this.cachedTableMetaRequests.get(key); - if (!tableMetaRequest) { - tableMetaRequest = this.awaitResponseToMessage( - { type: "GET_TABLE_META", table }, - requestId - ); - this.cachedTableMetaRequests.set(key, tableMetaRequest); - } - return tableMetaRequest == null ? void 0 : tableMetaRequest.then((response) => this.cacheTableMeta(response)); - } - awaitResponseToMessage(message, requestId = nextRequestId()) { - return new Promise((resolve, reject) => { - this.sendMessageToServer(message, requestId); - this.pendingRequests.set(requestId, { reject, resolve }); - }); - } - sendIfReady(message, requestId, isReady = true) { - if (isReady) { - this.sendMessageToServer(message, requestId); - } else { - this.queuedRequests.push(message); - } - return isReady; - } - sendMessageToServer(body, requestId = \`\${_requestId++}\`, options = DEFAULT_OPTIONS) { - const { module = "CORE" } = options; - if (this.authToken) { - this.connection.send({ - requestId, - sessionId: this.sessionId, - token: this.authToken, - user: this.user, - module, - body - }); - } - } - handleMessageFromServer(message) { - var _a, _b, _c; - const { body, requestId, sessionId } = message; - const pendingRequest = this.pendingRequests.get(requestId); - if (pendingRequest) { - const { resolve } = pendingRequest; - this.pendingRequests.delete(requestId); - resolve(body); - return; - } - const { viewports } = this; - switch (body.type) { - case HB: - this.sendMessageToServer( - { type: HB_RESP, ts: +/* @__PURE__ */ new Date() }, - "NA" - ); - break; - case "LOGIN_SUCCESS": - if (sessionId) { - this.sessionId = sessionId; - (_a = this.pendingLogin) == null ? void 0 : _a.resolve(sessionId); - this.pendingLogin = void 0; - } else { - throw Error("LOGIN_SUCCESS did not provide sessionId"); - } - break; - case "REMOVE_VP_SUCCESS": - { - const viewport = viewports.get(body.viewPortId); - if (viewport) { - this.mapClientToServerViewport.delete(viewport.clientViewportId); - viewports.delete(body.viewPortId); - this.removeViewportFromVisualLinks(body.viewPortId); - } - } - break; - case SET_SELECTION_SUCCESS: - { - const viewport = this.viewports.get(body.vpId); - if (viewport) { - viewport.completeOperation(requestId); - } - } - break; - case CHANGE_VP_SUCCESS: - case DISABLE_VP_SUCCESS: - if (viewports.has(body.viewPortId)) { - const viewport = this.viewports.get(body.viewPortId); - if (viewport) { - const response = viewport.completeOperation(requestId); - if (response !== void 0) { - this.postMessageToClient(response); - if (debugEnabled4) { - debug4(\`postMessageToClient \${JSON.stringify(response)}\`); - } - } - } - } - break; - case ENABLE_VP_SUCCESS: - { - const viewport = this.viewports.get(body.viewPortId); - if (viewport) { - const response = viewport.completeOperation(requestId); - if (response) { - this.postMessageToClient(response); - const [size, rows] = viewport.resume(); - this.postMessageToClient({ - clientViewportId: viewport.clientViewportId, - mode: "batch", - rows, - size, - type: "viewport-update" - }); - } - } - } - break; - case TABLE_ROW: - { - const viewportRowMap = groupRowsByViewport(body.rows); - if (debugEnabled4) { - const [firstRow, secondRow] = body.rows; - if (body.rows.length === 0) { - debug4("handleMessageFromServer TABLE_ROW 0 rows"); - } else if ((firstRow == null ? void 0 : firstRow.rowIndex) === -1) { - if (body.rows.length === 1) { - if (firstRow.updateType === "SIZE") { - debug4( - \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE ONLY \${firstRow.vpSize}\` - ); - } else { - debug4( - \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE \${firstRow.vpSize} rowIdx \${firstRow.rowIndex}\` - ); - } - } else { - debug4( - \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows, SIZE \${firstRow.vpSize}, [\${secondRow == null ? void 0 : secondRow.rowIndex}] - [\${(_b = body.rows[body.rows.length - 1]) == null ? void 0 : _b.rowIndex}]\` - ); - } - } else { - debug4( - \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows [\${firstRow == null ? void 0 : firstRow.rowIndex}] - [\${(_c = body.rows[body.rows.length - 1]) == null ? void 0 : _c.rowIndex}]\` - ); - } - } - for (const [viewportId, rows] of Object.entries(viewportRowMap)) { - const viewport = viewports.get(viewportId); - if (viewport) { - viewport.updateRows(rows); - } else { - warn3 == null ? void 0 : warn3( - \`TABLE_ROW message received for non registered viewport \${viewportId}\` - ); - } - } - this.processUpdates(); - } - break; - case CHANGE_VP_RANGE_SUCCESS: - { - const viewport = this.viewports.get(body.viewPortId); - if (viewport) { - const { from, to } = body; - if (true) { - info3 == null ? void 0 : info3(\`CHANGE_VP_RANGE_SUCCESS \${from} - \${to}\`); - } - viewport.completeOperation(requestId, from, to); - } - } - break; - case OPEN_TREE_SUCCESS: - case CLOSE_TREE_SUCCESS: - break; - case "CREATE_VISUAL_LINK_SUCCESS": - { - const viewport = this.viewports.get(body.childVpId); - const parentViewport = this.viewports.get(body.parentVpId); - if (viewport && parentViewport) { - const { childColumnName, parentColumnName } = body; - const response = viewport.completeOperation( - requestId, - childColumnName, - parentViewport.clientViewportId, - parentColumnName - ); - if (response) { - this.postMessageToClient(response); - } - } - } - break; - case "REMOVE_VISUAL_LINK_SUCCESS": - { - const viewport = this.viewports.get(body.childVpId); - if (viewport) { - const response = viewport.completeOperation( - requestId - ); - if (response) { - this.postMessageToClient(response); - } - } - } - break; - case "VP_VISUAL_LINKS_RESP": - { - const activeLinkDescriptors = this.getActiveLinks(body.links); - const viewport = this.viewports.get(body.vpId); - if (activeLinkDescriptors.length && viewport) { - const linkDescriptorsWithLabels = addLabelsToLinks( - activeLinkDescriptors, - this.viewports - ); - const [clientMessage, pendingLink] = viewport.setLinks( - linkDescriptorsWithLabels - ); - this.postMessageToClient(clientMessage); - if (pendingLink) { - const { link, parentClientVpId } = pendingLink; - const requestId2 = nextRequestId(); - const serverViewportId = this.mapClientToServerViewport.get(parentClientVpId); - if (serverViewportId) { - const message2 = viewport.createLink( - requestId2, - link.fromColumn, - serverViewportId, - link.toColumn - ); - this.sendMessageToServer(message2, requestId2); - } - } - } - } - break; - case "VIEW_PORT_MENUS_RESP": - if (body.menu.name) { - const viewport = this.viewports.get(body.vpId); - if (viewport) { - const clientMessage = viewport.setMenu(body.menu); - this.postMessageToClient(clientMessage); - } - } - break; - case "VP_EDIT_RPC_RESPONSE": - { - this.postMessageToClient({ - action: body.action, - requestId, - rpcName: body.rpcName, - type: "VP_EDIT_RPC_RESPONSE" - }); - } - break; - case "VP_EDIT_RPC_REJECT": - { - const viewport = this.viewports.get(body.vpId); - if (viewport) { - this.postMessageToClient({ - requestId, - type: "VP_EDIT_RPC_REJECT", - error: body.error - }); - } - } - break; - case "VIEW_PORT_MENU_REJ": { - console.log(\`send menu error back to client\`); - const { error: error4, rpcName, vpId } = body; - const viewport = this.viewports.get(vpId); - if (viewport) { - this.postMessageToClient({ - clientViewportId: viewport.clientViewportId, - error: error4, - rpcName, - type: "VIEW_PORT_MENU_REJ", - requestId - }); - } - break; - } - case "VIEW_PORT_MENU_RESP": - { - if (isSessionTableActionMessage(body)) { - const { action, rpcName } = body; - this.awaitResponseToMessage({ - type: "GET_TABLE_META", - table: action.table - }).then((response) => { - const tableSchema = createSchemaFromTableMetadata( - response - ); - this.postMessageToClient({ - rpcName, - type: "VIEW_PORT_MENU_RESP", - action: { - ...action, - tableSchema - }, - tableAlreadyOpen: this.isTableOpen(action.table), - requestId - }); - }); - } else { - const { action } = body; - this.postMessageToClient({ - type: "VIEW_PORT_MENU_RESP", - action: action || NO_ACTION, - tableAlreadyOpen: action !== null && this.isTableOpen(action.table), - requestId - }); - } - } - break; - case RPC_RESP: - { - const { method, result } = body; - this.postMessageToClient({ - type: RPC_RESP, - method, - result, - requestId - }); - } - break; - case "ERROR": - error3(body.msg); - break; - default: - infoEnabled3 && info3(\`handleMessageFromServer \${body["type"]}.\`); - } - } - cacheTableMeta(messageBody) { - const { module, table } = messageBody.table; - const key = \`\${module}:\${table}\`; - let tableSchema = this.cachedTableSchemas.get(key); - if (!tableSchema) { - tableSchema = createSchemaFromTableMetadata(messageBody); - this.cachedTableSchemas.set(key, tableSchema); - } - return tableSchema; - } - isTableOpen(table) { - if (table) { - const tableName = table.table; - for (const viewport of this.viewports.values()) { - if (!viewport.suspended && viewport.table.table === tableName) { - return true; - } - } - } - } - // Eliminate links to suspended viewports - getActiveLinks(linkDescriptors) { - return linkDescriptors.filter((linkDescriptor) => { - const viewport = this.viewports.get(linkDescriptor.parentVpId); - return viewport && !viewport.suspended; - }); - } - processUpdates() { - this.viewports.forEach((viewport) => { - var _a; - if (viewport.hasUpdatesToProcess) { - const result = viewport.getClientRows(); - if (result !== NO_DATA_UPDATE) { - const [rows, mode] = result; - const size = viewport.getNewRowCount(); - if (size !== void 0 || rows && rows.length > 0) { - debugEnabled4 && debug4( - \`postMessageToClient #\${viewport.clientViewportId} viewport-update \${mode}, \${(_a = rows == null ? void 0 : rows.length) != null ? _a : "no"} rows, size \${size}\` - ); - if (mode) { - this.postMessageToClient({ - clientViewportId: viewport.clientViewportId, - mode, - rows, - size, - type: "viewport-update" - }); - } - } - } - } - }); - } -}; - -// src/worker.ts -var server; -var { info: info4, infoEnabled: infoEnabled4 } = logger("worker"); -async function connectToServer(url, protocol, token, username, onConnectionStatusChange, retryLimitDisconnect, retryLimitStartup) { - const connection = await connect( - url, - protocol, - // if this was called during connect, we would get a ReferenceError, but it will - // never be called until subscriptions have been made, so this is safe. - //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? - (msg) => { - if (isConnectionQualityMetrics(msg)) { - postMessage({ type: "connection-metrics", messages: msg }); - } else if (isConnectionStatusMessage(msg)) { - onConnectionStatusChange(msg); - if (msg.status === "reconnected") { - server.reconnect(); - } - } else { - server.handleMessageFromServer(msg); - } - }, - retryLimitDisconnect, - retryLimitStartup - ); - server = new ServerProxy(connection, (msg) => sendMessageToClient(msg)); - if (connection.requiresLogin) { - await server.login(token, username); - } -} -function sendMessageToClient(message) { - postMessage(message); -} -var handleMessageFromClient = async ({ - data: message -}) => { - switch (message.type) { - case "connect": - await connectToServer( - message.url, - message.protocol, - message.token, - message.username, - postMessage, - message.retryLimitDisconnect, - message.retryLimitStartup - ); - postMessage({ type: "connected" }); - break; - case "subscribe": - infoEnabled4 && info4(\`client subscribe: \${JSON.stringify(message)}\`); - server.subscribe(message); - break; - case "unsubscribe": - infoEnabled4 && info4(\`client unsubscribe: \${JSON.stringify(message)}\`); - server.unsubscribe(message.viewport); - break; - default: - infoEnabled4 && info4(\`client message: \${JSON.stringify(message)}\`); - server.handleMessageFromClient(message); - } -}; -self.addEventListener("message", handleMessageFromClient); -postMessage({ type: "ready" }); + \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{IDX:Jn}=V;var{SELECTED:Xn}=V,M={False:0,True:1,First:2,Last:4};var ft=(s,e)=>e>=s[0]&&e<=s[1],mt=M.True+M.First+M.Last,ht=M.True+M.First,Ct=M.True+M.Last,J=(s,e)=>{for(let t of s)if(typeof t=="number"){if(t===e)return mt}else if(ft(t,e))return e===t[0]?ht:e===t[1]?Ct:M.True;return M.False};var Ce=s=>{if(s.every(t=>typeof t=="number"))return s;let e=[];for(let t of s)if(typeof t=="number")e.push(t);else for(let n=t[0];n<=t[1];n++)e.push(n);return e};var bt=(()=>{let s=0,e=()=>\`0000\${(Math.random()*36**4<<0).toString(36)}\`.slice(-4);return()=>(s+=1,\`u\${e()}\${s}\`)})();var{debug:_s,debugEnabled:Os,error:Te,info:E,infoEnabled:wt,warn:D}=w("websocket-connection"),Re="ws",Et=s=>s.startsWith(Re+"://")||s.startsWith(Re+"s://"),Ee={},X=Symbol("setWebsocket"),W=Symbol("connectionCallback");async function Ve(s,e,t,n=10,r=5){return Ee[s]={status:"connecting",connect:{allowed:r,remaining:r},reconnect:{allowed:n,remaining:n}},Me(s,e,t)}async function Z(s){throw Error("connection broken")}async function Me(s,e,t,n){let{status:r,connect:o,reconnect:i}=Ee[s],u=r==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof n<"u",p=await Mt(s,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),n!==void 0&&n[X](p);let a=n!=null?n:new Q(p,s,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),a.status=l,u.remaining=u.allowed,a}catch{let p=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:p}),p)return Vt(s,e,t,n,2e3);throw Error("Failed to establish connection")}}var Vt=(s,e,t,n,r)=>new Promise(o=>{setTimeout(()=>{o(Me(s,e,t,n))},r)}),Mt=(s,e)=>new Promise((t,n)=>{let r=Et(s)?s:\`wss://\${s}\`;wt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(r,e);o.onopen=()=>t(o),o.onerror=i=>n(i)}),Se=()=>{D==null||D("Connection cannot be closed, socket not yet opened")},we=s=>{D==null||D(\`Message cannot be sent, socket closed \${s.body.type}\`)},xt=s=>{try{return JSON.parse(s)}catch{throw Error(\`Error parsing JSON response from server \${s}\`)}},Q=class{constructor(e,t,n,r){this.close=Se;this.requiresLogin=!0;this.send=we;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=xt(e.data);this.messagesCount+=1,this[W](t)};this.url=t,this.protocol=n,this[W]=r,this[X](e)}reconnect(){Z(this)}[(W,X)](e){let t=this[W];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{Te("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?Te("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=r)},e.onclose=()=>{E==null||E("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Z(this),this.send=r)};let n=o=>{e.send(JSON.stringify(o))},r=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=n,this.close=()=>{this.status="closed",e.close(),this.close=Se,this.send=we,E==null||E("close websocket")}}};var vt=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],xe=s=>vt.includes(s.type),ve=s=>s.type==="VIEW_PORT_RPC_CALL",\$=({requestId:s,...e})=>[s,e],Ie=s=>{let e=s.at(0);if(e.updateType==="SIZE"){if(s.length===1)return s;e=s.at(1)}let t=s.at(-1);return[e,t]},De=s=>{let e={};for(let t of s)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e};var ee=({columns:s,dataTypes:e,key:t,table:n})=>({table:n,columns:s.map((r,o)=>({name:r,serverDataType:e[o]})),key:t});var Pe=s=>s.type==="connection-status",Le=s=>s.type==="connection-metrics";var _e=s=>"viewport"in s,Oe=s=>s.type==="VIEW_PORT_MENU_RESP"&&s.action!==null&&q(s.action.table),q=s=>s!==null&&typeof s=="object"&&"table"in s&&"module"in s?s.table.startsWith("session"):!1;var ke="CHANGE_VP_SUCCESS";var Ae="CLOSE_TREE_NODE",Ue="CLOSE_TREE_SUCCESS";var Fe="CREATE_VP",Ne="DISABLE_VP",We="DISABLE_VP_SUCCESS";var \$e="ENABLE_VP",qe="ENABLE_VP_SUCCESS";var te="GET_VP_VISUAL_LINKS",Ge="GET_VIEW_PORT_MENUS";var Be="HB",Ke="HB_RESP",He="LOGIN",je="OPEN_TREE_NODE",ze="OPEN_TREE_SUCCESS";var Je="REMOVE_VP";var Ye="SET_SELECTION_SUCCESS";var Xe=s=>{switch(s){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var Qe=[],T=w("array-backed-moving-window");function It(s,e){if(!e||e.data.length!==s.data.length||e.sel!==s.sel)return!1;for(let t=0;t{var t;if((t=T.info)==null||t.call(T,\`setRowCount \${e}\`),e{let n=this.bufferSize*.25;return d(this,h).to-t0&&e-d(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:n,to:r}=this.range;if(t=r)return!0}setAtIndex(e){let{rowIndex:t}=e,n=t-d(this,h).from;if(It(e,this.internalData[n]))return!1;let r=this.isWithinClientRange(t);return(r||this.isWithinRange(t))&&(!this.internalData[n]&&r&&(this.rowsWithinRange+=1),this.internalData[n]=e),r}getAtIndex(e){return d(this,h).isWithin(e)&&this.internalData[e-d(this,h).from]!=null?this.internalData[e-d(this,h).from]:void 0}isWithinRange(e){return d(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=T.debug)==null||p.call(T,\`setClientRange \${e} - \${t}\`);let n=this.clientRange.from,r=Math.min(this.clientRange.to,this.rowCount);if(e===n&&t===r)return[!1,Qe];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let a=e;ao.to){let a=Math.max(e,o.to);i=this.internalData.slice(a-u,t-u)}else{let a=Math.min(o.from,t);i=this.internalData.slice(e-u,a-u)}return[this.bufferBreakout(e,t),i]}setRange(e,t){var n,r;if(e!==d(this,h).from||t!==d(this,h).to){(n=T.debug)==null||n.call(T,\`setRange \${e} - \${t}\`);let[o,i]=d(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let c=o;c=0;o--)if(e[o]!==void 0){r=e[o];break}return n&&r?[n.rowIndex,r.rowIndex]:[-1,-1]}};h=new WeakMap;var Dt=[],{debug:C,debugEnabled:B,error:Pt,info:g,infoEnabled:Lt,warn:P}=w("viewport"),_t=({rowKey:s,updateType:e})=>e==="U"&&!s.startsWith("\$root"),K=[void 0,void 0],Ot={count:0,mode:void 0,size:0,ts:0},H=class{constructor({aggregations:e,bufferSize:t=50,columns:n,filter:r,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},m){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=Ot;this.updateThrottleTimer=void 0;this.rangeMonitor=new U("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:n}=this.lastUpdateStatus,r=0;if(n===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,r=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,r=0;return this.lastUpdateStatus.mode=e,r};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,n=t*.25,{from:r}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(r>=o&&r{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(g==null||g("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=a,this.columns=n,this.filter=r,this.groupBy=o,this.keys=new N(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,Lt&&(g==null||g(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new G(this.clientRange,u,this.bufferSize),this.postMessageToClient=m}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:Fe,table:this.table,range:z(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:n,filterSpec:r,range:o,sort:i,groupBy:u},c){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=n,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:n,filter:r,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:n,pendingOperations:r}=this,o=r.get(e);if(!o){Pt(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(g==null||g(\`completeOperation \${i}\`),r.delete(e),i==="CHANGE_VP_RANGE"){let[c,p]=t;(u=this.dataWindow)==null||u.setRange(c,p);for(let a=this.pendingRangeRequests.length-1;a>=0;a--){let l=this.pendingRangeRequests[a];if(l.requestId===e){l.acked=!0;break}else P==null||P("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:m}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=m,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),C==null||C(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,config:o.data}}else{if(i==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,C==null||C(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:n,type:i,groupBy:o.data};if(i==="columns")return this.columns=o.data,{clientViewportId:n,type:i,columns:o.data};if(i==="filter")return this.filter=o.data,{clientViewportId:n,type:i,filter:o.data};if(i==="aggregate")return this.aggregations=o.data,{clientViewportId:n,type:"aggregate",aggregations:this.aggregations};if(i==="sort")return this.sort=o.data,{clientViewportId:n,type:i,sort:this.sort};if(i!=="selection"){if(i==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:n};if(i==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:n};if(i==="CREATE_VISUAL_LINK"){let[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:n,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:n}}}}rangeRequest(e,t){B&&this.rangeMonitor.set(t);let n="CHANGE_VP_RANGE";if(this.dataWindow){let[r,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=r&&!this.rangeRequestAlreadyPending(t)?{type:n,viewPortId:this.serverViewportId,...z(t,this.bufferSize,u)}:null;if(c){B&&(C==null||C(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:n});let a=this.pendingRangeRequests.at(-1);if(a)if(a.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:m}=a;this.dataWindow.outOfRange(l,m)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:P==null||P("Range Request before previous request is acked")}this.pendingRangeRequests.push({...c,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);this.keys.reset(this.dataWindow.clientRange);let p=this.isTree?re:ne;return o.length?[c,o.map(a=>p(a,this.keys,this.selectedRows))]:i?[c,void 0,i]:[c]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:je,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Ae,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,n,r){let o={type:"CREATE_VISUAL_LINK",parentVpId:n,childVpId:this.serverViewportId,parentColumnName:r,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,g==null||g("suspend")}resume(){return this.suspended=!1,B&&(C==null||C(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:n}=this,r=this.isTree?re:ne;for(let o of t)o&&e.push(r(o,n,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),g==null||g(\`enable: \${this.serverViewportId}\`),{type:\$e,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),g==null||g(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Ne,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),C==null||C(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:n}=t;return g==null||g(\`filterRequest: \${n}\`),this.createRequest({filterSpec:{filter:n}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:n,...r}=t;return this.useBatchMode&&(this.batchMode=!0),B?C==null||C(\`setConfig \${JSON.stringify(t)}\`):g==null||g("setConfig"),this.createRequest({...r,filterSpec:typeof(n==null?void 0:n.filter)=="string"?{filter:n.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),g==null||g(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),g==null||g(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=Dt){var n;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(n=this.dataWindow)==null||n.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),g==null||g(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:Ce(t)}}removePendingRangeRequest(e,t){for(let n=this.pendingRangeRequests.length-1;n>=0;n--){let{from:r,to:o}=this.pendingRangeRequests[n],i=!0;if(e>=r&&er&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,n,r));this.pendingUpdates.length=0}else{let i=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of i)e.push(o(u,n,r));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?K:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},ne=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>[s,r.keyFor(s),!0,!1,0,0,e,t?J(o,s):0].concat(n),re=({rowIndex:s,rowKey:e,sel:t,data:n},r,o)=>{let[i,u,,c,,p,...a]=n;return[s,r.keyFor(s),c,u,i,p,e,t?J(o,s):0].concat(a)};var et=1;var{debug:x,debugEnabled:se,error:L,info:S,infoEnabled:kt,warn:oe}=w("server-proxy"),b=()=>\`\${et++}\`,At={},Ut=s=>s.disabled!==!0&&s.suspended!==!0,Ft={type:"NO_ACTION"},Nt=(s,e,t)=>s.map(n=>n.parentVpId===e?{...n,label:t}:n);function Wt(s,e){return s.map(t=>{let{parentVpId:n}=t,r=e.get(n);if(r)return{...t,parentClientVpId:r.clientViewportId,label:r.title};throw Error("addLabelsToLinks viewport not found")})}var j=class{constructor(e,t){this.authToken="";this.user="user";this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableMetaRequests=new Map;this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=ge(Array.from(this.viewports.values()),Ut);this.viewports.clear(),this.mapClientToServerViewport.clear();let n=r=>{r.forEach(o=>{let{clientViewportId:i}=o;this.viewports.set(i,o),this.sendMessageToServer(o.subscribe(),i)})};n(e),setTimeout(()=>{n(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((n,r)=>{this.sendMessageToServer({type:He,token:this.authToken,user:t},""),this.pendingLogin={resolve:n,reject:r}});this.authToken===""&&L("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))L(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),n=new H(e,this.postMessageToClient);this.viewports.set(e.viewport,n);let r=this.awaitResponseToMessage(n.subscribe(),e.viewport);Promise.all([r,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=n;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,n)),this.mapClientToServerViewport.set(e.viewport,c);let a=n.handleSubscribed(i,u);a&&(this.postMessageToClient(a),se&&x(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),n.disabled&&this.disableViewport(n),p==="subscribing"&&!q(n.table)&&(this.sendMessageToServer({type:te,vpId:c}),this.sendMessageToServer({type:Ge,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:m}])=>l!==c&&!m).forEach(([l])=>{this.sendMessageToServer({type:te,vpId:l})}))})}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(S==null||S(\`Unsubscribe Message (Client to Server): + \${t}\`),this.sendMessageToServer({type:Je,viewPortId:t})):L(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let n=this.mapClientToServerViewport.get(e);if(n){let r=this.viewports.get(n);if(r)return r;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let n=b(),[r,o,i]=e.rangeRequest(n,t.range);S==null||S(\`setViewRange \${t.range.from} - \${t.range.to}\`),r&&this.sendIfReady(r,n,e.status==="subscribed"),o?(S==null||S(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):i&&this.postMessageToClient(i)}setConfig(e,t){let n=b(),r=e.setConfig(n,t.config);this.sendIfReady(r,n,e.status==="subscribed")}aggregate(e,t){let n=b(),r=e.aggregateRequest(n,t.aggregations);this.sendIfReady(r,n,e.status==="subscribed")}sort(e,t){let n=b(),r=e.sortRequest(n,t.sort);this.sendIfReady(r,n,e.status==="subscribed")}groupBy(e,t){let n=b(),r=e.groupByRequest(n,t.groupBy);this.sendIfReady(r,n,e.status==="subscribed")}filter(e,t){let n=b(),{filter:r}=t,o=e.filterRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setColumns(e,t){let n=b(),{columns:r}=t,o=e.columnRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let n=b(),{selected:r}=t,o=e.selectRequest(n,r);this.sendIfReady(o,n,e.status==="subscribed")}disableViewport(e){let t=b(),n=e.disable(t);this.sendIfReady(n,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=b(),n=e.enable(t);this.sendIfReady(n,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{S==null||S("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(x==null||x("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,n]=e.resume();x==null||x(\`resumeViewport size \${t}, \${n.length} rows sent to client\`),this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:n,size:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.openTreeNode(n,t),n,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let n=b();this.sendIfReady(e.closeTreeNode(n,t),n,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:n,parentColumnName:r,childColumnName:o}=t,i=b(),u=this.mapClientToServerViewport.get(n);if(u){let c=e.createLink(i,o,u,r);this.sendMessageToServer(c,i)}else L("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=b(),n=e.removeLink(t);this.sendMessageToServer(n,t)}updateTitleOnVisualLinks(e){var r;let{serverViewportId:t,title:n}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&n&&(r=o.links)!=null&&r.some(i=>i.parentVpId===t)){let[i]=o.setLinks(Nt(o.links,t,n));this.postMessageToClient(i)}}removeViewportFromVisualLinks(e){var t;for(let n of this.viewports.values())if((t=n.links)!=null&&t.some(({parentVpId:r})=>r===e)){let[r]=n.setLinks(n.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(r)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=\$(e);this.sendMessageToServer({...r,vpId:t.serverViewportId},n)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[n,r]=\$(e);this.sendMessageToServer({...r,vpId:t.serverViewportId,namedParams:{}},n)}}rpcCall(e){let[t,n]=\$(e),r=Xe(n.service);this.sendMessageToServer(n,t,{module:r})}handleMessageFromClient(e){var t;if(_e(e))if(e.type==="disable"){let n=this.getViewportForClient(e.viewport,!1);return n!==null?this.disableViewport(n):void 0}else{let n=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(n,e);case"config":return this.setConfig(n,e);case"aggregate":return this.aggregate(n,e);case"sort":return this.sort(n,e);case"groupBy":return this.groupBy(n,e);case"filter":return this.filter(n,e);case"select":return this.select(n,e);case"suspend":return this.suspendViewport(n);case"resume":return this.resumeViewport(n);case"enable":return this.enableViewport(n);case"openTreeNode":return this.openTreeNode(n,e);case"closeTreeNode":return this.closeTreeNode(n,e);case"createLink":return this.createLink(n,e);case"removeLink":return this.removeLink(n);case"setColumns":return this.setColumns(n,e);case"setTitle":return this.setTitle(n,e);default:}}else{if(ve(e))return this.viewportRpcCall(e);if(xe(e))return this.menuRpcCall(e);{let{type:n,requestId:r}=e;switch(n){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:n},r)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:r})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,r).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:r})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}L(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=b()){if(q(e))return Promise.resolve(void 0);let n=\`\${e.module}:\${e.table}\`,r=this.cachedTableMetaRequests.get(n);return r||(r=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(n,r)),r==null?void 0:r.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=b()){return new Promise((n,r)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:r,resolve:n})})}sendIfReady(e,t,n=!0){return n?this.sendMessageToServer(e,t):this.queuedRequests.push(e),n}sendMessageToServer(e,t=\`\${et++}\`,n=At){let{module:r="CORE"}=n;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:r,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:n,sessionId:r}=e,o=this.pendingRequests.get(n);if(o){let{resolve:a}=o;this.pendingRequests.delete(n),a(t);return}let{viewports:i}=this;switch(t.type){case Be:this.sendMessageToServer({type:Ke,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(r)this.sessionId=r,(u=this.pendingLogin)==null||u.resolve(r),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case"REMOVE_VP_SUCCESS":{let a=i.get(t.viewPortId);a&&(this.mapClientToServerViewport.delete(a.clientViewportId),i.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case Ye:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(n)}break;case ke:case We:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);l!==void 0&&(this.postMessageToClient(l),se&&x(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case qe:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(n);if(l){this.postMessageToClient(l);let[m,R]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:R,size:m,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=De(t.rows);for(let[l,m]of Object.entries(a)){let R=i.get(l);R?R.updateRows(m):oe==null||oe(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case"CHANGE_VP_RANGE_SUCCESS":{let a=this.viewports.get(t.viewPortId);if(a){let{from:l,to:m}=t;a.completeOperation(n,l,m)}}break;case ze:case Ue:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:m,parentColumnName:R}=t,_=a.completeOperation(n,m,l.clientViewportId,R);_&&this.postMessageToClient(_)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(n);l&&this.postMessageToClient(l)}}break;case"VP_VISUAL_LINKS_RESP":{let a=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(a.length&&l){let m=Wt(a,this.viewports),[R,_]=l.setLinks(m);if(this.postMessageToClient(R),_){let{link:ue,parentClientVpId:tt}=_,le=b(),ce=this.mapClientToServerViewport.get(tt);if(ce){let nt=l.createLink(le,ue.fromColumn,ce,ue.toColumn);this.sendMessageToServer(nt,le)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let a=this.viewports.get(t.vpId);if(a){let l=a.setMenu(t.menu);this.postMessageToClient(l)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:n,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:n,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_REJ":{console.log("send menu error back to client");let{error:a,rpcName:l,vpId:m}=t,R=this.viewports.get(m);R&&this.postMessageToClient({clientViewportId:R.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:n});break}case"VIEW_PORT_MENU_RESP":if(Oe(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(m=>{let R=ee(m);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:R},tableAlreadyOpen:this.isTableOpen(a.table),requestId:n})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Ft,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:n})}break;case"RPC_RESP":{let{method:a,result:l}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:l,requestId:n})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:l}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:l,requestId:n})}break;case"ERROR":L(t.msg);break;default:kt&&S(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:n}=e.table,r=\`\${t}:\${n}\`,o=this.cachedTableSchemas.get(r);return o||(o=ee(e),this.cachedTableSchemas.set(r,o)),o}isTableOpen(e){if(e){let t=e.table;for(let n of this.viewports.values())if(!n.suspended&&n.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let n=this.viewports.get(t.parentVpId);return n&&!n.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let n=e.getClientRows();if(n!==K){let[r,o]=n,i=e.getNewRowCount();(i!==void 0||r&&r.length>0)&&(se&&x(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=r==null?void 0:r.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:r,size:i,type:"viewport-update"}))}}})}};var I,{info:ie,infoEnabled:ae}=w("worker");async function \$t(s,e,t,n,r,o,i){let u=await Ve(s,e,c=>{Le(c)?postMessage({type:"connection-metrics",messages:c}):Pe(c)?(r(c),c.status==="reconnected"&&I.reconnect()):I.handleMessageFromServer(c)},o,i);I=new j(u,c=>qt(c)),u.requiresLogin&&await I.login(t,n)}function qt(s){postMessage(s)}var Gt=async({data:s})=>{switch(s.type){case"connect":await \$t(s.url,s.protocol,s.token,s.username,postMessage,s.retryLimitDisconnect,s.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":ae&&ie(\`client subscribe: \${JSON.stringify(s)}\`),I.subscribe(s);break;case"unsubscribe":ae&&ie(\`client unsubscribe: \${JSON.stringify(s)}\`),I.unsubscribe(s.viewport);break;default:ae&&ie(\`client message: \${JSON.stringify(s)}\`),I.handleMessageFromClient(s)}};self.addEventListener("message",Gt);postMessage({type:"ready"}); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data/src/remote-data-source.ts b/vuu-ui/packages/vuu-data/src/remote-data-source.ts index fb598fd04..c95913826 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -3,6 +3,7 @@ import { Selection } from "@finos/vuu-datagrid-types"; import { ClientToServerEditRpc, ClientToServerMenuRPC, + ClientToServerViewportRpcCall, LinkDescriptorWithLabel, VuuAggregation, VuuDataRowDto, @@ -35,6 +36,7 @@ import { DataSourceStatus, isDataSourceConfigMessage, OptimizeStrategy, + RpcResponse, SubscribeCallback, SubscribeProps, vanillaConfig, @@ -646,9 +648,11 @@ export class RemoteDataSource } } - async rpcCall(rpcRequest: Omit) { + async rpcCall( + rpcRequest: Omit + ) { if (this.viewport) { - return this.server?.rpcCall({ + return this.server?.rpcCall({ vpId: this.viewport, ...rpcRequest, } as ClientToServerViewportRpcCall); 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 5382233bd..1945afbd5 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 @@ -16,7 +16,6 @@ import { logger, partition } from "@finos/vuu-utils"; import { Connection } from "../connectionTypes"; import { DataSourceCallbackMessage, - DataSourceEnabledMessage, DataSourceVisualLinkCreatedMessage, DataSourceVisualLinkRemovedMessage, } from "../data-source"; @@ -824,10 +823,9 @@ export class ServerProxy { } } break; - case Message.TABLE_ROW: + case "TABLE_ROW": { const viewportRowMap = groupRowsByViewport(body.rows); - if (process.env.NODE_ENV === "development" && debugEnabled) { const [firstRow, secondRow] = body.rows; if (body.rows.length === 0) { @@ -876,7 +874,7 @@ export class ServerProxy { } break; - case Message.CHANGE_VP_RANGE_SUCCESS: + case "CHANGE_VP_RANGE_SUCCESS": { const viewport = this.viewports.get(body.viewPortId); if (viewport) { @@ -1049,12 +1047,12 @@ export class ServerProxy { } break; - case Message.RPC_RESP: + case "RPC_RESP": { const { method, result } = body; // check to see if the orderEntry is already open on the page this.postMessageToClient({ - type: Message.RPC_RESP, + type: "RPC_RESP", method, result, requestId, @@ -1062,6 +1060,18 @@ export class ServerProxy { } break; + case "VIEW_PORT_RPC_REPONSE": + { + const { method, action } = body; + this.postMessageToClient({ + type: "VIEW_PORT_RPC_RESPONSE", + rpcName: method, + action, + requestId, + }); + } + break; + case "ERROR": error(body.msg); break; diff --git a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts index 29ed6e16a..1ca9331b4 100644 --- a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts +++ b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts @@ -4,6 +4,7 @@ import { LinkDescriptorWithLabel, ServerToClientBody, ServerToClientMenuSessionTableAction, + ServerToClientViewportRpcResponse, TypeAheadMethod, VuuAggregation, VuuColumns, @@ -142,6 +143,12 @@ export interface MenuRpcResponse { tableAlreadyOpen?: boolean; type: "VIEW_PORT_MENU_RESP"; } +export interface ViewportRpcResponse { + action: ServerToClientViewportRpcResponse["action"]; + requestId: string; + rpcName?: string; + type: "VIEW_PORT_RPC_RESPONSE"; +} export interface MenuRpcReject extends ViewportMessageIn { error?: string; requestId: string; @@ -160,6 +167,7 @@ export type VuuUIMessageIn = | VuuUIMessageInConnected | VuuUIMessageInWorkerReady | VuuUIMessageInRPC + | ViewportRpcResponse | MenuRpcResponse | MenuRpcReject | VuuUIMessageInTableList diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 7cd0b2df2..71ea992e4 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -30,9 +30,9 @@ export type DataCellEditHandler = ( export interface TableCellProps { className?: string; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; columnMap: ColumnMap; - onClick?: (event: MouseEvent, column: KeyedColumnDescriptor) => void; + onClick?: (event: MouseEvent, column: RuntimeColumnDescriptor) => void; onDataEdited?: DataCellEditHandler; row: DataSourceRow; } @@ -181,6 +181,8 @@ export interface ColumnDescriptor { aggregate?: VuuAggType; align?: ColumnAlignment; className?: string; + colHeaderContentRenderer?: string; + colHeaderLabelRenderer?: string; editable?: boolean; flex?: number; /** @@ -211,9 +213,11 @@ export interface ColumnDescriptorCustomRenderer /** This is an internal description of a Column that extends the public * definitin with internal state values. */ -export interface KeyedColumnDescriptor extends ColumnDescriptor { +export interface RuntimeColumnDescriptor extends ColumnDescriptor { align?: "left" | "right"; CellRenderer?: FunctionComponent; + HeaderCellLabelRenderer?: FunctionComponent; + HeaderCellContentRenderer?: FunctionComponent; className?: string; clientSideEditValidationCheck?: ClientSideValidationChecker; endPin?: true | undefined; @@ -239,8 +243,8 @@ export interface KeyedColumnDescriptor extends ColumnDescriptor { width: number; } -export interface GroupColumnDescriptor extends KeyedColumnDescriptor { - columns: KeyedColumnDescriptor[]; +export interface GroupColumnDescriptor extends RuntimeColumnDescriptor { + columns: RuntimeColumnDescriptor[]; groupConfirmed: boolean; } diff --git a/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx b/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx index a41f5a104..69af245e7 100644 --- a/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx @@ -17,7 +17,7 @@ import "./column-bearer.css"; import { GridModelType } from "./grid-model"; import { ColumnDragState, dragPhase } from "./gridTypes"; import { buildColumnMap } from "@finos/vuu-utils"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { DataSourceRow } from "@finos/vuu-data-types"; const LEFT = "left"; @@ -105,7 +105,7 @@ function getTargetColumn( function getScrollBounds( gridModel: GridModelType, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { const { columnGroups, width } = gridModel; if (!columnGroups || width === undefined) { @@ -135,7 +135,7 @@ function getScrollBounds( function useScrollBounds( gridModel: GridModelType, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { const scrollBounds = useRef(getScrollBounds(gridModel, column)); @@ -164,7 +164,7 @@ export interface ColumnBearerProps gridModel: GridModelType; onDrag: ( phase: dragPhase, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, insertIdx: number, insertPos: number, columnPosition?: number diff --git a/vuu-ui/packages/vuu-datagrid/src/ColumnGroupHeader.tsx b/vuu-ui/packages/vuu-datagrid/src/ColumnGroupHeader.tsx index ae12709b1..9ae739844 100644 --- a/vuu-ui/packages/vuu-datagrid/src/ColumnGroupHeader.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/ColumnGroupHeader.tsx @@ -17,7 +17,7 @@ import { ColumnGroupType } from "./grid-model/gridModelTypes"; import { GridModel } from "./grid-model/gridModelUtils"; import { ColumnDragStartHandler, resizePhase } from "./gridTypes"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { isGroupColumn } from "@finos/vuu-utils"; import "./ColumnGroupHeader.css"; @@ -51,7 +51,7 @@ const ColumnGroupHeader = React.memo( const sortIndicator = ( sort: VuuSort | undefined, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) => { if (!sort || sort.sortDefs.length === 0) { return undefined; @@ -130,7 +130,7 @@ const ColumnGroupHeader = React.memo( ); const handleHeaderClick = useCallback( - (_groupColumn, column: KeyedColumnDescriptor) => { + (_groupColumn, column: RuntimeColumnDescriptor) => { if (gridModel) { dispatchGridAction?.({ type: "sort", diff --git a/vuu-ui/packages/vuu-datagrid/src/canvas/Canvas.tsx b/vuu-ui/packages/vuu-datagrid/src/canvas/Canvas.tsx index cc78e53f1..189c1308b 100644 --- a/vuu-ui/packages/vuu-datagrid/src/canvas/Canvas.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/canvas/Canvas.tsx @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { buildColumnMap, DataRow, metadataKeys } from "@finos/vuu-utils"; import cx from "classnames"; import { @@ -79,7 +79,7 @@ export interface CanvasProps { } export interface CanvasAPI { - beginDrag: (column: KeyedColumnDescriptor) => number | undefined; + beginDrag: (column: RuntimeColumnDescriptor) => number | undefined; beginHorizontalScroll: () => void; beginVerticalScroll: () => void; endDrag: ( @@ -88,7 +88,7 @@ export interface CanvasAPI { columnLeft: number ) => void; endHorizontalScroll: () => void; - isWithinScrollWindow: (column: KeyedColumnDescriptor) => boolean; + isWithinScrollWindow: (column: RuntimeColumnDescriptor) => boolean; endVerticalScroll: (scrollTol: number) => void; scrollBy: (distance: number) => number; scrollLeft: number; @@ -132,7 +132,7 @@ export const Canvas = forwardRef(function Canvas( dispatchCanvasAction({ type: "refresh", columnGroup }); }, [columnGroup.width, columnGroup.columns]); - const getColumnIdx = (column: KeyedColumnDescriptor) => + const getColumnIdx = (column: RuntimeColumnDescriptor) => columns.findIndex((col) => col.key === column.key); useImperativeHandle(forwardedRef, () => ({ @@ -245,7 +245,7 @@ export const Canvas = forwardRef(function Canvas( } }, - isWithinScrollWindow: (column: KeyedColumnDescriptor) => + isWithinScrollWindow: (column: RuntimeColumnDescriptor) => getColumnIdx(column) !== -1, scrollBy: (scrollDistance: number) => scrollBy(scrollDistance), diff --git a/vuu-ui/packages/vuu-datagrid/src/canvas/canvas-reducer.ts b/vuu-ui/packages/vuu-datagrid/src/canvas/canvas-reducer.ts index 3db68281b..541f11210 100644 --- a/vuu-ui/packages/vuu-datagrid/src/canvas/canvas-reducer.ts +++ b/vuu-ui/packages/vuu-datagrid/src/canvas/canvas-reducer.ts @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { Reducer } from "react"; import { ColumnGroupType } from "../grid-model/gridModelTypes"; @@ -23,7 +23,7 @@ export interface CanvasActionRefresh { export type CanvasAction = CanvasActionRefresh | CanvasActionScrollLeft; export type CanvasState = [ - KeyedColumnDescriptor[], + RuntimeColumnDescriptor[], Map, ColumnGroupType, number @@ -57,12 +57,12 @@ export const canvasReducer: CanvasReducer = (state, action) => { } }; -function initialKeys(columns: KeyedColumnDescriptor[]) { +function initialKeys(columns: RuntimeColumnDescriptor[]) { return new Map(columns.map((column, idx) => [column.key, idx])); } function nextKeys( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], prevKeys: Map ): Map { if (columns.every(({ key }) => prevKeys.has(key))) { @@ -111,7 +111,7 @@ function nextKeys( function getRenderColumns( columnGroup: ColumnGroupType, scrollLeft = 0 -): KeyedColumnDescriptor[] { +): RuntimeColumnDescriptor[] { if (!isVirtualizationRequired(columnGroup)) { return columnGroup.columns; } diff --git a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/use-direction.ts b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/use-direction.ts index b604a67d9..1b3b71d0b 100644 --- a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/use-direction.ts +++ b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/use-direction.ts @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { getMovingValueDirection, isTypeDescriptor, @@ -9,12 +9,12 @@ import { useEffect, useRef } from "react"; const INITIAL_VALUE = [undefined, undefined, undefined, undefined]; -type State = [string, unknown, KeyedColumnDescriptor, valueChangeDirection]; +type State = [string, unknown, RuntimeColumnDescriptor, valueChangeDirection]; export function useDirection( key: string, value: unknown, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { const ref = useRef(); const [prevKey, prevValue, prevColumn, prevDirection] = diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts index b581ae1de..de0111188 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { isNumericColumn } from "@finos/vuu-utils"; import { ContextMenuGroupItemDescriptor, @@ -126,7 +126,7 @@ function buildAggregationMenuItems( const getSortType = ( sortCols?: VuuSortCol[], - column?: KeyedColumnDescriptor + column?: RuntimeColumnDescriptor ): "A" | "D" | void => { if (sortCols && column) { const sortCol = sortCols.find((sortDef) => sortDef.column === column.name); diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/contextMenuTypes.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/contextMenuTypes.ts index c29ee4b50..397db40c5 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/contextMenuTypes.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/contextMenuTypes.ts @@ -1,9 +1,9 @@ import { Filter } from "@finos/vuu-filter-types"; import { VuuFilter } from "@finos/vuu-protocol-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; export interface ContextMenuOptions { - column?: KeyedColumnDescriptor; + column?: RuntimeColumnDescriptor; filter?: Filter; sort?: VuuFilter; } diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts index ae21ba02a..417fb7b3b 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts @@ -1,7 +1,7 @@ /* eslint-disable no-sequences */ import { DataSource } from "@finos/vuu-data"; import { DataSourceFilter, MenuActionHandler } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { MenuActionClosePopup } from "@finos/vuu-popups"; import { removeColumnFromFilter, setAggregations } from "@finos/vuu-utils"; import { AggregationType } from "../constants"; @@ -19,7 +19,7 @@ export interface ContextMenuHookProps { const removeFilterColumn = ( dataSourceFilter: DataSourceFilter, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) => { if (dataSourceFilter.filterStruct && column) { const [filterStruct, filter] = removeColumnFromFilter( diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx index 00471a5a3..22f6543cf 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GridCell.tsx @@ -1,6 +1,6 @@ import { ColumnTypeRendering, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { ColumnMap, DataRow, isTypeDescriptor } from "@finos/vuu-utils"; import cx from "classnames"; @@ -10,7 +10,7 @@ import { useCellFormatter } from "./useCellFormatter"; import "./GridCell.css"; -const columnType = (column: KeyedColumnDescriptor) => +const columnType = (column: RuntimeColumnDescriptor) => !column.type ? null : typeof column.type === "string" @@ -18,7 +18,7 @@ const columnType = (column: KeyedColumnDescriptor) => : column.type.name; // TODO we want to allow css class to be determined by value -function useGridCellClassName(column: KeyedColumnDescriptor) { +function useGridCellClassName(column: RuntimeColumnDescriptor) { // const count = getInstanceCount(classes); // console.log(`instance count = ${JSON.stringify(count)}`) @@ -39,7 +39,7 @@ const cellValuesAreEqual = (prev: GridCellProps, next: GridCellProps) => { }; export interface GridCellProps extends HTMLAttributes { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; columnMap: ColumnMap; row: DataRow; } diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GroupHeaderCell.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GroupHeaderCell.tsx index 3e8d13581..b308ac813 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/GroupHeaderCell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/GroupHeaderCell.tsx @@ -1,6 +1,6 @@ import { GroupColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { useContextMenu } from "@finos/vuu-popups"; import cx from "classnames"; @@ -16,11 +16,11 @@ const classBase = "hwGroupHeaderCell"; export interface ColHeaderProps extends Omit, "onClick"> { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; expandState: 0 | 1 | -1; - onClick: (column: KeyedColumnDescriptor) => void; - onRemoveColumn: (column: KeyedColumnDescriptor) => void; - onToggle: (column: KeyedColumnDescriptor, expandState: number) => void; + onClick: (column: RuntimeColumnDescriptor) => void; + onRemoveColumn: (column: RuntimeColumnDescriptor) => void; + onToggle: (column: RuntimeColumnDescriptor, expandState: number) => void; } const ColHeader = (props: ColHeaderProps) => { const { column, className, onClick, onRemoveColumn, expandState, onToggle } = @@ -58,9 +58,9 @@ export interface GroupHeaderCellProps groupState?: any; onClick: ( groupCol: GroupColumnDescriptor, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) => void; - onRemoveColumn: (column: KeyedColumnDescriptor) => void; + onRemoveColumn: (column: RuntimeColumnDescriptor) => void; onToggleGroupState: () => void; } @@ -87,7 +87,7 @@ export const GroupHeaderCell = ({ }); const handleClick = useCallback( - (column: KeyedColumnDescriptor) => { + (column: RuntimeColumnDescriptor) => { onClick(groupCol, column); }, [groupCol, onClick] diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx index 956020dc8..fee3f9557 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx @@ -1,5 +1,5 @@ import { useContextMenu } from "@finos/vuu-popups"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import cx from "classnames"; import { MouseEvent, useCallback, useRef } from "react"; import { AggregationType } from "../constants"; @@ -34,7 +34,7 @@ export interface HeaderCellProps filter?: DataSourceFilter; onDrag?: ( phase: dragPhase, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, columnPosition: number, mousePosition: number ) => void; @@ -51,7 +51,7 @@ export const HeaderCell = function HeaderCell({ sorted, }: HeaderCellProps) { const rootRef = useRef(null); - const col = useRef(column); + const col = useRef(column); // const isResizing = useRef(false); const { dispatchGridAction, gridModel } = useGridContext(); diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx index 886ef56a6..cfe17715f 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx @@ -5,7 +5,7 @@ import cx from "classnames"; import { HTMLAttributes, useCallback, useMemo } from "react"; import "./filter-indicator.css"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; export const Direction = { ASC: "asc", @@ -13,7 +13,7 @@ export const Direction = { }; export interface FilterIndicatorProps extends HTMLAttributes { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; filter?: Filter; } diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts index 0abefbe50..7be5df7aa 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellFormatter.ts @@ -1,6 +1,6 @@ import { isTypeDescriptor, roundDecimal } from "@finos/vuu-utils"; import { - KeyedColumnDescriptor, + RuntimeColumnDescriptor, ColumnTypeFormatting, } from "@finos/vuu-datagrid-types"; import { createElement, useRef } from "react"; @@ -8,7 +8,7 @@ import { createElement, useRef } from "react"; const defaultFormatter = (value: unknown) => value == null ? "" : typeof value === "string" ? value : value.toString(); -const getFormatter = (column: KeyedColumnDescriptor) => { +const getFormatter = (column: RuntimeColumnDescriptor) => { if (isTypeDescriptor(column.type)) { const { name, formatting } = column.type; if (name === "number") { @@ -18,7 +18,7 @@ const getFormatter = (column: KeyedColumnDescriptor) => { return defaultFormatter; }; -export const useCellFormatter = (column: KeyedColumnDescriptor) => { +export const useCellFormatter = (column: RuntimeColumnDescriptor) => { const formatter = useRef(getFormatter(column)); return [formatter.current]; }; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellResize.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellResize.tsx index 5ce34c20a..ff6d2ac7a 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellResize.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/useCellResize.tsx @@ -1,10 +1,10 @@ import { RefObject, useCallback, useRef } from "react"; -import { Heading, KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { Heading, RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { resizePhase } from "../gridTypes"; export type ResizeHandler = (evt: MouseEvent, moveBy: number) => void; export interface CellResizeHookProps { - column: KeyedColumnDescriptor | Heading; + column: RuntimeColumnDescriptor | Heading; onResize?: (phase: resizePhase, columnName: string, width?: number) => void; rootRef: RefObject; } diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx index 87e908643..869e89343 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx @@ -12,7 +12,7 @@ import { resizePhase } from "./gridTypes"; import { ColumnDescriptor, GridAction, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { DataSourceFilter } from "@finos/vuu-data-types"; @@ -60,7 +60,7 @@ export interface GridActionSort { export interface GridModelActionAddCol { type: "add-col"; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; insertIdx: number; } @@ -72,7 +72,7 @@ export interface GridModelActionAggregate { export interface GridModelActionHideColumn { type: "column-hide"; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; } export interface GridModelActionGridConfig { type: "grid-config"; @@ -122,7 +122,7 @@ export interface GridModelActionRowHeight { } export interface GridModelActionShowColumn { type: "column-show"; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; } export interface GridModelActionSort { type: "sort"; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-model/GridModelReducer.ts b/vuu-ui/packages/vuu-datagrid/src/grid-model/GridModelReducer.ts index e8edcccb5..e52c34fa2 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-model/GridModelReducer.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-model/GridModelReducer.ts @@ -2,7 +2,7 @@ import { isSimpleColumnType, metadataKeys } from "@finos/vuu-utils"; import { ColumnDescriptor, ColumnTypeSimple, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { ColumnGroupType, @@ -45,7 +45,7 @@ import { Reducer } from "react"; const DEFAULT_COLUMN_MIN_WIDTH = 30; const DEFAULT_COLUMN_WIDTH = 80; const DEFAULT_COLUMN_TYPE = { name: "string" as ColumnTypeSimple }; -const CHECKBOX_COLUMN: KeyedColumnDescriptor = { +const CHECKBOX_COLUMN: RuntimeColumnDescriptor = { label: "", name: "", key: metadataKeys.SELECTED, @@ -61,7 +61,7 @@ const CHECKBOX_COLUMN: KeyedColumnDescriptor = { valueFormatter: undefined, }; -const LINE_NUMBER_COLUMN: KeyedColumnDescriptor = { +const LINE_NUMBER_COLUMN: RuntimeColumnDescriptor = { className: "vuuLineNumber", flex: 0, isSystemColumn: true, @@ -481,7 +481,7 @@ function hideColumn( { column }: GridModelActionHideColumn ) { const columns = GridModel.columns(state).filter( - (col: KeyedColumnDescriptor) => col.name !== column.name + (col: RuntimeColumnDescriptor) => col.name !== column.name ); // const groupBy = GridModel.groupBy(state); const { columnNames, columnGroups } = buildColumnGroups( @@ -656,7 +656,7 @@ function buildColumnGroups( const columnGroups: ColumnGroupType[] = []; const gridContentWidth = gridWidth - 17; // how do we know about vertical scrollbar let availableWidth = gridContentWidth; - const preCols: KeyedColumnDescriptor[] = + const preCols: RuntimeColumnDescriptor[] = selectionModel === "checkbox" ? [CHECKBOX_COLUMN] : showLineNumbers @@ -719,7 +719,7 @@ function buildColumnGroups( headings, isGroup: true as const, locked, - columns: [] as KeyedColumnDescriptor[], + columns: [] as RuntimeColumnDescriptor[], left: totalColumnWidth, // TODO this won't be right if we introduce more than one locked group width: 0, contentWidth: 0, @@ -829,7 +829,7 @@ const getMaxHeadingDepth = (columns: ColumnDescriptor[]) => { function addColumnToHeadings( maxHeadingDepth: number, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, headings: Headings, collapsedColumns?: string[] ) { diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelTypes.ts b/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelTypes.ts index 12d853c3b..01146d7ca 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelTypes.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelTypes.ts @@ -5,14 +5,14 @@ import { AdornmentsDescriptor } from "../grid-adornments"; import { GridModelDispatch } from "../grid-context"; import { GridProps } from "../gridTypes"; import { Size } from "./useMeasuredSize"; -import { Heading, KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { Heading, RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; export type Headings = Heading[][]; export type GridModelStatus = "pending" | "ready"; export type ColumnGroupType = { - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; contentWidth: number; headings?: Headings; isGroup: true; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelUtils.ts b/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelUtils.ts index 2c1fe5feb..c0484fe1a 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelUtils.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-model/gridModelUtils.ts @@ -9,7 +9,7 @@ import { ColumnDescriptor, GroupColumnDescriptor, Heading, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { ColumnGroupType, GridModelType } from "./gridModelTypes"; @@ -59,7 +59,7 @@ export const getColumnHeading = ( export function getColumnGroup( { columnGroups }: GridModelType, - target: number | KeyedColumnDescriptor + target: number | RuntimeColumnDescriptor ) { const isNumber = typeof target === "number"; if (isNumber && columnGroups) { @@ -92,7 +92,7 @@ export function getColumnGroup( export function getColumnGroupIdx( { columnGroups }: GridModelType, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { if (columnGroups) { for (let i = 0; i < columnGroups.length; i++) { @@ -105,7 +105,7 @@ export function getColumnGroupIdx( } const cloneColumn = ( - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, { locked }: { locked?: boolean } ) => { return { ...column, locked }; @@ -114,7 +114,7 @@ const cloneColumn = ( export const ColumnGroup = { insertColumnAt: ( columnGroup: ColumnGroupType, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, idx: number ) => { const columns = columnGroup.columns.slice(); @@ -123,7 +123,7 @@ export const ColumnGroup = { }, moveColumnTo: ( columnGroup: ColumnGroupType, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, idx: number ) => { const sourceIdx = columnGroup.columns.findIndex( @@ -235,7 +235,7 @@ function updateGroupColumnWidth( updateColumnHeading(updatedGroup); const resizedColumn = updatedGroup.columns.find( (col) => col.name === columnName - ) as KeyedColumnDescriptor; + ) as RuntimeColumnDescriptor; const widthAdjustment = width - resizedColumn.width; // why isn't this done already ? @@ -371,12 +371,12 @@ function updateGroupColumn( export const columnKeysToIndices = ( keys: number[], - columns: KeyedColumnDescriptor[] + columns: RuntimeColumnDescriptor[] ) => keys.map((key) => columns.findIndex((c) => c.key === key)); function addGroupColumn( { groupBy }: { groupBy?: VuuGroupBy }, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { if (groupBy) { return groupBy.concat(column.name); @@ -402,7 +402,7 @@ function addSortColumn( function setSortColumn( gridModel: GridModelType, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, sortType?: "A" | "D" ): VuuSort { if (sortType === undefined) { @@ -425,7 +425,7 @@ function setSortColumn( function removeGroupColumn( { groupBy }: { groupBy?: VuuGroupBy }, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ): VuuGroupBy | undefined { if (Array.isArray(groupBy)) { if (groupBy.length === 1) { @@ -440,19 +440,19 @@ function removeGroupColumn( } } -const omitSystemColumns = (column: KeyedColumnDescriptor) => +const omitSystemColumns = (column: RuntimeColumnDescriptor) => !column.isSystemColumn; const addColumnToColumns = ( - column: KeyedColumnDescriptor, - columns: KeyedColumnDescriptor[], + column: RuntimeColumnDescriptor, + columns: RuntimeColumnDescriptor[], index: number ) => { const currentIndex = columns.findIndex((col) => col.name === column.name); if (currentIndex === index) { return columns; } else { - const newColumns: KeyedColumnDescriptor[] = + const newColumns: RuntimeColumnDescriptor[] = currentIndex !== -1 ? columns.filter((_, i) => i !== currentIndex) : columns.slice(); @@ -461,7 +461,7 @@ const addColumnToColumns = ( } }; -const countLeadingSystemColumns = (columns: KeyedColumnDescriptor[]) => { +const countLeadingSystemColumns = (columns: RuntimeColumnDescriptor[]) => { let count = 0; for (let i = 0; i < columns.length; i++) { if (!columns[i].isSystemColumn) { @@ -475,7 +475,7 @@ const countLeadingSystemColumns = (columns: KeyedColumnDescriptor[]) => { const setAggregation = ( { aggregations }: GridModelType, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, aggType: VuuAggType ) => { return aggregations @@ -532,14 +532,14 @@ export function expandStatesfromGroupState( return results; } -const flattenColumnGroup = (columns: KeyedColumnDescriptor[]) => { +const flattenColumnGroup = (columns: RuntimeColumnDescriptor[]) => { if (columns.length === 0 || !columns[0].isGroup) { return columns; } const [groupColumn, ...nonGroupColumns] = columns as [ GroupColumnDescriptor, - ...KeyedColumnDescriptor[] + ...RuntimeColumnDescriptor[] ]; // traverse the group columns in reverse, but do not reverse (mutate) the original array for (let i = groupColumn.columns.length - 1; i >= 0; i--) { @@ -554,9 +554,9 @@ const flattenColumnGroup = (columns: KeyedColumnDescriptor[]) => { }; export function extractGroupColumn( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], groupBy?: VuuGroupBy -): [GroupColumnDescriptor | null, KeyedColumnDescriptor[]] { +): [GroupColumnDescriptor | null, RuntimeColumnDescriptor[]] { if (groupBy && groupBy.length > 0) { // Note: groupedColumns will be in column order, not groupBy order const [groupedColumns, rest] = columns.reduce( @@ -573,7 +573,7 @@ export function extractGroupColumn( return result; }, - [[], []] as [KeyedColumnDescriptor[], KeyedColumnDescriptor[]] + [[], []] as [RuntimeColumnDescriptor[], RuntimeColumnDescriptor[]] ); if (groupedColumns.length !== groupBy.length) { throw Error( @@ -583,11 +583,11 @@ export function extractGroupColumn( ); } const groupCount = groupBy.length; - const groupCols: KeyedColumnDescriptor[] = groupBy.map((name, idx) => { + const groupCols: RuntimeColumnDescriptor[] = groupBy.map((name, idx) => { // Keep the cols in same order defined on groupBy const column = groupedColumns.find( (col) => col.name === name - ) as KeyedColumnDescriptor; + ) as RuntimeColumnDescriptor; return { ...column, groupLevel: groupCount - idx, @@ -611,13 +611,13 @@ export function extractGroupColumn( export const splitKeys = (compositeKey: string): number[] => `${compositeKey}`.split(":").map((k) => parseInt(k, 10)); -const isKeyedColumn = (column: unknown): column is KeyedColumnDescriptor => - typeof (column as KeyedColumnDescriptor).key === "number"; +const isKeyedColumn = (column: unknown): column is RuntimeColumnDescriptor => + typeof (column as RuntimeColumnDescriptor).key === "number"; export const assignKeysToColumns = ( - columns: (ColumnDescriptor | KeyedColumnDescriptor | string)[], + columns: (ColumnDescriptor | RuntimeColumnDescriptor | string)[], defaultWidth: number -): KeyedColumnDescriptor[] => { +): RuntimeColumnDescriptor[] => { const start = metadataKeys.count; return columns.map((column, i) => typeof column === "string" @@ -643,7 +643,7 @@ export const assignKeysToColumns = ( const { DEPTH, IS_LEAF } = metadataKeys; export const getGroupValueAndOffset = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], row: DataRow ): [unknown, number | null] => { const { [DEPTH]: depth, [IS_LEAF]: isLeaf } = row; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-row.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-row.tsx index 6c0a0666f..757d1b921 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-row.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-row.tsx @@ -9,7 +9,7 @@ import { import { GridCell, GroupCell } from "./grid-cells"; import "./grid-row.css"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; const classBase = "vuuDataGridRow"; @@ -18,7 +18,7 @@ const { KEY, SELECTED, IS_LEAF, IS_EXPANDED } = metadataKeys; export interface RowProps extends Omit, "onClick"> { columnMap: ColumnMap; - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; height: number; idx: number; onClick: ( diff --git a/vuu-ui/packages/vuu-datagrid/src/gridTypes.ts b/vuu-ui/packages/vuu-datagrid/src/gridTypes.ts index f0aec0f9f..746014964 100644 --- a/vuu-ui/packages/vuu-datagrid/src/gridTypes.ts +++ b/vuu-ui/packages/vuu-datagrid/src/gridTypes.ts @@ -2,7 +2,7 @@ import { ConfigChangeHandler, DataSource } from "@finos/vuu-data"; import { DataSourceFilter } from "@finos/vuu-data-types"; import { ColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { VuuAggregation, @@ -18,7 +18,7 @@ export type dragPhase = "drag-start" | "drag" | "drag-end"; export type resizePhase = "begin" | "resize" | "end"; export type ColumnDragState = { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; columnGroupIdx: number; columnIdx: number; initialColumnPosition: number; @@ -87,7 +87,7 @@ export interface ViewportProps { onChangeRange: (range: VuuRange) => void; onColumnDrop?: ( phase: dragPhase, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, index: number ) => void; onColumnDragStart?: ColumnDragStartHandler; diff --git a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx b/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx deleted file mode 100644 index 1b60895f8..000000000 --- a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { GridConfig } from "@finos/vuu-datagrid-types"; -import { Table, TablePropsDeprecated as TableProps } from "@finos/vuu-table"; -import { ReactElement, useCallback, useState } from "react"; -import { Dialog } from "@finos/vuu-popups"; - -export const ConfigurableTable = ({ - config, - dataSource, - ...restProps -}: TableProps) => { - const [dialogContent, setDialogContent] = useState(null); - const [tableConfig] = useState>(config); - - const hideSettings = useCallback(() => { - setDialogContent(null); - }, []); - - return ( - <> - - - {dialogContent} - - - ); -}; diff --git a/vuu-ui/packages/vuu-datatable/src/configurable-table/index.ts b/vuu-ui/packages/vuu-datatable/src/configurable-table/index.ts deleted file mode 100644 index 3fe444e02..000000000 --- a/vuu-ui/packages/vuu-datatable/src/configurable-table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ConfigurableTable"; diff --git a/vuu-ui/packages/vuu-datatable/src/index.ts b/vuu-ui/packages/vuu-datatable/src/index.ts index 2aa9e6454..6772e4a5c 100644 --- a/vuu-ui/packages/vuu-datatable/src/index.ts +++ b/vuu-ui/packages/vuu-datatable/src/index.ts @@ -1,3 +1,2 @@ -export * from "./configurable-table"; export * from "./filter-table"; export * from "./json-table"; diff --git a/vuu-ui/packages/vuu-layout/src/dock-layout/Drawer.tsx b/vuu-ui/packages/vuu-layout/src/dock-layout/Drawer.tsx index f2e2d4240..673296b26 100644 --- a/vuu-ui/packages/vuu-layout/src/dock-layout/Drawer.tsx +++ b/vuu-ui/packages/vuu-layout/src/dock-layout/Drawer.tsx @@ -67,6 +67,8 @@ const Drawer = ({ state: "open", }); + console.log(`Drawer sizeOpen ${sizeOpen} sizeClosed ${sizeClosed}`); + const className = cx(classBase, classNameProp, `${classBase}-${position}`, { [`${classBase}-open`]: open, [`${classBase}-inline`]: inline, diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts index 88ca5bf96..3ac7c097d 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts @@ -1,4 +1,4 @@ -import { LayoutJSON } from "@finos/vuu-layout"; +import { ApplicationJSON, LayoutJSON } from "@finos/vuu-layout"; import { LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell"; export interface LayoutPersistenceManager { @@ -52,16 +52,18 @@ export interface LayoutPersistenceManager { loadMetadata: () => Promise; /** - * Retrieves the application layout which includes all layouts on screen + * Retrieves the application JSON. This includes the application layout, + * which describes all layouts on screen * - * @returns Full JSON representation of the application layout + * @returns Full JSON representation of the application json */ - loadApplicationLayout: () => Promise; + loadApplicationJSON: () => Promise; /** - * Saves the application layout which includes all layouts on screen + * Saves the application JSON. This includes the application layout, + * which describes all layouts on screen * * @param layout - Full JSON representation of the application layout to be saved */ - saveApplicationLayout: (layout: LayoutJSON) => Promise; + saveApplicationJSON: (layout: ApplicationJSON) => Promise; } 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 7a9d47e5c..dbcf80ad6 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -4,11 +4,15 @@ import { LayoutMetadataDto, WithId, } from "@finos/vuu-shell"; -import { LayoutJSON, LayoutPersistenceManager } from "@finos/vuu-layout"; +import { + ApplicationJSON, + LayoutJSON, + LayoutPersistenceManager, +} from "@finos/vuu-layout"; import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; import { formatDate, getUniqueId } from "@finos/vuu-utils"; -import { defaultLayout } from "./defaultLayout"; +import { defaultApplicationJson } from "./defaultApplicationJson"; const metadataSaveLocation = "layouts/metadata"; const layoutsSaveLocation = "layouts/layouts"; @@ -107,24 +111,27 @@ export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { }); } - loadApplicationLayout(): Promise { + loadApplicationJSON(): Promise { return new Promise((resolve) => { - const applicationLayout = getLocalEntity(this.#urlKey); - if (applicationLayout) { - resolve(applicationLayout); + const applicationJSON = getLocalEntity(this.#urlKey); + if (applicationJSON) { + resolve(applicationJSON); } else { - resolve(defaultLayout); + resolve(defaultApplicationJson); } }); } - saveApplicationLayout(layout: LayoutJSON): Promise { + saveApplicationJSON(applicationJSON: ApplicationJSON): Promise { return new Promise((resolve, reject) => { - const savedLayout = saveLocalEntity(this.#urlKey, layout); + const savedLayout = saveLocalEntity( + this.#urlKey, + applicationJSON + ); if (savedLayout) { resolve(); } else { - reject(new Error("Layout failed to save")); + reject(new Error("Application Json failed to save")); } }); } diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts index 3a0007a33..30a53f538 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts @@ -1,9 +1,6 @@ -import { - LayoutMetadata, - LayoutMetadataDto, -} from "@finos/vuu-shell"; +import { LayoutMetadata, LayoutMetadataDto } from "@finos/vuu-shell"; import { LayoutPersistenceManager } from "./LayoutPersistenceManager"; -import { LayoutJSON } from "../layout-reducer"; +import { ApplicationJSON, LayoutJSON } from "../layout-reducer"; const baseURL = process.env.LAYOUT_BASE_URL; const metadataSaveLocation = "layouts/metadata"; @@ -134,7 +131,7 @@ export class RemoteLayoutPersistenceManager ); } - saveApplicationLayout(layout: LayoutJSON): Promise { + saveApplicationJSON(applicationJSON: ApplicationJSON): Promise { return new Promise((resolve, reject) => fetch(`${baseURL}/${applicationLayoutsSaveLocation}`, { method: "PUT", @@ -142,7 +139,7 @@ export class RemoteLayoutPersistenceManager "Content-Type": "application/json", username: "vuu-user", }, - body: JSON.stringify(layout), + body: JSON.stringify(applicationJSON), }) .then((response) => { if (!response.ok) { @@ -156,7 +153,7 @@ export class RemoteLayoutPersistenceManager ); } - loadApplicationLayout(): Promise { + loadApplicationJSON(): Promise { return new Promise((resolve, reject) => fetch(`${baseURL}/${applicationLayoutsSaveLocation}`, { method: "GET", @@ -168,15 +165,15 @@ export class RemoteLayoutPersistenceManager if (!response.ok) { reject(new Error(response.statusText)); } - response.json().then((applicationLayout: LayoutJSON) => { - if (!applicationLayout) { + response.json().then((applicationJSON: ApplicationJSON) => { + if (!applicationJSON) { reject( new Error( "Response did not contain valid application layout information" ) ); } - resolve(applicationLayout); + resolve(applicationJSON); }); }) .catch((error: Error) => { diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultApplicationJson.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultApplicationJson.ts new file mode 100644 index 000000000..a8e805109 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultApplicationJson.ts @@ -0,0 +1,54 @@ +import { ApplicationJSON, LayoutJSON } from "../layout-reducer"; + +export const warningLayout: LayoutJSON = { + type: "View", + props: { + style: { height: "calc(100% - 6px)" }, + }, + children: [ + { + props: { + className: "vuuShell-warningPlaceholder", + }, + type: "Placeholder", + }, + ], +}; + +export const loadingApplicationJson: Readonly = { + layout: { + type: "Component", + id: "loading-main", + props: {}, + }, +}; + +export const defaultApplicationJson: ApplicationJSON = { + layout: { + type: "Stack", + id: "main-tabs", + props: { + className: "vuuShell-mainTabs", + TabstripProps: { + allowAddTab: true, + allowCloseTab: true, + allowRenameTab: true, + animateSelectionThumb: false, + location: "main-tab", + tabClassName: "MainTab", + }, + preserve: true, + active: 0, + }, + children: [ + { + props: { + id: "tab1", + title: "Tab 1", + className: "vuuShell-Placeholder", + }, + type: "Placeholder", + }, + ], + }, +}; diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultLayout.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultLayout.ts deleted file mode 100644 index f0beb8e20..000000000 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/defaultLayout.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { LayoutJSON } from "../layout-reducer"; - -export const warningLayout: LayoutJSON = { - type: "View", - props: { - style: { height: "calc(100% - 6px)" }, - }, - children: [ - { - props: { - className: "vuuShell-warningPlaceholder", - }, - type: "Placeholder", - }, - ], -}; - -export const defaultLayout: LayoutJSON = { - type: "Stack", - id: "main-tabs", - props: { - className: "vuuShell-mainTabs", - TabstripProps: { - allowAddTab: true, - allowCloseTab: true, - allowRenameTab: true, - animateSelectionThumb: false, - location: "main-tab", - tabClassName: "MainTab", - }, - preserve: true, - active: 0, - }, - children: [ - { - props: { - id: "tab1", - title: "Tab 1", - className: "vuuShell-Placeholder", - }, - type: "Placeholder", - }, - ], -}; diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts index 541131f1e..06c29a51c 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts @@ -1,4 +1,4 @@ -export * from "./defaultLayout"; +export * from "./defaultApplicationJson"; export * from "./LayoutPersistenceManager"; export * from "./LocalLayoutPersistenceManager"; export * from "./RemoteLayoutPersistenceManager"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx b/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx index d0e746636..264d7bb45 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx @@ -60,6 +60,7 @@ export const useLayoutContextMenuItems = (setDialogState: SetDialog) => { onCancel={handleCloseDialog} onSave={handleSave} componentId={action.options.controlledComponentId} + defaultTitle={action.options.controlledComponentTitle} /> ), title: "Save Layout", diff --git a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx index df29ca34c..5981dbfc3 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx @@ -4,7 +4,6 @@ import { useCallback, useContext, useEffect, - useMemo, useRef, useState, } from "react"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts index 761c9c1ff..238a29564 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { LeftNavProps } from "packages/vuu-shell/src"; import { CSSProperties, ReactElement } from "react"; import { DragDropRect, DragInstructions } from "../drag-drop"; import { DropTarget } from "../drag-drop/DropTarget"; @@ -20,6 +21,18 @@ export interface LayoutRoot extends WithProps { type: string; } +export interface ApplicationSettings { + leftNav?: { + activeTabIndex: number; + expanded: boolean; + }; +} + +export interface ApplicationJSON { + layout: LayoutJSON; + settings?: ApplicationSettings; +} + export interface LayoutJSON extends WithType { active?: number; children?: LayoutJSON[]; @@ -209,7 +222,8 @@ export type ApplicationLevelChange = | "switch-active-layout" | "open-layout" | "close-layout" - | "rename-layout"; + | "rename-layout" + | "resize-application-chrome"; export type LayoutChangeReason = LayoutLevelChange | ApplicationLevelChange; diff --git a/vuu-ui/packages/vuu-layout/src/stack/Stack.css b/vuu-ui/packages/vuu-layout/src/stack/Stack.css index 1869b913b..f40439272 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/Stack.css +++ b/vuu-ui/packages/vuu-layout/src/stack/Stack.css @@ -19,6 +19,14 @@ background: var(--grey60); } +.Tabs-tabPanel { + flex: 1; +} + +.Tabs-tabPanel > * { + height: 100%; +} + .vuuTabHeader { --saltTabs-activationIndicator-background: transparent; --saltToolbarField-marginTop: calc(var(--salt-size-unit) - 1px); @@ -27,11 +35,6 @@ border-width: 1px; } -.vuuTabHeader + .hwFlexbox, -.vuuTabHeader + * { - flex: 1; -} - .vuuTabHeader + .vuuView > .vuuHeader { height: 0; overflow: hidden; diff --git a/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx b/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx index 5ed3f5b53..5094c458a 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx +++ b/vuu-ui/packages/vuu-layout/src/stack/Stack.tsx @@ -158,7 +158,13 @@ export const Stack = forwardRef(function Stack( {renderTabs()} ) : null} - {child} +
+ {child} +
); }); diff --git a/vuu-ui/packages/vuu-popups/src/notifications/NotificationsProvider.tsx b/vuu-ui/packages/vuu-popups/src/notifications/NotificationsProvider.tsx index 1eddb05b6..eadbc5825 100644 --- a/vuu-ui/packages/vuu-popups/src/notifications/NotificationsProvider.tsx +++ b/vuu-ui/packages/vuu-popups/src/notifications/NotificationsProvider.tsx @@ -1,10 +1,12 @@ import React, { useState, useContext, useCallback, useEffect } from "react"; -import classNames from "classnames" +import classNames from "classnames"; import { getUniqueId } from "@finos/vuu-utils"; -import "./notifications.css" +import "./notifications.css"; +import { Portal } from "../portal"; // animation times in milliseconds +const toastOffsetTop = 60; const toastDisplayDuration = 6000; const horizontalTransitionDuration = 1000; const verticalTransitionDuration = 300; @@ -14,11 +16,10 @@ const toastHeight = 56; const toastWidth = 300; const toastContainerContentGap = 10; const toastContainerLeftPadding = 10; -// rightPadding is used together with the toastWidth to compute the toast position +// rightPadding is used together with the toastWidth to compute the toast position // at the beginning and at the end of the animation const toastContainerRightPadding = 50; - const classBase = "vuuToastNotifications"; export enum NotificationLevel { @@ -29,96 +30,106 @@ export enum NotificationLevel { } type Notification = { - type: NotificationLevel, - header: string, - body: string, - id: string -} + type: NotificationLevel; + header: string; + body: string; + id: string; +}; export const NotificationsContext = React.createContext<{ - notify: (notification: Omit) => void, + notify: (notification: Omit) => void; }>({ - notify: () => "have you forgotten to provide a NotificationProvider?" -}) + notify: () => "have you forgotten to provide a NotificationProvider?", +}); export const NotificationsProvider = (props: { - children: JSX.Element | JSX.Element[] + children: JSX.Element | JSX.Element[]; }) => { const [notifications, setNotifications] = useState([]); const notify = useCallback((notification: Omit) => { - const newNotification = { ...notification, id: getUniqueId() } + const newNotification = { ...notification, id: getUniqueId() }; - setNotifications(prev => [...prev, newNotification]) + setNotifications((prev) => [...prev, newNotification]); setTimeout(() => { - setNotifications(prev => prev.filter(n => n !== newNotification)) - }, toastDisplayDuration + horizontalTransitionDuration * 2) - }, []) + setNotifications((prev) => prev.filter((n) => n !== newNotification)); + }, toastDisplayDuration + horizontalTransitionDuration * 2); + }, []); return (
- { - notifications.map((notification, i) => - - ) - } + {notifications.map((notification, i) => ( + + ))}
{props.children}
- ) -} + ); +}; export const useNotifications = () => useContext(NotificationsContext); type ToastNotificationProps = { - top: number, - notification: Notification, - animated?: boolean -} + top: number; + notification: Notification; + animated?: boolean; +}; // Only exported for use in individual toast examples. Normal usage will be through the provider export const ToastNotification = (props: ToastNotificationProps) => { + const { top, notification, animated = true } = props; - const { - top, - notification, - animated = true - } = props; - - const [right, setRight] = useState(-toastWidth - toastContainerRightPadding) + const [right, setRight] = useState(-toastWidth - toastContainerRightPadding); useEffect(() => { - setRight(toastContainerRightPadding) + setRight(toastContainerRightPadding); if (animated) { - setTimeout(() => setRight(-toastWidth - toastContainerRightPadding), toastDisplayDuration + horizontalTransitionDuration) + setTimeout( + () => setRight(-toastWidth - toastContainerRightPadding), + toastDisplayDuration + horizontalTransitionDuration + ); } - }, [animated]) + }, [animated]); return ( -
-
-
- {notification.header} -
{notification.body}
+ +
+
+
+ + {notification.header} + +
{notification.body}
+
-
- ) -} +
+ ); +}; diff --git a/vuu-ui/packages/vuu-popups/src/notifications/notifications.css b/vuu-ui/packages/vuu-popups/src/notifications/notifications.css index 5d88b46bb..d49c8d00c 100644 --- a/vuu-ui/packages/vuu-popups/src/notifications/notifications.css +++ b/vuu-ui/packages/vuu-popups/src/notifications/notifications.css @@ -1,7 +1,6 @@ .vuuToastNotifications-toastContainer { --top: 60px; position: absolute; - z-index: 100000; right: 0; top: var(--top); overflow: hidden; @@ -18,6 +17,7 @@ gap: 8px; border-radius: 6px; box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.40); + z-index: 100000; } .vuuToastNotifications-toastContent{ diff --git a/vuu-ui/packages/vuu-protocol-types/index.d.ts b/vuu-ui/packages/vuu-protocol-types/index.d.ts index 56d5e0e05..757b367df 100644 --- a/vuu-ui/packages/vuu-protocol-types/index.d.ts +++ b/vuu-ui/packages/vuu-protocol-types/index.d.ts @@ -211,6 +211,20 @@ export interface ServerToClientRPC { method: string; result: any; } + +// TODO flesh out as we know more +export interface ServerToClientViewportRpcResponse { + action: { + msg: string; + type: "VP_RPC_FAILURE"; + }; + type: "VIEW_PORT_RPC_REPONSE"; + method: string; + namedParams: { [key: string]: VuuRowDataItemType }; + params: string[]; + vpId: string; +} + export interface ServerToClientEditRPC { action: unknown; type: "VP_EDIT_RPC_RESPONSE"; @@ -269,6 +283,7 @@ export declare type ServerToClientBody = | ServerToClientMenuReject | ServerToClientMenuSessionTableAction | ServerToClientRPC + | ServerToClientViewportRpcResponse | ServerToClientViewPortVisualLinks | ServerToClientOpenTreeNodeSuccess | ServerToClientCloseTreeNodeSuccess diff --git a/vuu-ui/packages/vuu-shell/src/feature/Loader.tsx b/vuu-ui/packages/vuu-shell/src/feature/Loader.tsx index b69f28004..8afcea8e9 100644 --- a/vuu-ui/packages/vuu-shell/src/feature/Loader.tsx +++ b/vuu-ui/packages/vuu-shell/src/feature/Loader.tsx @@ -1,2 +1,2 @@ // TODO -export const Loader = () =>
loading
; +export const Loader = () =>
; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx index b5aeea957..a86abd873 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/SaveLayoutPanel.tsx @@ -19,14 +19,15 @@ type RadioValue = (typeof radioValues)[number]; type SaveLayoutPanelProps = { componentId?: string; + defaultTitle?: string; onCancel: () => void; onSave: (layoutMetadata: LayoutMetadataDto) => void; }; export const SaveLayoutPanel = (props: SaveLayoutPanelProps) => { - const { onCancel, onSave, componentId } = props; + const { defaultTitle = "", onCancel, onSave, componentId } = props; - const [layoutName, setLayoutName] = useState(""); + const [layoutName, setLayoutName] = useState(defaultTitle); const [group, setGroup] = useState(""); const [checkValues, setCheckValues] = useState([]); const [radioValue, setRadioValue] = useState(radioValues[0]); 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 f5054cbed..799691457 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -6,13 +6,17 @@ import React, { useState, } from "react"; import { - defaultLayout, + ApplicationJSON, + ApplicationSettings, + loadingApplicationJson, LayoutJSON, LayoutPersistenceManager, LocalLayoutPersistenceManager, RemoteLayoutPersistenceManager, resolveJSONPath, + defaultApplicationJson, } from "@finos/vuu-layout"; +import { NotificationLevel, useNotifications } from "@finos/vuu-popups"; import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes"; let _persistenceManager: LayoutPersistenceManager; @@ -29,15 +33,18 @@ const getPersistenceManager = () => { export const LayoutManagementContext = React.createContext<{ layoutMetadata: LayoutMetadata[]; saveLayout: (n: LayoutMetadataDto) => void; - applicationLayout: LayoutJSON; + applicationJson: ApplicationJSON; saveApplicationLayout: (layout: LayoutJSON) => void; + saveApplicationSettings: (settings: ApplicationSettings) => void; loadLayoutById: (id: string) => void; }>({ layoutMetadata: [], saveLayout: () => undefined, - applicationLayout: defaultLayout, + // The default Application JSON will be served if no LayoutManagementProvider + applicationJson: defaultApplicationJson, saveApplicationLayout: () => undefined, - loadLayoutById: () => defaultLayout, + saveApplicationSettings: () => undefined, + loadLayoutById: () => undefined, }); type LayoutManagementProviderProps = { @@ -46,7 +53,7 @@ type LayoutManagementProviderProps = { const ensureLayoutHasTitle = ( layout: LayoutJSON, - layoutMetadata: LayoutMetadata + layoutMetadata: LayoutMetadataDto ) => { if (layout.props?.title !== undefined) { return layout; @@ -68,11 +75,12 @@ export const LayoutManagementProvider = ( // TODO this default should probably be a loading state rather than the placeholder // It will be replaced as soon as the localStorage/remote layout is resolved const [, forceRefresh] = useState({}); - const applicationLayoutRef = useRef(defaultLayout); + const { notify } = useNotifications(); + const applicationJSONRef = useRef(loadingApplicationJson); - const setApplicationLayout = useCallback( - (layout: LayoutJSON, rerender = true) => { - applicationLayoutRef.current = layout; + const setApplicationJSON = useCallback( + (applicationJSON: ApplicationJSON, rerender = true) => { + applicationJSONRef.current = applicationJSON; if (rerender) { forceRefresh({}); } @@ -80,6 +88,39 @@ export const LayoutManagementProvider = ( [] ); + const setApplicationLayout = useCallback( + (layout: LayoutJSON, rerender = true) => { + console.log(`save layout`, { + layout, + }); + setApplicationJSON( + { + ...applicationJSONRef.current, + layout, + }, + rerender + ); + }, + [setApplicationJSON] + ); + + const setApplicationSettings = useCallback( + (settings: ApplicationSettings) => { + console.log(`save settings`); + setApplicationJSON( + { + ...applicationJSONRef.current, + settings: { + ...applicationJSONRef.current.settings, + ...settings, + }, + }, + false + ); + }, + [setApplicationJSON] + ); + useEffect(() => { const persistenceManager = getPersistenceManager(); @@ -89,67 +130,111 @@ export const LayoutManagementProvider = ( setLayoutMetadata(metadata); }) .catch((error: Error) => { - //TODO: Show error toaster + notify({ + type: NotificationLevel.Error, + header: "Failed to Load Layouts", + body: "Could not load list of available layouts", + }); console.error("Error occurred while retrieving metadata", error); }); persistenceManager - .loadApplicationLayout() - .then((layout: LayoutJSON) => { - setApplicationLayout(layout); + .loadApplicationJSON() + .then((applicationJSON: ApplicationJSON) => { + setApplicationJSON(applicationJSON); }) .catch((error: Error) => { - //TODO: Show error toaster + notify({ + type: NotificationLevel.Error, + header: "Failed to Load Layout", + body: "Could not load your latest view", + }); console.error( "Error occurred while retrieving application layout", error ); }); - }, [setApplicationLayout]); + }, [notify, setApplicationJSON]); const saveApplicationLayout = useCallback( (layout: LayoutJSON) => { setApplicationLayout(layout, false); - getPersistenceManager().saveApplicationLayout(layout); + getPersistenceManager().saveApplicationJSON(applicationJSONRef.current); }, [setApplicationLayout] ); - const saveLayout = useCallback((metadata: LayoutMetadataDto) => { - const layoutToSave = resolveJSONPath( - applicationLayoutRef.current, - "#main-tabs.ACTIVE_CHILD" - ); + const saveLayout = useCallback( + (metadata: LayoutMetadataDto) => { + const layoutToSave = resolveJSONPath( + applicationJSONRef.current.layout, + "#main-tabs.ACTIVE_CHILD" + ); - if (layoutToSave) { - getPersistenceManager() - .createLayout(metadata, ensureLayoutHasTitle(layoutToSave, metadata)) - .then((metadata) => { - //TODO: Show success toast - setLayoutMetadata((prev) => [...prev, metadata]); - }) - .catch((error: Error) => { - //TODO: Show error toaster - console.error("Error occurred while saving layout", error); + if (layoutToSave) { + getPersistenceManager() + .createLayout(metadata, ensureLayoutHasTitle(layoutToSave, metadata)) + .then((metadata) => { + console.log("NOTIFY"); + notify({ + type: NotificationLevel.Success, + header: "Layout Saved Successfully", + body: `${metadata.name} saved successfully`, + }); + setLayoutMetadata((prev) => [...prev, metadata]); + }) + .catch((error: Error) => { + notify({ + type: NotificationLevel.Error, + header: "Failed to Save Layout", + body: `Failed to save layout ${metadata.name}`, + }); + console.error("Error occurred while saving layout", error); + }); + } else { + notify({ + type: NotificationLevel.Error, + header: "Failed to Save Layout", + body: "Cannot save undefined layout", }); - } - //TODO else{ show error message} - }, []); + } + }, + [notify] + ); + + const saveApplicationSettings = useCallback( + (settings: ApplicationSettings) => { + setApplicationSettings(settings); + getPersistenceManager().saveApplicationJSON(applicationJSONRef.current); + }, + [setApplicationSettings] + ); const loadLayoutById = useCallback( (id: string) => { getPersistenceManager() .loadLayout(id) .then((layoutJson) => { - const { current: prev } = applicationLayoutRef; + const { layout: currentLayout } = applicationJSONRef.current; setApplicationLayout({ - ...prev, - active: prev.children?.length ?? 0, - children: [...(prev.children || []), layoutJson], + ...currentLayout, + children: (currentLayout.children || []).concat(layoutJson), + props: { + ...currentLayout.props, + active: currentLayout.children?.length ?? 0, + }, + }); + }) + .catch((error: Error) => { + notify({ + type: NotificationLevel.Error, + header: "Failed to Load Layout", + body: "Failed to load the requested layout", }); + console.error("Error occurred while loading layout", error); }); }, - [setApplicationLayout] + [notify, setApplicationLayout] ); return ( @@ -157,8 +242,9 @@ export const LayoutManagementProvider = ( value={{ layoutMetadata, saveLayout, - applicationLayout: applicationLayoutRef.current, + applicationJson: applicationJSONRef.current, saveApplicationLayout, + saveApplicationSettings, loadLayoutById, }} > diff --git a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css index e05b72301..cf8fb562c 100644 --- a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css +++ b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.css @@ -9,8 +9,8 @@ --vuuTabstrip-fontWeight: 700; --vuuTabstrip-width: 100%; --svg-demo: url('data:image/svg+xml;utf8,'); + --svg-features: url('data:image/svg+xml;utf8,'); --svg-tables: url('data:image/svg+xml;utf8,'); - --svg-templates: url('data:image/svg+xml;utf8,'); --svg-layouts: url('data:image/svg+xml;utf8,'); --vuu-light-text-primary: #15171b; @@ -18,12 +18,14 @@ box-shadow: 3px 4px 4px 0px rgba(0, 0, 0, 0.15); display: flex; + height: calc(100% - 4px); margin-bottom: 4px; overflow: hidden; position: relative; transition: width .2s ease-out; z-index: 0; - width: calc(var(--menu-width) + var(--menu-level-2-width)); + /* width: calc(var(--menu-width) + var(--menu-level-2-width)); */ + /* width: 100%; */ } @@ -47,7 +49,7 @@ .vuuLeftNav-menu-icons-content .vuuLeftNav-menu-secondary, .vuuLeftNav-menu-full-content .vuuLeftNav-menu-secondary { - display: block; + display: flex; } .vuuLeftNav-menu-primary { @@ -65,7 +67,6 @@ .vuuLeftNav-menu-secondary { flex: 1 1 auto; - height: 100%; display: none; /* position: absolute; */ top:0; @@ -118,8 +119,8 @@ --vuu-icon-svg: var(--svg-tables); } -.vuuLeftNav [data-icon='templates'] { - --vuu-icon-svg: var(--svg-templates); +.vuuLeftNav [data-icon='features'] { + --vuu-icon-svg: var(--svg-features); } .vuuLeftNav [data-icon='layouts'] { diff --git a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx index 9e80f8060..d6ca3515b 100644 --- a/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx +++ b/vuu-ui/packages/vuu-shell/src/left-nav/LeftNav.tsx @@ -1,5 +1,5 @@ import { VuuLogo } from "@finos/vuu-icons"; -import { Action, Stack, useLayoutProviderDispatch } from "@finos/vuu-layout"; +import { Stack, useLayoutProviderDispatch } from "@finos/vuu-layout"; import { LayoutResizeAction } from "@finos/vuu-layout/src/layout-reducer"; import { Tab, Tabstrip } from "@finos/vuu-ui-controls"; import cx from "classnames"; @@ -19,138 +19,109 @@ export type NavDisplayStatus = | "menu-full-content" | "menu-icons-content"; +const getDisplayStatus = ( + activeTabIndex: number, + expanded: boolean +): NavDisplayStatus => { + if (activeTabIndex === 0) { + return expanded ? "menu-full" : "menu-icons"; + } else { + return expanded ? "menu-full-content" : "menu-icons-content"; + } +}; + export type NavDisplayStatusHandler = ( navDisplayStatus: NavDisplayStatus ) => void; -interface LeftNavProps extends HTMLAttributes { +export interface LeftNavProps extends HTMLAttributes { "data-path"?: string; defaultActiveTabIndex?: number; - defaultDisplayStatus?: NavDisplayStatus; + defaultExpanded?: boolean; features: FeatureProps[]; - tableFeatures: FeatureProps[]; - onChangeDisplayStatus?: NavDisplayStatusHandler; - onResize?: (size: number) => void; + onActiveChange?: (activeTabIndex: number) => void; + onTogglePrimaryMenu?: (expanded: boolean) => void; sizeCollapsed?: number; sizeContent?: number; sizeExpanded?: number; + tableFeatures: FeatureProps[]; } type NavState = { activeTabIndex: number; - navStatus: NavDisplayStatus; + expanded: boolean; }; -export const LeftNav = ({ - "data-path": path, - defaultDisplayStatus = "menu-full", - defaultActiveTabIndex = 0, - features, - onChangeDisplayStatus, - onResize, - sizeCollapsed = 80, - sizeContent = 300, - sizeExpanded = 240, - style: styleProp, - tableFeatures, - ...htmlAttributes -}: LeftNavProps) => { +export const LeftNav = (props: LeftNavProps) => { const dispatch = useLayoutProviderDispatch(); + const [themeClass] = useThemeAttributes(); + const { + "data-path": path, + defaultExpanded = true, + defaultActiveTabIndex = 0, + features, + onActiveChange, + onTogglePrimaryMenu, + sizeCollapsed = 80, + sizeContent = 300, + sizeExpanded = 240, + style: styleProp, + tableFeatures, + ...htmlAttributes + } = props; + const [navState, setNavState] = useState({ activeTabIndex: defaultActiveTabIndex, - navStatus: defaultDisplayStatus, + expanded: defaultExpanded, }); - const [themeClass] = useThemeAttributes(); - - const toggleNavWidth = useCallback( - (navStatus: NavDisplayStatus) => { - switch (navStatus) { - case "menu-icons": - return sizeExpanded; - case "menu-full": - return sizeCollapsed; - case "menu-full-content": - return sizeCollapsed + sizeContent; - case "menu-icons-content": - return sizeExpanded + sizeContent; - } - }, - [sizeCollapsed, sizeContent, sizeExpanded] - ); - const toggleNavStatus = (navStatus: NavDisplayStatus) => { - switch (navStatus) { - case "menu-icons": - return "menu-full"; - case "menu-full": - return "menu-icons"; - case "menu-full-content": - return "menu-icons-content"; - case "menu-icons-content": - return "menu-full-content"; - } - }; - - const getWidthAndStatus = useCallback( - ( - navState: NavDisplayStatus, - tabIndex: number - ): [number, NavDisplayStatus] => { + const getFullWidth = useCallback( + (tabIndex: number, expanded: boolean): number => { if (tabIndex === 0) { - const newNavState = - navState === "menu-full-content" - ? "menu-full" - : navState === "menu-icons-content" - ? "menu-icons" - : navState; - - return newNavState === "menu-icons" - ? [sizeCollapsed, newNavState] - : [sizeExpanded, newNavState]; + return expanded ? sizeExpanded : sizeCollapsed; } else { - const newNavState = - navState === "menu-full" - ? "menu-full-content" - : navState === "menu-icons" - ? "menu-icons-content" - : navState; - return newNavState === "menu-icons-content" - ? [sizeCollapsed + sizeContent, newNavState] - : [sizeExpanded + sizeContent, newNavState]; + return expanded + ? sizeExpanded + sizeContent + : sizeCollapsed + sizeContent; } }, [sizeCollapsed, sizeContent, sizeExpanded] ); const handleTabSelection = useCallback( - (value: number) => { - const [width, navStatus] = getWidthAndStatus(navState.navStatus, value); - setNavState({ - activeTabIndex: value, - navStatus, - }); - dispatch({ - type: Action.LAYOUT_RESIZE, - path, - size: width, - } as LayoutResizeAction); + (activeTabIndex: number) => { + const { activeTabIndex: currentIndex, expanded } = navState; + const newState = { activeTabIndex, expanded }; + setNavState(newState); + if (activeTabIndex === 0 || currentIndex === 0) { + const width = getFullWidth(activeTabIndex, expanded); + dispatch({ + type: "layout-resize", + path: "#vuu-side-panel", + size: width, + } as LayoutResizeAction); + } + onActiveChange?.(activeTabIndex); }, - [dispatch, getWidthAndStatus, navState, path] + [dispatch, getFullWidth, navState, onActiveChange] + ); + + const displayStatus = getDisplayStatus( + navState.activeTabIndex, + navState.expanded ); - const toggleSize = useCallback(() => { - const { activeTabIndex, navStatus: currentNavStatus } = navState; - const newNavStatus = toggleNavStatus(currentNavStatus); - setNavState({ - activeTabIndex, - navStatus: newNavStatus, - }); + const toggleExpanded = useCallback(() => { + const { activeTabIndex, expanded } = navState; + const primaryMenuExpanded = !expanded; + const newState = { activeTabIndex, expanded: primaryMenuExpanded }; + setNavState(newState); dispatch({ - type: Action.LAYOUT_RESIZE, - path, - size: toggleNavWidth(currentNavStatus), + type: "layout-resize", + path: "#vuu-side-panel", + size: getFullWidth(activeTabIndex, primaryMenuExpanded), } as LayoutResizeAction); - onChangeDisplayStatus?.(newNavStatus); - }, [dispatch, navState, onChangeDisplayStatus, path, toggleNavWidth]); + onTogglePrimaryMenu?.(primaryMenuExpanded); + }, [dispatch, getFullWidth, navState, onTogglePrimaryMenu]); const style = { ...styleProp, @@ -162,7 +133,7 @@ export const LeftNav = ({ return (
-
@@ -191,16 +161,16 @@ export const LeftNav = ({
@@ -211,9 +181,6 @@ export const LeftNav = ({ > -
- LAYOUT TEMPLATES -
diff --git a/vuu-ui/packages/vuu-shell/src/shell-layouts/index.ts b/vuu-ui/packages/vuu-shell/src/shell-layouts/index.ts index 21a7b3147..9a8e5a949 100644 --- a/vuu-ui/packages/vuu-shell/src/shell-layouts/index.ts +++ b/vuu-ui/packages/vuu-shell/src/shell-layouts/index.ts @@ -1,2 +1,3 @@ export * from "./context-panel"; export * from "./useShellLayout"; +export * from "./side-panel"; diff --git a/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.css b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.css new file mode 100644 index 000000000..0907859cf --- /dev/null +++ b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.css @@ -0,0 +1,4 @@ +.vuuShellSidePanel { + transition: width .2s ease-out; + width: var(--shell-left-nav-size); +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.tsx b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.tsx new file mode 100644 index 000000000..4bd61099c --- /dev/null +++ b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/SidePanel.tsx @@ -0,0 +1,43 @@ +import { CSSProperties, HTMLAttributes, useMemo } from "react"; +import cx from "classnames"; + +import "./SidePanel.css"; +// import { useLayoutManager } from "../../layout-management"; + +const classBase = "vuuShellSidePanel"; + +export interface SidePanelProps extends HTMLAttributes { + open?: boolean; + path?: string; + sizeOpen?: number; + sizeClosed?: number; +} + +export const SidePanel = ({ + children, + open = true, + sizeClosed = 90, + sizeOpen = 200, + style: styleProp, + ...htmlAttributes +}: SidePanelProps) => { + // const { applicationJson, saveApplicationSettings } = useLayoutManager(); + // console.log(`settings`, { + // expanded: applicationJson?.settings?.leftNav?.expanded, + // active: applicationJson?.settings?.leftNav?.activeTabIndex, + // }); + + const style = useMemo( + () => + ({ + ...styleProp, + "--shell-left-nav-size": open ? `${sizeOpen}px` : `${sizeClosed}px`, + } as CSSProperties), + [open, sizeClosed, sizeOpen, styleProp] + ); + return ( +
+ {children} +
+ ); +}; diff --git a/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/index.ts b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/index.ts new file mode 100644 index 000000000..01efecde3 --- /dev/null +++ b/vuu-ui/packages/vuu-shell/src/shell-layouts/side-panel/index.ts @@ -0,0 +1 @@ +export * from "./SidePanel"; diff --git a/vuu-ui/packages/vuu-shell/src/shell-layouts/useFullHeightLeftPanel.tsx b/vuu-ui/packages/vuu-shell/src/shell-layouts/useFullHeightLeftPanel.tsx index 541e31d1c..2dcc3147d 100644 --- a/vuu-ui/packages/vuu-shell/src/shell-layouts/useFullHeightLeftPanel.tsx +++ b/vuu-ui/packages/vuu-shell/src/shell-layouts/useFullHeightLeftPanel.tsx @@ -1,11 +1,12 @@ import { DraggableLayout, Flexbox } from "@finos/vuu-layout"; import { ReactElement } from "react"; import { ContextPanel } from "./context-panel"; +import { SidePanel } from "./side-panel"; import { ShellLayoutProps } from "./useShellLayout"; export const useFullHeightLeftPanel = ({ appHeader, - leftSidePanel, + LeftSidePanelProps, }: ShellLayoutProps): ReactElement => { return ( - {leftSidePanel} + { const paletteView = useRef(null); const [open, setOpen] = useState(true); @@ -64,7 +64,7 @@ export const useInlayLeftPanel = ({ > {appHeader} - {getDrawers(leftSidePanel).concat( + {getDrawers(LeftSidePanelProps?.children).concat( { LayoutProps?: Pick< LayoutProviderProps, "createNewChild" | "pathToDropTarget" >; + LeftSidePanelProps?: SidePanelProps; children?: ReactNode; - leftSidePanel?: ReactElement; leftSidePanelLayout?: "full-height" | "inlay"; loginUrl?: string; // paletteConfig: any; @@ -48,9 +56,9 @@ export interface ShellProps extends HTMLAttributes { export const Shell = ({ LayoutProps, + LeftSidePanelProps = defaultLeftSidePanel, children, className: classNameProp, - leftSidePanel, leftSidePanelLayout, loginUrl, saveLocation = "remote", @@ -60,15 +68,17 @@ export const Shell = ({ ...htmlAttributes }: ShellProps) => { const rootRef = useRef(null); + const { dialog, setDialogState } = useDialog(); const layoutId = useRef("latest"); - const { applicationLayout, saveApplicationLayout, loadLayoutById } = + const { applicationJson, saveApplicationLayout, loadLayoutById } = useLayoutManager(); + const { buildMenuOptions, handleMenuAction } = + useLayoutContextMenuItems(setDialogState); const handleLayoutChange = useCallback( (layout, layoutChangeReason) => { try { saveApplicationLayout(layout); - // saveLayoutConfig(layout); } catch { error?.("Failed to save layout"); } @@ -82,6 +92,7 @@ export const Shell = ({ } }, []); + // TODO this is out of date const handleNavigate = useCallback( (id) => { layoutId.current = id; @@ -103,7 +114,10 @@ export const Shell = ({ const [themeClass, densityClass, dataMode] = useThemeAttributes(); const className = cx("vuuShell", classNameProp, themeClass, densityClass); + const isLoading = applicationJson === loadingApplicationJson; + const shellLayout = useShellLayout({ + LeftSidePanelProps, leftSidePanelLayout, appHeader: ( ), - leftSidePanel, }); - return ( + return isLoading ? null : ( - - - {shellLayout} - - - {children} + + {shellLayout} + + + {children || dialog} + ); }; diff --git a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/useDirection.ts b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/useDirection.ts index b604a67d9..1b3b71d0b 100644 --- a/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/useDirection.ts +++ b/vuu-ui/packages/vuu-table-extras/src/cell-renderers-next/background-cell/useDirection.ts @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { getMovingValueDirection, isTypeDescriptor, @@ -9,12 +9,12 @@ import { useEffect, useRef } from "react"; const INITIAL_VALUE = [undefined, undefined, undefined, undefined]; -type State = [string, unknown, KeyedColumnDescriptor, valueChangeDirection]; +type State = [string, unknown, RuntimeColumnDescriptor, valueChangeDirection]; export function useDirection( key: string, value: unknown, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { const ref = useRef(); const [prevKey, prevValue, prevColumn, prevDirection] = diff --git a/vuu-ui/packages/vuu-table/src/index.ts b/vuu-ui/packages/vuu-table/src/index.ts index 8c80575ce..8ba981799 100644 --- a/vuu-ui/packages/vuu-table/src/index.ts +++ b/vuu-ui/packages/vuu-table/src/index.ts @@ -1,8 +1,2 @@ -export * from "./table"; -export { - GroupHeaderCellNext, - TableNext, - useControlledTableNavigation, -} from "./table-next"; -export type { RowProps } from "./table-next"; +export * from "./table-next"; export { updateTableConfig } from "./table-next/table-config"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/Row.tsx b/vuu-ui/packages/vuu-table/src/table-next/Row.tsx index 2e713c02d..7521ab35f 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/Row.tsx @@ -1,7 +1,7 @@ import { DataSourceRow } from "@finos/vuu-data-types"; import { DataCellEditHandler, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, RowClickHandler, } from "@finos/vuu-datagrid-types"; import { @@ -22,13 +22,13 @@ import "./Row.css"; export interface RowProps { className?: string; columnMap: ColumnMap; - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; highlighted?: boolean; row: DataSourceRow; offset: number; onClick?: RowClickHandler; onDataEdited?: DataCellEditHandler; - onToggleGroup?: (row: DataSourceRow, column: KeyedColumnDescriptor) => void; + onToggleGroup?: (row: DataSourceRow, column: RuntimeColumnDescriptor) => void; style?: CSSProperties; zebraStripes?: boolean; } @@ -80,7 +80,7 @@ export const Row = memo( const style = { transform: `translate3d(0px, ${offset}px, 0px)` }; const handleGroupCellClick = useCallback( - (evt: MouseEvent, column: KeyedColumnDescriptor) => { + (evt: MouseEvent, column: RuntimeColumnDescriptor) => { if (isGroupColumn(column) || isJsonGroup(column, row)) { evt.stopPropagation(); onToggleGroup?.(row, column); diff --git a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx index 1a1106f8a..552e7eea7 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx @@ -1,15 +1,31 @@ -import { MeasuredContainer, useId } from "@finos/vuu-layout"; +import { + DataSource, + SchemaColumn, + VuuFeatureInvocationMessage, +} from "@finos/vuu-data"; +import { + SelectionChangeHandler, + TableConfig, + TableRowClickHandler, + TableSelectionModel, +} from "@finos/vuu-datagrid-types"; +import { DataSourceRow } from "@finos/vuu-data-types"; +import { + MeasuredContainer, + MeasuredContainerProps, + useId, +} from "@finos/vuu-layout"; import { ContextMenuProvider } from "@finos/vuu-popups"; -import { TableProps } from "@finos/vuu-table"; +import { DragStartHandler, dragStrategy } from "@finos/vuu-ui-controls"; import { isGroupColumn, metadataKeys, notHidden } from "@finos/vuu-utils"; import { useForkRef } from "@salt-ds/core"; import cx from "classnames"; -import { CSSProperties, ForwardedRef, forwardRef, useRef } from "react"; +import { CSSProperties, FC, ForwardedRef, forwardRef, useRef } from "react"; import { GroupHeaderCellNext as GroupHeaderCell, HeaderCell, } from "./header-cell"; -import { Row as DefaultRow } from "./Row"; +import { Row as DefaultRow, RowProps } from "./Row"; import { useTable } from "./useTableNext"; import "./TableNext.css"; @@ -18,6 +34,75 @@ const classBase = "vuuTableNext"; const { IDX, RENDER_IDX } = metadataKeys; +// TODO implement a Model object to represent a row data for better API +export type TableRowSelectHandler = (row: DataSourceRow) => void; + +export type TableNavigationStyle = "none" | "cell" | "row"; + +export interface TableProps + extends Omit { + Row?: FC; + allowConfigEditing?: boolean; + allowDragDrop?: boolean | dragStrategy; + /** + * required if a fully featured column picker is to be available + */ + availableColumns?: SchemaColumn[]; + config: TableConfig; + dataSource: DataSource; + disableFocus?: boolean; + headerHeight?: number; + /** + * Defined how focus navigation within data cells will be handled by table. + * Default is cell. + */ + highlightedIndex?: number; + navigationStyle?: TableNavigationStyle; + /** + * required if a fully featured column picker is to be available. + * Available columns can be changed by the addition or removal of + * one or more calculated columns. + */ + onAvailableColumnsChange?: (columns: SchemaColumn[]) => void; + /** + * This callback will be invoked any time a config attribute of TableConfig + * is changed. By persisting this value and providing it to the Table as a + * prop, table state can be persisted across sessions. + */ + onConfigChange?: (config: TableConfig) => void; + onDragStart?: DragStartHandler; + onDrop?: () => void; + /** + * When a Vuu feature e.g. context menu action, has been invoked, the Vuu server + * response must be handled. This callback provides that response. + */ + onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; + + onHighlight?: (idx: number) => void; + /** + * callback invoked when user 'clicks' a table row. CLick triggered either + * via mouse click or keyboard (default ENTER); + */ + onRowClick?: TableRowClickHandler; + onShowConfigEditor?: () => void; + onSelect?: TableRowSelectHandler; + onSelectionChange?: SelectionChangeHandler; + renderBufferSize?: number; + rowHeight?: number; + /** + * Selection Bookends style the left and right edge of a selection block. + * They are optional, value defaults to zero. + * TODO this should just live in CSS + */ + selectionBookendWidth?: number; + selectionModel?: TableSelectionModel; + /** + * if false, table rendered without headers. Useful when table is being included in a + * composite component. + */ + showColumnHeaders?: boolean; +} + export const TableNext = forwardRef(function TableNext( { Row = DefaultRow, diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.tsx index 6300731f3..21d8ecbd8 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/ColumnHeaderPill.tsx @@ -1,13 +1,13 @@ import cx from "classnames"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { HTMLAttributes, MouseEvent, useCallback } from "react"; import "./ColumnHeaderPill.css"; export interface ColumnHeaderPillProps extends HTMLAttributes { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; removable?: boolean; - onRemove?: (column: KeyedColumnDescriptor) => void; + onRemove?: (column: RuntimeColumnDescriptor) => void; } const classBase = "vuuColumnHeaderPill"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/GroupColumnPill.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/GroupColumnPill.tsx index 8629eb857..0d632b638 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/GroupColumnPill.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/GroupColumnPill.tsx @@ -1,10 +1,10 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { ColumnHeaderPill, ColumnHeaderPillProps } from "./ColumnHeaderPill"; import "./GroupColumnPill.css"; export interface GroupColumnPillProps extends ColumnHeaderPillProps { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; } export const GroupColumnPill = ({ diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/SortIndicator.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/SortIndicator.tsx index 54c257225..6303ff953 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/SortIndicator.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-header-pill/SortIndicator.tsx @@ -1,10 +1,10 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { ColumnHeaderPill } from "./ColumnHeaderPill"; import "./SortIndicator.css"; export interface SortIndicatorProps { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; } export const SortIndicator = ({ column }: SortIndicatorProps) => { diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx index dfc0e6cf6..e63aba105 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-menu/ColumnMenu.tsx @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { useContextMenu } from "@finos/vuu-popups"; import cx from "classnames"; import { @@ -12,7 +12,7 @@ import { import "./ColumnMenu.css"; export interface ColumnMenuProps extends HTMLAttributes { - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; } const getPosition = (element: HTMLElement | null) => { diff --git a/vuu-ui/packages/vuu-table/src/table-next/column-resizing/useTableColumnResize.tsx b/vuu-ui/packages/vuu-table/src/table-next/column-resizing/useTableColumnResize.tsx index 5f7f16c46..50b8c73d5 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/column-resizing/useTableColumnResize.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/column-resizing/useTableColumnResize.tsx @@ -1,4 +1,4 @@ -import { Heading, KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { Heading, RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { RefObject, useCallback, useRef, useState } from "react"; import { ResizePhase } from "../useTableModel"; @@ -10,7 +10,7 @@ export type TableColumnResizeHandler = ( export type ResizeHandler = (evt: MouseEvent, moveBy: number) => void; export interface CellResizeHookProps { - column: KeyedColumnDescriptor | Heading; + column: RuntimeColumnDescriptor | Heading; onResize?: (phase: ResizePhase, columnName: string, width?: number) => void; rootRef: RefObject; } diff --git a/vuu-ui/packages/vuu-table/src/table-next/context-menu/buildContextMenuDescriptors.ts b/vuu-ui/packages/vuu-table/src/table-next/context-menu/buildContextMenuDescriptors.ts index b992a3225..8dbeb0913 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/context-menu/buildContextMenuDescriptors.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/context-menu/buildContextMenuDescriptors.ts @@ -1,12 +1,15 @@ import { DataSource } from "@finos/vuu-data"; import { ContextMenuItemDescriptor, MenuBuilder } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor, PinLocation } from "@finos/vuu-datagrid-types"; +import { + RuntimeColumnDescriptor, + PinLocation, +} from "@finos/vuu-datagrid-types"; import { Filter } from "@finos/vuu-filter-types"; import { isNumericColumn } from "@finos/vuu-utils"; export type ContextMenuLocation = "header" | "filter" | "grid"; -type MaybeColumn = { column?: KeyedColumnDescriptor }; +type MaybeColumn = { column?: RuntimeColumnDescriptor }; type MaybeFilter = { filter?: Filter }; export const buildContextMenuDescriptors = diff --git a/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts index 70b84b051..d9a7070ff 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/context-menu/useHandleTableContextMenu.ts @@ -1,6 +1,6 @@ /* eslint-disable no-sequences */ import { DataSource } from "@finos/vuu-data"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { Filter } from "@finos/vuu-filter-types"; import { removeColumnFromFilter } from "@finos/vuu-utils"; import { VuuFilter } from "@finos/vuu-protocol-types"; @@ -15,7 +15,7 @@ import { } from "@finos/vuu-utils"; export interface ContextMenuOptions { - column?: KeyedColumnDescriptor; + column?: RuntimeColumnDescriptor; filter?: Filter; sort?: VuuFilter; } @@ -31,7 +31,7 @@ export interface ContextMenuHookProps { const removeFilterColumn = ( dataSourceFilter: DataSourceFilter, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) => { if (dataSourceFilter.filterStruct && column) { const [filterStruct, filter] = removeColumnFromFilter( diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCell.tsx index f740e1784..b02bda7b6 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCell.tsx @@ -1,6 +1,6 @@ import { GroupColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import cx from "classnames"; import { useRef } from "react"; @@ -16,7 +16,7 @@ const classBase = "vuuTableNextGroupHeaderCell"; export interface GroupHeaderCellProps extends Omit { column: GroupColumnDescriptor; - onRemoveColumn: (column: KeyedColumnDescriptor) => void; + onRemoveColumn: (column: RuntimeColumnDescriptor) => void; } export const GroupHeaderCell = ({ diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx index 9be31ded7..d7d3f77ce 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx @@ -1,6 +1,6 @@ import { GroupColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import cx from "classnames"; import { useCallback, useRef, useState } from "react"; @@ -15,8 +15,8 @@ import "./GroupHeaderCell.css"; const classBase = "vuuTableNextGroupHeaderCell"; const switchIfChanged = ( - columns: KeyedColumnDescriptor[], - newColumns: KeyedColumnDescriptor[] + columns: RuntimeColumnDescriptor[], + newColumns: RuntimeColumnDescriptor[] ) => { if (columns === newColumns) { return columns; @@ -28,7 +28,7 @@ const switchIfChanged = ( export interface GroupHeaderCellNextProps extends Omit { column: GroupColumnDescriptor; - onRemoveColumn: (column: KeyedColumnDescriptor) => void; + onRemoveColumn: (column: RuntimeColumnDescriptor) => void; } export const GroupHeaderCellNext = ({ diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.tsx b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.tsx index 095c28ae0..6cae58011 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/HeaderCell.tsx @@ -1,4 +1,4 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { HTMLAttributes, MouseEvent, useCallback, useRef } from "react"; import { useCell } from "../useCell"; import { ColumnMenu } from "../column-menu"; @@ -16,7 +16,7 @@ const classBase = "vuuTableNextHeaderCell"; export interface HeaderCellProps extends HTMLAttributes { classBase?: string; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; onResize?: TableColumnResizeHandler; } @@ -27,6 +27,7 @@ export const HeaderCell = ({ onResize, ...htmlAttributes }: HeaderCellProps) => { + const { HeaderCellContentRenderer, HeaderCellLabelRenderer } = column; const rootRef = useRef(null); const { isResizing, ...resizeProps } = useTableColumnResize({ column, @@ -45,14 +46,21 @@ export const HeaderCell = ({ const { className, style } = useCell(column, classBase, true); const columnMenu = ; - const columnLabel = ( + const columnLabel = HeaderCellLabelRenderer ? ( + + ) : (
{column.label ?? column.name}
); + + const columnContent = HeaderCellContentRenderer + ? [] + : []; + const sortIndicator = ; const headerItems = column.align === "right" - ? [sortIndicator, columnLabel, columnMenu] - : [columnMenu, columnLabel, sortIndicator]; + ? [sortIndicator, columnLabel].concat(columnContent).concat(columnMenu) + : [columnMenu, columnLabel, sortIndicator].concat(columnContent); return (
diff --git a/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts b/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts index bb24595a3..ce66dbcc2 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useCellEditing.ts @@ -2,6 +2,7 @@ import { isCharacterKey } from "@finos/vuu-utils"; import { FocusEventHandler, KeyboardEvent as ReactKeyboardEvent, + MouseEvent, useCallback, } from "react"; import { cellIsTextInput } from "./table-dom-utils"; @@ -15,23 +16,32 @@ export const useCellEditing = ({ navigate }: CellEditingHookProps) => { navigate(); }, [navigate]); - const editInput = useCallback((evt: ReactKeyboardEvent) => { - const cellEl = evt.target as HTMLDivElement; - const input = cellEl.querySelector("input"); - if (input) { - input.focus(); - input.select(); - } - }, []); + const editInput = useCallback( + (evt: MouseEvent | ReactKeyboardEvent) => { + const cellEl = evt.target as HTMLDivElement; + const input = cellEl.matches("input") + ? (cellEl as HTMLInputElement) + : cellEl.querySelector("input"); - const focusInput = useCallback((evt: ReactKeyboardEvent) => { - const cellEl = evt.target as HTMLDivElement; - const input = cellEl.querySelector("input"); - if (input) { - input.focus(); - input.select(); - } - }, []); + if (input) { + input.focus(); + input.select(); + } + }, + [] + ); + + const focusInput = useCallback( + (evt: MouseEvent | ReactKeyboardEvent) => { + const cellEl = evt.target as HTMLDivElement; + const input = cellEl.querySelector("input"); + if (input) { + input.focus(); + input.select(); + } + }, + [] + ); const handleKeyDown = useCallback( (e: ReactKeyboardEvent) => { @@ -47,6 +57,17 @@ export const useCellEditing = ({ navigate }: CellEditingHookProps) => { [editInput, focusInput] ); + const handleDoubleClick = useCallback( + (e: MouseEvent) => { + const el = e.target as HTMLElement; + if (el.matches("input") || el.querySelector("input")) { + editInput(e); + e.stopPropagation(); + } + }, + [editInput] + ); + const handleBlur = useCallback( (e) => { const el = e.target as HTMLElement; @@ -65,6 +86,7 @@ export const useCellEditing = ({ navigate }: CellEditingHookProps) => { return { onBlur: handleBlur, + onDoubleClick: handleDoubleClick, onFocus: handleFocus, onKeyDown: handleKeyDown, }; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts index bb4e6e48e..3cf85b56f 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts @@ -240,6 +240,7 @@ NavigationHookProps) => { if (direction && distance) { requestScroll?.({ type: "scroll-distance", distance, direction }); } + console.log(`activeCell focus`); activeCell.focus({ preventScroll: true }); } } @@ -351,7 +352,6 @@ NavigationHookProps) => { const moveHighlightedRow = useCallback( async (key: NavigationKey) => { - console.log(`moveHighlightedRow`); const { current: highlighted } = highlightedIndexRef; const [nextRowIdx] = isPagingKey(key) ? await nextPageItemIdx(key, [highlighted ?? -1, 0]) @@ -436,7 +436,7 @@ NavigationHookProps) => { focusableCell.current = cell; } } - }, [containerRef, fullyRendered]); + }, [containerRef, disableFocus, fullyRendered]); return { highlightedIndexRef, diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts index 945e98938..22293f38a 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableContextMenu.ts @@ -1,12 +1,12 @@ import { DataSource } from "@finos/vuu-data"; import { DataSourceRow } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { useContextMenu as usePopupContextMenu } from "@finos/vuu-popups"; import { buildColumnMap } from "@finos/vuu-utils"; import { MouseEvent, useCallback } from "react"; export interface TableContextMenuHookProps { - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; data: DataSourceRow[]; dataSource: DataSource; getSelectedRows: () => DataSourceRow[]; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts index 4d843304f..0c810acf2 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableModel.ts @@ -1,6 +1,6 @@ import { ColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, PinLocation, TableAttributes, TableConfig, @@ -60,7 +60,7 @@ const getDataType = ( * data-related config from DataSource. */ export interface TableModel extends TableAttributes { - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; headings: TableHeadings; } @@ -89,16 +89,16 @@ export interface ColumnActionInit { export interface ColumnActionHide { type: "hideColumns"; - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; } export interface ColumnActionShow { type: "showColumns"; - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; } export interface ColumnActionMove { type: "moveColumn"; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; moveBy?: 1 | -1; } @@ -112,7 +112,7 @@ export type ResizePhase = "begin" | "resize" | "end"; export interface ColumnActionResize { type: "resizeColumn"; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; phase: ResizePhase; width?: number; } @@ -129,10 +129,10 @@ export interface ColumnActionUpdate { export interface ColumnActionUpdateProp { align?: ColumnDescriptor["align"]; - column: KeyedColumnDescriptor; + column: RuntimeColumnDescriptor; hidden?: ColumnDescriptor["hidden"]; label?: ColumnDescriptor["label"]; - resizing?: KeyedColumnDescriptor["resizing"]; + resizing?: RuntimeColumnDescriptor["resizing"]; type: "updateColumnProp"; width?: ColumnDescriptor["width"]; } @@ -243,7 +243,9 @@ function init({ dataSource, tableConfig }: InitialConfig): InternalTableModel { const { config: dataSourceConfig, tableSchema } = dataSource; const keyedColumns = columns .filter(subscribedOnly(dataSourceConfig?.columns)) - .map(columnDescriptorToKeyedColumDescriptor(tableAttributes, tableSchema)); + .map( + columnDescriptorToInternalColumDescriptor(tableAttributes, tableSchema) + ); const maybePinnedColumns = keyedColumns.some(isPinned) ? sortPinnedColumns(keyedColumns) @@ -276,12 +278,12 @@ const getLabel = ( return label; }; -const columnDescriptorToKeyedColumDescriptor = +const columnDescriptorToInternalColumDescriptor = (tableAttributes: TableAttributes, tableSchema?: TableSchema) => ( column: ColumnDescriptor & { key?: number }, index: number - ): KeyedColumnDescriptor => { + ): RuntimeColumnDescriptor => { const { columnDefaultWidth = DEFAULT_COLUMN_WIDTH, columnFormatHeader } = tableAttributes; const serverDataType = getDataType(column, tableSchema); @@ -298,6 +300,8 @@ const columnDescriptorToKeyedColumDescriptor = ...rest, align, CellRenderer: getCellRenderer(column), + HeaderCellLabelRenderer: getCellRenderer(column, "col-label"), + HeaderCellContentRenderer: getCellRenderer(column, "col-content"), clientSideEditValidationCheck: hasValidationRules(column.type) ? buildValidationChecker(column.type.renderer.rules) : undefined, @@ -313,7 +317,10 @@ const columnDescriptorToKeyedColumDescriptor = if (isGroupColumn(keyedColumnWithDefaults)) { keyedColumnWithDefaults.columns = keyedColumnWithDefaults.columns.map( (col) => - columnDescriptorToKeyedColumDescriptor(tableAttributes)(col, col.key) + columnDescriptorToInternalColumDescriptor(tableAttributes)( + col, + col.key + ) ); } diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts index 7722774e3..b6a6a8693 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts @@ -7,13 +7,17 @@ import { DataSourceRow } from "@finos/vuu-data-types"; import { ColumnDescriptor, DataCellEditHandler, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, RowClickHandler, SelectionChangeHandler, TableConfig, TableSelectionModel, } from "@finos/vuu-datagrid-types"; -import { MeasuredSize, useLayoutEffectSkipFirst } from "@finos/vuu-layout"; +import { + MeasuredSize, + useLayoutEffectSkipFirst, + MeasuredProps, +} from "@finos/vuu-layout"; import { VuuRange, VuuSortType } from "@finos/vuu-protocol-types"; import { useTableAndColumnSettings } from "@finos/vuu-table-extras"; import { @@ -42,22 +46,21 @@ import { useMemo, useState, } from "react"; -import { - buildContextMenuDescriptors, - ColumnActionHide, - ColumnActionPin, - MeasuredProps, - TableProps, -} from "../table"; +import { TableProps } from "./TableNext"; import { TableColumnResizeHandler } from "./column-resizing"; import { updateTableConfig } from "./table-config"; import { useDataSource } from "./useDataSource"; import { useInitialValue } from "./useInitialValue"; import { useSelection } from "./useSelection"; import { useTableContextMenu } from "./useTableContextMenu"; -import { useHandleTableContextMenu } from "./context-menu"; +import { + buildContextMenuDescriptors, + useHandleTableContextMenu, +} from "./context-menu"; import { useCellEditing } from "./useCellEditing"; import { + ColumnActionHide, + ColumnActionPin, isShowColumnSettings, isShowTableSettings, PersistentColumnAction, @@ -98,6 +101,12 @@ export interface TableHookProps const { KEY, IS_EXPANDED, IS_LEAF } = metadataKeys; +const NULL_DRAG_DROP = { + draggable: undefined, + onMouseDown: undefined, +}; +const useNullDragDrop = () => NULL_DRAG_DROP; + const addColumn = ( tableConfig: TableConfig, column: ColumnDescriptor @@ -139,6 +148,8 @@ export const useTable = ({ // // that logic when dataSource itself changes. // dataSourceRef.current = dataSource; + const useRowDragDrop = allowDragDrop ? useDragDrop : useNullDragDrop; + const [size, setSize] = useState(); const handleResize = useCallback((size: MeasuredSize) => { setSize(size); @@ -184,7 +195,7 @@ export const useTable = ({ /** * These stateColumns are required only for the duration of a column resize operation */ - const [stateColumns, setStateColumns] = useState(); + const [stateColumns, setStateColumns] = useState(); const [columns, setColumnSize] = useMemo(() => { const setSize = (columnName: string, width: number) => { const cols = updateColumn(modelColumns, columnName, { width }); @@ -361,7 +372,7 @@ export const useTable = ({ const handleSort = useCallback( ( - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, extendSort = false, sortType?: VuuSortType ) => { @@ -421,7 +432,7 @@ export const useTable = ({ ); const onToggleGroup = useCallback( - (row: DataSourceRow, column: KeyedColumnDescriptor) => { + (row: DataSourceRow, column: RuntimeColumnDescriptor) => { const isJson = isJsonGroup(column, row); const key = row[KEY]; @@ -501,6 +512,7 @@ export const useTable = ({ const { onBlur: editingBlur, + onDoubleClick: editingDoubleClick, onKeyDown: editingKeyDown, onFocus: editingFocus, } = useCellEditing({ @@ -539,7 +551,7 @@ export const useTable = ({ ); const onRemoveGroupColumn = useCallback( - (column: KeyedColumnDescriptor) => { + (column: RuntimeColumnDescriptor) => { if (isGroupColumn(column)) { dataSource.groupBy = []; } else { @@ -681,7 +693,7 @@ export const useTable = ({ // Drag Drop rowss const { onMouseDown: rowDragMouseDown, draggable: draggableRow } = - useDragDrop({ + useRowDragDrop({ allowDragDrop, containerRef, draggableClassName: `vuuTableNext`, @@ -703,6 +715,7 @@ export const useTable = ({ draggableColumn, draggableRow, onBlur: editingBlur, + onDoubleClick: editingDoubleClick, onFocus: handleFocus, onKeyDown: handleKeyDown, onMouseDown: rowDragMouseDown, diff --git a/vuu-ui/packages/vuu-table/src/table-next/useTableViewport.ts b/vuu-ui/packages/vuu-table/src/table-next/useTableViewport.ts index a49d34b7b..4f51d0a78 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useTableViewport.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useTableViewport.ts @@ -4,7 +4,7 @@ * to support pinned columns. */ import { - KeyedColumnDescriptor, + RuntimeColumnDescriptor, TableHeadings, } from "@finos/vuu-datagrid-types"; import { useCallback, useMemo, useRef } from "react"; @@ -18,7 +18,7 @@ import { } from "@finos/vuu-utils"; export interface TableViewportHookProps { - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; headerHeight: number; headings: TableHeadings; rowCount: number; @@ -66,7 +66,7 @@ const UNMEASURED_VIEWPORT: TableViewportHookResult = { viewportBodyHeight: 0, }; -const measurePinnedColumns = (columns: KeyedColumnDescriptor[]) => { +const measurePinnedColumns = (columns: RuntimeColumnDescriptor[]) => { let pinnedWidthLeft = 0; let pinnedWidthRight = 0; let unpinnedWidth = 0; diff --git a/vuu-ui/packages/vuu-table/src/table-next/useVirtualViewport.ts b/vuu-ui/packages/vuu-table/src/table-next/useVirtualViewport.ts index 7c4e39d78..60db1f181 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/useVirtualViewport.ts +++ b/vuu-ui/packages/vuu-table/src/table-next/useVirtualViewport.ts @@ -1,11 +1,11 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { VuuRange } from "@finos/vuu-protocol-types"; import { RowAtPositionFunc } from "@finos/vuu-utils"; import { useCallback, useEffect, useRef } from "react"; import { ViewportMeasurements } from "@finos/vuu-table"; export interface VirtualViewportHookProps { - columns: KeyedColumnDescriptor[]; + columns: RuntimeColumnDescriptor[]; getRowAtPosition: RowAtPositionFunc; setRange: (range: VuuRange) => void; viewportMeasurements: ViewportMeasurements; diff --git a/vuu-ui/packages/vuu-table/src/table/ColumnResizer.css b/vuu-ui/packages/vuu-table/src/table/ColumnResizer.css deleted file mode 100644 index f91a83e11..000000000 --- a/vuu-ui/packages/vuu-table/src/table/ColumnResizer.css +++ /dev/null @@ -1,22 +0,0 @@ -.vuuColumnResizer { - background-color: var(--columnResizer-color); - cursor: col-resize; - height: var(--header-height); - position: relative; - width: 4px; -} - -.vuuColumnResizer:hover { - --columnResizer-color: var(--salt-color-blue-500); -} - -.vuuColumnResizer:after { - content: ''; - position: absolute; - width: var(--columnResizer-width, 1px); - top: 0; - bottom:0; - right: -1px; - background-color: var(--columnResizer-color, var(--salt-separable-tertiary-borderColor)); - height: var(--columnResizer-height, calc(100% + 1px)); -} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/ColumnResizer.tsx b/vuu-ui/packages/vuu-table/src/table/ColumnResizer.tsx deleted file mode 100644 index b05725d8d..000000000 --- a/vuu-ui/packages/vuu-table/src/table/ColumnResizer.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// export interface ColumnResizerProps {} -import { useCallback, useRef } from "react"; -import "./ColumnResizer.css"; - -const NOOP = () => undefined; - -const baseClass = "vuuColumnResizer"; -export interface TableColumnResizerProps { - onDrag: (evt: MouseEvent, moveBy: number) => void; - onDragEnd: (evt: MouseEvent) => void; - onDragStart: (evt: React.MouseEvent) => void; -} - -export const ColumnResizer = ({ - onDrag, - onDragEnd = NOOP, - onDragStart = NOOP, -}: TableColumnResizerProps) => { - const position = useRef(0); - - const onMouseMove = useCallback( - (e: MouseEvent) => { - if (e.stopPropagation) { - e.stopPropagation(); - } - - if (e.preventDefault) { - e.preventDefault(); - } - - const x = Math.round(e.clientX); - const moveBy = x - position.current; - position.current = x; - - if (moveBy !== 0) { - onDrag(e, moveBy); - } - }, - [onDrag] - ); - - const onMouseUp = useCallback( - (e: MouseEvent) => { - window.removeEventListener("mouseup", onMouseUp); - window.removeEventListener("mousemove", onMouseMove); - onDragEnd(e); - }, - [onDragEnd, onMouseMove] - ); - - const handleMouseDown = useCallback( - (e: React.MouseEvent) => { - onDragStart(e); - position.current = Math.round(e.clientX); - - window.addEventListener("mouseup", onMouseUp); - window.addEventListener("mousemove", onMouseMove); - - if (e.stopPropagation) { - e.stopPropagation(); - } - - if (e.preventDefault) { - e.preventDefault(); - } - }, - [onDragStart, onMouseMove, onMouseUp] - ); - - return ( -
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/DragVisualizer.css b/vuu-ui/packages/vuu-table/src/table/DragVisualizer.css deleted file mode 100644 index a8f921702..000000000 --- a/vuu-ui/packages/vuu-table/src/table/DragVisualizer.css +++ /dev/null @@ -1,33 +0,0 @@ -.DragVizItem:nth-child(odd) { -background-color: rgba(255,255,0,.5); -} - -.DragVizItem:nth-child(even) { -background-color: rgba(255,192,203, .5); -} - -.DragVizItem-dropTarget { - background-color: navy !important; - color: white; - position: relative; -} - -.DragVizItem-dropTarget-start:after { - background-color: red; - content: ''; - position: absolute; - height: 5px; - width: 100%; - left: 0; - top: 0; -} - -.DragVizItem-dropTarget-end:after { - background-color: red; - content: ''; - position: absolute; - height: 5px; - width: 100%; - left: 0; - bottom: 0; -} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/DragVisualizer.tsx b/vuu-ui/packages/vuu-table/src/table/DragVisualizer.tsx deleted file mode 100644 index 0c8742778..000000000 --- a/vuu-ui/packages/vuu-table/src/table/DragVisualizer.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { - createContext, - ReactNode, - useCallback, - useContext, - useLayoutEffect, - useRef, - useState, -} from "react"; -import cx from "classnames"; -import { MeasuredDropTarget } from "@finos/vuu-ui-controls"; - -import "./DragVisualizer.css"; - -const DragVizContext = createContext({}); - -export const useListViz = () => useContext(DragVizContext); - -export interface DragVisualizerProps { - children: ReactNode; - orientation?: "vertical" | "horizontal"; -} - -export const DragVisualizer: React.FC = ({ - children, - orientation = "vertical", -}) => { - const [content, setContent] = useState([]); - const [dropTarget, setDropTarget] = useState(); - const [dropZone, setDropZone] = useState([]); - const [vizKey, setVisKey] = useState(1); - const vizRootRef = useRef(null); - const vizRootOffset = useRef(0); - - const setMeasurements = useCallback( - ( - measurements: MeasuredDropTarget[], - dropTarget: MeasuredDropTarget, - dropZone = "" - ) => { - console.log(measurements); - setContent(measurements); - setDropTarget(dropTarget); - setDropZone(dropZone); - setVisKey((vk) => vk + 1); - }, - [] - ); - - const isHorizontal = orientation === "horizontal"; - const flexDirection = isHorizontal ? "column" : "row"; - const START = isHorizontal ? "left" : "top"; - const DIMENSION = isHorizontal ? "width" : "height"; - - useLayoutEffect(() => { - if (vizRootRef.current) { - const { left } = vizRootRef.current.getBoundingClientRect(); - vizRootOffset.current = left; - } - }, []); - - return ( - -
-
{children}
-
- {content.map((item, i) => ( -
- {item.id} - {`[${item.index}]`} - {`[${item.currentIndex}]`} -
- ))} -
-
-
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/README b/vuu-ui/packages/vuu-table/src/table/README deleted file mode 100644 index e69de29bb..000000000 diff --git a/vuu-ui/packages/vuu-table/src/table/RowBasedTable.css b/vuu-ui/packages/vuu-table/src/table/RowBasedTable.css deleted file mode 100644 index ed15d5bba..000000000 --- a/vuu-ui/packages/vuu-table/src/table/RowBasedTable.css +++ /dev/null @@ -1,26 +0,0 @@ -.vuuTable-table { - --vuuTable-rowHeight: var(--row-height); - --vuuTableCell-border-bottomColor: transparent; - --vuuTableCell-border-rightColor: var(--salt-separable-tertiary-borderColor); - - border-collapse: separate; - border-spacing: 0; - border-left: 1px solid #ccc; - border: none; - font-size: var(--vuuTable-font-size, 10px); - margin: 0; - min-height: 100%; - width: var(--content-width); -} - -.vuuTable-headers { - position: sticky; - top: 0; - z-index: 1; -} - -.vuuTable-body { - height: var(--content-height); - position: relative; -} - diff --git a/vuu-ui/packages/vuu-table/src/table/RowBasedTable.tsx b/vuu-ui/packages/vuu-table/src/table/RowBasedTable.tsx deleted file mode 100644 index 8c6ff3a00..000000000 --- a/vuu-ui/packages/vuu-table/src/table/RowBasedTable.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { - buildColumnMap, - getColumnStyle, - isGroupColumn, - metadataKeys, - notHidden, - visibleColumnAtIndex, -} from "@finos/vuu-utils"; -import { MouseEvent, useCallback, useMemo } from "react"; -import { TableImplementationProps } from "./dataTableTypes"; -import { TableRow } from "./TableRow"; -import { TableGroupHeaderCell } from "./TableGroupHeaderCell"; -import { TableHeaderCell } from "./TableHeaderCell"; - -import "./RowBasedTable.css"; - -const classBase = "vuuTable"; -const { RENDER_IDX } = metadataKeys; - -export const RowBasedTable = ({ - columns, - columnsWithinViewport, - data, - getRowOffset, - headings, - onColumnResize, - onHeaderCellDragStart, - onContextMenu, - onRemoveColumnFromGroupBy, - onRowClick, - onSort, - onToggleGroup, - tableId, - virtualColSpan = 0, - rowCount, -}: TableImplementationProps) => { - const handleDragStart = useCallback( - (evt: MouseEvent) => { - onHeaderCellDragStart?.(evt); - }, - [onHeaderCellDragStart] - ); - - const visibleColumns = useMemo(() => { - return columns.filter(notHidden); - }, [columns]); - - const columnMap = useMemo(() => buildColumnMap(columns), [columns]); - - const handleHeaderClick = useCallback( - (evt: MouseEvent) => { - const targetElement = evt.target as HTMLElement; - const headerCell = targetElement.closest( - ".vuuTable-headerCell" - ) as HTMLElement; - const colIdx = parseInt(headerCell?.dataset.idx ?? "-1"); - const column = visibleColumnAtIndex(columns, colIdx); - const isAdditive = evt.shiftKey; - column && onSort(column, isAdditive); - }, - [columns, onSort] - ); - - return ( -
-
- {headings.map((colHeaders, i) => ( -
- {colHeaders.map(({ label, width }, j) => ( -
- {label} -
- ))} -
- ))} -
- {visibleColumns.map((column, i) => { - const style = getColumnStyle(column); - return isGroupColumn(column) ? ( - - ) : ( - - ); - })} -
-
-
- {data?.map((row) => ( - - ))} -
-
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/SortIndicator.css b/vuu-ui/packages/vuu-table/src/table/SortIndicator.css deleted file mode 100644 index aad63199b..000000000 --- a/vuu-ui/packages/vuu-table/src/table/SortIndicator.css +++ /dev/null @@ -1,15 +0,0 @@ -.vuuSortIndicator { - --menu-icon-size: 18px; - --menu-item-icon-color: black; - display: flex; - flex-direction: column; - position: relative; - width: 18px; -} - -.vuuSortPosition { - font-size: 10px; - line-height: 10px; - text-align: center; -} - diff --git a/vuu-ui/packages/vuu-table/src/table/SortIndicator.tsx b/vuu-ui/packages/vuu-table/src/table/SortIndicator.tsx deleted file mode 100644 index 2c0b6e279..000000000 --- a/vuu-ui/packages/vuu-table/src/table/SortIndicator.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ColumnSort } from "@finos/vuu-datagrid-types"; -import cx from "classnames"; - -import "./SortIndicator.css"; - -export interface SortIndicatorProps { - sorted?: ColumnSort; -} - -const classBase = "vuuSortIndicator"; - -export const SortIndicator = ({ sorted }: SortIndicatorProps) => { - if (!sorted) { - return null; - } - - const direction = - typeof sorted === "number" - ? sorted < 0 - ? "dsc" - : "asc" - : sorted === "A" - ? "asc" - : "dsc"; - - return typeof sorted === "number" ? ( -
- - {Math.abs(sorted)} -
- ) : ( -
- -
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/Table-loading.css b/vuu-ui/packages/vuu-table/src/table/Table-loading.css deleted file mode 100644 index 8d06b1db5..000000000 --- a/vuu-ui/packages/vuu-table/src/table/Table-loading.css +++ /dev/null @@ -1,63 +0,0 @@ -.vuuTable-loading .vuuTable-table { - --skeleton-height: 20px; - --skeleton-width: calc(var(--content-width) - 8px); - --skeleton-left: 4px; - --skeleton-row-height: 16px; - --skeleton-size: var(--skeleton-width) var(--skeleton-height); - --skeleton-row: linear-gradient( - var(--salt-color-gray-20-fade-background) var(--skeleton-row-height), - transparent 0 - ); - --skeleton-background-image: var(--skeleton-row); - - background-image: var(--skeleton-background-image); - background-repeat: repeat-y; - background-size: var(--skeleton-size); - background-position-x: var(--skeleton-left); - background-position-y: 27px; - } - - .vuuTable-loading .vuuTable-table { - --skeleton-height: 20px; - --skeleton-width: calc(var(--content-width) - 8px); - --skeleton-left: 4px; - --skeleton-row-height: 16px; - --skeleton-size: var(--skeleton-width) var(--skeleton-height); - --skeleton-row: linear-gradient( - var(--salt-color-gray-20-fade-background) var(--skeleton-row-height), - transparent 0 - ); - --skeleton-background-image: var(--skeleton-row); - - background-image: var(--skeleton-background-image); - background-repeat: repeat-y; - background-size: var(--skeleton-size); - background-position-x: var(--skeleton-left); - background-position-y: 27px; - - /* animation:linearAnim 2s infinite linear */ - } - - .vuuTable-loading .vuuTable-table:after { - animation: shimmer 2s infinite; - background: linear-gradient( - 90deg, - rgba(255,255,255, 0) 0, - rgba(255,255,255, .2) 20%, - rgba(255,255,255, .6) 60%, - rgba(255,255,255, 0) - ); - content: ''; - height: var(--table-height); - left: 0px; - position: absolute; - transform: translateX(-100%); - top: var(--header-height); - width: var(--content-width); - } - - @keyframes shimmer { - 100% { - transform: translateX(100%); - } - } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/Table.css b/vuu-ui/packages/vuu-table/src/table/Table.css deleted file mode 100644 index 7dc6c5386..000000000 --- a/vuu-ui/packages/vuu-table/src/table/Table.css +++ /dev/null @@ -1,86 +0,0 @@ -/** - variables injected by Table component - --content-height - --content-width - --header-height - --horizontal-scrollbar-height - --pinned-width-left - --pinned-width-right - --row-height - --table-height - --table-width - --total-header-height - --vertical-scrollbar-width - --viewport-body-height -*/ -.vuuTable { - - --dataTable-background: var(--salt-container-primary-background, inherit); - --row-background-even: var(--dataTable-background); - --row-background-odd: var(--dataTable-background); - --table-background: var(--dataTable-background, none); - - background-color: var(--dataTable-background); - position: relative; -} - -.vuuTable-zebra { - --row-background-even: var(--salt-container-secondary-background); -} - -.vuuTable-scrollbarContainer { - --scroll-content-width: calc(var(--content-width) - var(--pinned-width-left)); - border-bottom: none !important; - border-top: none !important; - border-left: solid 1px var(--salt-container-primary-borderColor); - /* a top border */ - box-shadow: 0px -1px 0px 0px var(--salt-container-primary-borderColor); - height: var(--viewport-body-height); - left: var(--pinned-width-left); - overflow: auto; - position: absolute; - top: var(--total-header-height); - width: calc(var(--table-width) - var(--pinned-width-left) + 1px); -} - -.vuuTable-scrollbarContent { - height: calc(var(--content-height) + var(--horizontal-scrollbar-height)); - position: absolute; - width: var(--scroll-content-width, var(--content-width)); -} - -.vuuTable-contentContainer { - --vuuTableHeaderHeight: var(--header-height, 30px); - - background-color: var(--salt-container-primary-background); - - height: calc(var(--table-height) - var(--horizontal-scrollbar-height)); - position: relative; - overflow: auto; - overscroll-behavior: none; - width: calc(var(--table-width) - var(--vertical-scrollbar-width)); -} - -.vuuTable-contentContainer::-webkit-scrollbar { - display: none; -} - - -:is(.vuuPinLeft, .vuuPinRight, .vuuPinFloating) { - background-color: inherit; - position: sticky; - z-index: 1; -} - -.vuuTable-settings { - --saltButton-height: var(--header-height); - --saltButton-width: 15px; - position: absolute !important; - right: 0; - top: 0; -} - -.vuuTable:has(.vuuTable-headerCell-resizing) * { - cursor: col-resize; -} - diff --git a/vuu-ui/packages/vuu-table/src/table/Table.tsx b/vuu-ui/packages/vuu-table/src/table/Table.tsx deleted file mode 100644 index 2b996ae13..000000000 --- a/vuu-ui/packages/vuu-table/src/table/Table.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { ContextMenuProvider } from "@finos/vuu-popups"; -import { Button, useIdMemo } from "@salt-ds/core"; -import { CSSProperties } from "react"; -import { buildContextMenuDescriptors } from "./context-menu"; -import { TableProps } from "./dataTableTypes"; -// import { RowBasedTable } from "./RowBasedTable"; -import { RowBasedTable } from "./RowBasedTable"; -import { useTable } from "./useTable"; -import cx from "classnames"; - -import "./Table.css"; -import "./Table-loading.css"; - -import { isDataLoading } from "@finos/vuu-utils"; - -const classBase = "vuuTable"; - -export interface TablePropsDeprecated - extends Omit { - height?: number; - width?: number; -} - -export const Table = ({ - allowConfigEditing: showSettings = false, - className: classNameProp, - config, - dataSource, - headerHeight = 25, - height, - id: idProp, - onConfigChange, - onFeatureInvocation, - onSelect, - onSelectionChange, - onShowConfigEditor: onShowSettings, - renderBufferSize = 0, - rowHeight = 20, - selectionModel = "extended", - style: styleProp, - width, - ...htmlAttributes -}: TablePropsDeprecated) => { - const id = useIdMemo(idProp); - const { - containerMeasurements: { containerRef, innerSize, outerSize }, - containerProps, - dispatchColumnAction, - draggable, - draggedItemIndex, - handleContextMenuAction, - scrollProps, - viewportMeasurements, - ...tableProps - } = useTable({ - config, - dataSource, - renderBufferSize, - headerHeight, - height, - onConfigChange, - onFeatureInvocation, - onSelectionChange, - rowHeight, - selectionModel, - width, - }); - - console.log({ tableProps }); - const style = { - ...outerSize, - "--content-height": `${viewportMeasurements.contentHeight}px`, - "--horizontal-scrollbar-height": `${viewportMeasurements.horizontalScrollbarHeight}px`, - "--content-width": `${viewportMeasurements.contentWidth}px`, - "--pinned-width-left": `${viewportMeasurements.pinnedWidthLeft}px`, - "--pinned-width-right": `${viewportMeasurements.pinnedWidthRight}px`, - "--header-height": `${headerHeight}px`, - "--row-height": `${rowHeight}px`, - "--table-height": `${innerSize?.height}px`, - "--table-width": `${innerSize?.width}px`, - "--total-header-height": `${viewportMeasurements.totalHeaderHeight}px`, - "--vertical-scrollbar-width": `${viewportMeasurements.verticalScrollbarWidth}px`, - "--viewport-body-height": `${viewportMeasurements.viewportBodyHeight}px`, - } as CSSProperties; - - const className = cx(classBase, classNameProp, { - [`${classBase}-zebra`]: config.zebraStripes, - [`${classBase}-loading`]: isDataLoading(tableProps.columns), - }); - - return ( - -
- {innerSize ? ( -
-
-
- ) : null} - {innerSize ? ( -
- - {draggable} -
- ) : null} - {showSettings && innerSize ? ( -
- - ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/TableCell.css b/vuu-ui/packages/vuu-table/src/table/TableCell.css deleted file mode 100644 index 268410e3f..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableCell.css +++ /dev/null @@ -1,102 +0,0 @@ -.vuuTable { - --cell-outline-width: 2px; - user-select: none; -} - -[role="cell"] { - display: inline-block; -} - -[data-align="end"] { - margin-left: auto; -} - -[data-align="end"] + [data-align="end"] { - margin-left: 0; -} - - -.vuuTable-table [role="cell"] { - --saltEditableLabel-height: 17px; - --saltInput-height: 17px; - --saltInput-minHeight: 17px; - - border-right: 1px solid var(--vuuTableCell-border-rightColor); - border-bottom: 1px solid var(--vuuTableCell-border-bottomColor); - color: var(--salt-text-primary-foreground); - cursor: default; - height: var(--vuuTable-rowHeight); - line-height: calc(var(--vuuTable-rowHeight) - 1px); - overflow: hidden; - padding: 0 5px; - vertical-align: top; -} - - -.vuuTable-headerCell:focus, -.vuuTable [role="cell"]:focus { - outline: var(--vuuTableCell-outline, dotted var(--salt-color-blue-400) var(--cell-outline-width)); - outline-offset: calc(var(--cell-outline-width) * -1); - /** This is to achieve a white background to outline dashes */ - box-shadow: inset 0 0 0 var(--cell-outline-width) white; - border-bottom: none; -} - -.vuuTable-headerCell:focus .vuuTable-headerCell-inner{ - /** This is to achieve a white background to outline dashes */ - padding-bottom: var(--cell-outline-width); -} - -.vuuTable-headerCell:not(.vuuTable-headerCell-resizing):focus .vuuTable-headerCell-inner{ - --columnResizer-color: transparent; -} - - -.vuuTable [role="cell"]:focus { - /** This is to achieve a white background to outline dashes */ - border-right: none; - padding-bottom: 1px; -} - - -[role="cell"][data-editable="true"] { - --salt-text-fontSize: 10px; - --vuu-icon-size: 5px; - position: relative; -} - -[role="cell"][data-editable="true"]:after { - top: 0; - content: ""; - background-color: var(--salt-text-secondary-foreground, black); - left: 0; - height: var(--vuu-icon-height, var(--vuu-icon-size, 12px)); - -webkit-mask: var(--svg-corner-triangle) center center/var(--vuu-icon-size) var(--vuu-icon-size); - mask: var(--svg-corner-triangle) center center/var(--vuu-icon-size) var(--vuu-icon-size); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - position: absolute; - transform: rotate(180deg); - width: var(--vuu-icon-width, var(--vuu-icon-size, 12px)); - } - - [role="cell"]:focus[data-editable], - [role="cell"]:focus-within[data-editable], - [role="cell"]:has(.saltEditableLabel-editing) { - outline: solid var(--salt-color-blue-400) 1px; - background-color: white; - outline-offset: -1px; -} - -[role="cell"]:focus[data-editable="true"]:after, -[role="cell"]:has(.saltEditableLabel):after { - /* background-color: black; */ - background-color: var(--salt-color-blue-400); - left: 1px; - top: 1px; -} - -.vuuAlignRight { - text-align: right; - } - \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/TableCell.tsx b/vuu-ui/packages/vuu-table/src/table/TableCell.tsx deleted file mode 100644 index aef47b64d..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableCell.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { TableCellProps } from "@finos/vuu-datagrid-types"; -import { getColumnStyle, metadataKeys } from "@finos/vuu-utils"; -import { EditableLabel } from "@finos/vuu-ui-controls"; -import cx from "classnames"; -import { - KeyboardEvent, - memo, - MouseEvent, - useCallback, - useRef, - useState, -} from "react"; - -import "./TableCell.css"; - -const { KEY } = metadataKeys; - -export const TableCell = memo( - ({ - className: classNameProp, - column, - columnMap, - onClick, - row, - }: TableCellProps) => { - const labelFieldRef = useRef(null); - const { - align, - CellRenderer, - key, - pin, - editable, - resizing, - valueFormatter, - } = column; - const [editing, setEditing] = useState(false); - const value = valueFormatter(row[key]); - const [editableValue, setEditableValue] = useState(value); - const handleTitleMouseDown = () => { - labelFieldRef.current?.focus(); - }; - const handleTitleKeyDown = (evt: KeyboardEvent) => { - if (evt.key === "Enter") { - setEditing(true); - } - }; - - const handleClick = useCallback( - (evt: MouseEvent) => { - onClick?.(evt, column); - }, - [column, onClick] - ); - - const handleEnterEditMode = () => { - setEditing(true); - }; - - const handleExitEditMode = ( - originalValue = "", - finalValue = "", - allowDeactivation = true, - editCancelled = false - ) => { - setEditing(false); - if (editCancelled) { - setEditableValue(originalValue); - } else if (finalValue !== originalValue) { - setEditableValue(finalValue); - } - if (allowDeactivation === false) { - labelFieldRef.current?.focus(); - } - }; - - // might want to useMemo here, this won't change often - const className = - cx(classNameProp, { - vuuAlignRight: align === "right", - vuuPinFloating: pin === "floating", - vuuPinLeft: pin === "left", - vuuPinRight: pin === "right", - "vuuTableCell-resizing": resizing, - }) || undefined; - const style = getColumnStyle(column); - return editable ? ( -
- -
- ) : ( -
- {CellRenderer ? ( - - ) : ( - value - )} -
- ); - }, - cellValuesAreEqual -); -TableCell.displayName = "TableCell"; - -function cellValuesAreEqual(prev: TableCellProps, next: TableCellProps) { - return ( - prev.column === next.column && - prev.onClick === next.onClick && - prev.row[KEY] === next.row[KEY] && - prev.row[prev.column.key] === next.row[next.column.key] - ); -} diff --git a/vuu-ui/packages/vuu-table/src/table/TableGroupCell.css b/vuu-ui/packages/vuu-table/src/table/TableGroupCell.css deleted file mode 100644 index 05963341b..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableGroupCell.css +++ /dev/null @@ -1,39 +0,0 @@ -.vuuTableGroupCell { - --spacer-width: 20px; - --toggle-icon-transform: var(--row-toggle-icon-transform, none); - --vuu-icon-width: 18px; - - align-items: center; - display: inline-flex; -} - -.vuuTableGroupCell-spacer { - height: 100%; - position: relative; - width: var(--spacer-width); -} - -.vuuTableGroupCell-spacer:after { - background: var(--salt-container-primary-borderColor); - content: ''; - position: absolute; - top:0; - bottom: -1px; - /* left: calc(var(--spacer-width / 2)); */ - left: 9px; - width: 1px; -} - -.vuuTableGroupCell-toggle { - transition: transform 0.25s; - transform: var(--toggle-icon-transform); -} - - -/* .vuuTableGroupCell-toggle[data-icon='triangle-right']{ - --vuu-icon-svg: var(--svg-plus-box); -} - -.vuuTableRow-expanded .vuuTableGroupCell-toggle[data-icon='triangle-right']{ - --vuu-icon-svg: var(--svg-minus-box); -} */ diff --git a/vuu-ui/packages/vuu-table/src/table/TableGroupCell.tsx b/vuu-ui/packages/vuu-table/src/table/TableGroupCell.tsx deleted file mode 100644 index def12270e..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableGroupCell.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { - GroupColumnDescriptor, - TableCellProps, -} from "@finos/vuu-datagrid-types"; -import { - getColumnStyle, - getGroupValueAndOffset, - metadataKeys, -} from "@finos/vuu-utils"; -import { MouseEvent, useCallback } from "react"; - -import "./TableGroupCell.css"; - -const { IS_LEAF } = metadataKeys; - -export const TableGroupCell = ({ column, onClick, row }: TableCellProps) => { - const { columns } = column as GroupColumnDescriptor; - const [value, offset] = getGroupValueAndOffset(columns, row); - - const handleClick = useCallback( - (evt: MouseEvent) => { - onClick?.(evt, column); - }, - [column, onClick] - ); - - const style = getColumnStyle(column); - const isLeaf = row[IS_LEAF]; - const spacers = Array(offset) - .fill(0) - .map((n, i) => ); - return ( -
- {spacers} - {isLeaf ? null : ( - - )} - {value} -
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.css b/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.css deleted file mode 100644 index 68df6e62c..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.css +++ /dev/null @@ -1,82 +0,0 @@ - -.salt-theme { - --svg-spinner: url('data:image/svg+xml;utf8,'); -} - -.vuuTable-groupHeaderCell { - --cell-align: 'flex-start'; - text-align: left; - background: var(--dataTable-background); - cursor: default; - height: var(--vuuTableHeaderHeight); - /* ensure header row sits atop everything else when scrolling down */ - } - - - .vuuTable-groupHeaderCell-inner { - align-items: center; - display: flex; - height: 100%; - padding-left: 1px; - } - - .vuuTable-groupHeaderCell-label { - align-items: center; - display: flex; - flex: 0 0 auto; - } - - .vuuTable-groupHeaderCell-col { - align-items: center; - background-color: inherit; - display: inline-flex; - flex: 0 1 auto; - height: calc(var(--vuuTableHeaderHeight) - 2px); - justify-content: space-between; - padding-right: 8px; - position: relative; - } - - .vuuTable-groupHeaderCell-close { - --vuu-icon-height: 18px; - --vuu-icon-width: 18px; - cursor: pointer; - left: 3px; - } - - .vuuTable-groupHeaderCell-col:nth-child(odd) { - background-color: var(--salt-color-gray-50); - } - .vuuTable-groupHeaderCell-col:nth-child(even) { - background-color: var(--salt-color-gray-40); - } - - .vuuTable-groupHeaderCell-col:first-child { - clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%); - padding-left: 3px; - z-index: 1; - } - - .vuuTable-groupHeaderCell-col:not(:first-child) { - margin-left: -6px; - padding-left: 12px; - clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%); - } - - .vuuTable-groupHeaderCell-resizing { - --columnResizer-color: var(--salt-color-blue-500); - --columnResizer-height: var(--table-height); - --columnResizer-width: 2px; - } - .vuuTable-groupHeaderCell-pending { - --pending-content: ''; - } - - .vuuTable-groupHeaderCell-col:has(+ .vuuColumnResizer):after { - content: var(--pending-content); - width: 24px; - height:24px; - background-image: var(--svg-spinner); - background-repeat: no-repeat; - background-size: cover; - } diff --git a/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.tsx b/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.tsx deleted file mode 100644 index abd2ae127..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableGroupHeaderCell.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import cx from "classnames"; -import { HTMLAttributes, useRef } from "react"; -import { ColumnResizer } from "./ColumnResizer"; -import { TableHeaderCellProps } from "./TableHeaderCell"; -import { - GroupColumnDescriptor, - KeyedColumnDescriptor, -} from "@finos/vuu-datagrid-types"; -import { useTableColumnResize } from "./useTableColumnResize"; - -import "./TableGroupHeaderCell.css"; - -const classBase = "vuuTable-groupHeaderCell"; - -interface RemoveButtonProps - extends Omit, "onClick"> { - column?: KeyedColumnDescriptor; - onClick?: (column?: KeyedColumnDescriptor) => void; -} -const RemoveButton = ({ - column, - onClick, - ...htmlAttributes -}: RemoveButtonProps) => { - return ( - onClick?.(column)} - /> - ); -}; - -export interface ColHeaderProps - extends Omit, "onClick"> { - column: KeyedColumnDescriptor; - onRemove?: (column?: KeyedColumnDescriptor) => void; -} - -const ColHeader = (props: ColHeaderProps) => { - const { children, column, className } = props; - return ( -
- {column.name} - {children} -
- ); -}; - -export interface TableGroupHeaderCellProps - extends Omit { - column: GroupColumnDescriptor; - onRemoveColumn?: (column?: KeyedColumnDescriptor) => void; -} - -export const TableGroupHeaderCell = ({ - column: groupColumn, - className: classNameProp, - onRemoveColumn, - onResize, - ...props -}: TableGroupHeaderCellProps) => { - const rootRef = useRef(null); - const { isResizing, ...resizeProps } = useTableColumnResize({ - column: groupColumn, - onResize, - rootRef, - }); - const className = cx(classBase, classNameProp, { - vuuPinLeft: groupColumn.pin === "left", - [`${classBase}-right`]: groupColumn.align === "right", - [`${classBase}-resizing`]: groupColumn.resizing, - [`${classBase}-pending`]: groupColumn.groupConfirmed === false, - }); - const { columns } = groupColumn; - - return ( -
-
- {columns.map((column) => ( - - {columns.length > 1 ? ( - - ) : null} - - ))} - - {groupColumn.resizeable !== false ? ( - - ) : null} -
-
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.css b/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.css deleted file mode 100644 index d625137bc..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.css +++ /dev/null @@ -1,100 +0,0 @@ -/* We support multi level headings up to a maximum of 4 heading levels */ -.vuuTable-heading:nth-child(2) { - --heading-top: calc(var(--header-height)); -} -.vuuTable-heading:nth-child(3) { - --heading-top: calc(var(--header-height) * 2); -} -.vuuTable-heading:nth-child(3) { - --heading-top: calc(var(--header-height) * 3); -} - -.vuuTable-headingCell { - background: var(--dataTable-background); - border-color: var(--salt-separable-tertiary-borderColor); - border-style: solid solid solid none; - border-width: 1px; - color: var(--salt-text-secondary-foreground); - display: inline-block; - height: var(--vuuTableHeaderHeight); - padding: 0 !important; -} - -.vuuTable-heading:has(+ .vuuTable-heading) > .vuuTable-headingCell { - border-bottom-color: transparent; -} - -[role="columnHeader"] { - --vuuTableCell-border-bottomColor: var(--salt-separable-tertiary-borderColor); - --cell-align: 'flex-start'; - display: inline-block; - text-align: left; - background: var(--dataTable-background); - border-right: 1px solid var(--vuuTableCell-border-rightColor); - border-bottom: 1px solid var(--vuuTableCell-border-bottomColor); - color: var(--salt-text-secondary-foreground); - cursor: default; - height: var(--vuuTableHeaderHeight); - padding: 0 !important; - vertical-align: top; - } - - .vuuTable-headerCell-right { - --cell-align: flex-end; - } - - .vuuTable-headerCell-inner { - align-items: stretch; - display: flex; - height: 100%; - padding: 0 0 0 3px; - } - - .vuuTable-headerCell-inner:has(.vuuFilterIndicator){ - padding-left: 0; - } - - .vuuTable-headerCell-label { - align-items: center; - justify-content: var(--cell-align); - display: flex; - flex: 1 1 auto; - } - - .vuuTable-headerCell-resizing { - --columnResizer-color: var(--salt-color-blue-500); - --columnResizer-height: var(--table-height); - --columnResizer-width: 2px; - } - - [role='headerCell'].vuuPinLeft.vuuEndPin:after { - box-shadow: 2px 0px 5px rgba(0,0,0,0.4); - content: ""; - position: absolute; - width: 1px; - background-color: transparent; - height: var(--table-height); - top:0; - right: 0px; - } - - [role='headerCell'].vuuPinRight.vuuEndPin:after { - box-shadow: -2px 0px 5px rgba(0,0,0,0.3); - content: ""; - position: absolute; - width: 1px; - background-color: transparent; - height: var(--table-height); - top:0; - left: 0px; - } - - [role='headerCell']:is(.vuuPinLeft, .vuuPinRight, .vuuPinFloating) { - top:0; - z-index: 20; - } - - .saltDraggable-vuuTable-headerCell { - --dataTable-background: ivory; - --vuuTableHeaderHeight: 25px; - } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.tsx b/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.tsx deleted file mode 100644 index afabccacc..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableHeaderCell.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import cx from "classnames"; -import { HTMLAttributes, MouseEvent, useCallback, useRef } from "react"; -import { ColumnResizer } from "./ColumnResizer"; -import { SortIndicator } from "./SortIndicator"; -import { useTableColumnResize } from "./useTableColumnResize"; -import { TableColumnResizeHandler } from "./dataTableTypes"; - -import "./TableHeaderCell.css"; -import { useContextMenu } from "@finos/vuu-popups"; -import { FilterIndicator } from "./filter-indicator"; - -const classBase = "vuuTable-headerCell"; - -export interface TableHeaderCellProps - extends HTMLAttributes { - column: KeyedColumnDescriptor; - debugString?: string; - onDragStart?: (evt: MouseEvent) => void; - onResize?: TableColumnResizeHandler; -} - -export const TableHeaderCell = ({ - column, - className: classNameProp, - onClick, - onDragStart, - onResize, - ...props -}: TableHeaderCellProps) => { - const rootRef = useRef(null); - const { isResizing, ...resizeProps } = useTableColumnResize({ - column, - onResize, - rootRef, - }); - - const [showContextMenu] = useContextMenu(); - const dragTimerRef = useRef(null); - - const handleContextMenu = (e: MouseEvent) => { - showContextMenu(e, "header", { column }); - }; - - const handleClick = useCallback( - (evt: MouseEvent) => !isResizing && onClick?.(evt), - [isResizing, onClick] - ); - - const handleMouseDown = useCallback( - (evt: MouseEvent) => { - dragTimerRef.current = window.setTimeout(() => { - onDragStart?.(evt); - dragTimerRef.current = null; - }, 500); - }, - [onDragStart] - ); - const handleMouseUp = useCallback(() => { - if (dragTimerRef.current !== null) { - window.clearTimeout(dragTimerRef.current); - dragTimerRef.current = null; - } - }, []); - - const className = cx(classBase, classNameProp, { - vuuPinFloating: column.pin === "floating", - vuuPinLeft: column.pin === "left", - vuuPinRight: column.pin === "right", - vuuEndPin: column.endPin, - [`${classBase}-resizing`]: column.resizing, - [`${classBase}-right`]: column.align === "right", - }); - return ( -
-
- -
{column.label}
- - {column.resizeable !== false ? ( - - ) : null} -
-
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/TableRow.css b/vuu-ui/packages/vuu-table/src/table/TableRow.css deleted file mode 100644 index 9d93b06c8..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableRow.css +++ /dev/null @@ -1,33 +0,0 @@ - .vuuTableRow { - --row-background: var(--table-background); - position: absolute; - top:0; - } - - .vuuTableRow-even { - --row-background: var(--row-background-even); - } - - /* .vuuTableRow :is(.vuuPinFloating, .vuuPinLeft, .vuuPinRight) { - background-color: var(--row-background); - } */ - .vuuTableRow { - background-color: var(--row-background); - } - - .vuuTableRow-expanded { - --row-toggle-icon-transform: rotate(90deg); - } - - .vuuTableRow[aria-selected] { - background-color: var(--vuuTableRow-selected-background, var(--salt-selectable-background-selected)); - --vuuTableCell-border-bottomColor: var(--salt-selectable-borderColor-selected); - } - - /* .vuuTableRow:not([aria-selected]):has(+ [aria-selected]) { - --vuuTableCell-border-bottomColor: var(--salt-selectable-borderColor-selected); - } */ - - .vuuTableRow-preSelected { - --vuuTableCell-border-bottomColor: var(--salt-selectable-borderColor-selected); - } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/TableRow.tsx b/vuu-ui/packages/vuu-table/src/table/TableRow.tsx deleted file mode 100644 index 9f8b4925b..000000000 --- a/vuu-ui/packages/vuu-table/src/table/TableRow.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { DataSourceRow } from "@finos/vuu-data-types"; -import { - KeyedColumnDescriptor, - RowClickHandler, -} from "@finos/vuu-datagrid-types"; -import { - ColumnMap, - isGroupColumn, - isJsonColumn, - isJsonGroup, - metadataKeys, - notHidden, - RowSelected, -} from "@finos/vuu-utils"; -import cx from "classnames"; -import { HTMLAttributes, memo, MouseEvent, useCallback } from "react"; -import { TableCell } from "./TableCell"; -import { TableGroupCell } from "./TableGroupCell"; - -import "./TableRow.css"; - -const { IDX, IS_EXPANDED, SELECTED } = metadataKeys; -const { True, First, Last } = RowSelected; - -const classBase = "vuuTableRow"; - -export interface RowProps - extends Omit, "children" | "onClick"> { - columnMap: ColumnMap; - columns: KeyedColumnDescriptor[]; - offset: number; - onClick?: RowClickHandler; - onToggleGroup?: (row: DataSourceRow, column: KeyedColumnDescriptor) => void; - row: DataSourceRow; - virtualColSpan?: number; -} - -export const TableRow = memo(function Row({ - columnMap, - columns, - offset, - onClick, - onToggleGroup, - virtualColSpan = 0, - row, -}: RowProps) { - const { - [IDX]: rowIndex, - [IS_EXPANDED]: isExpanded, - [SELECTED]: selectionStatus, - } = row; - - const className = cx(classBase, { - [`${classBase}-even`]: rowIndex % 2 === 0, - [`${classBase}-expanded`]: isExpanded, - [`${classBase}-selected`]: selectionStatus & True, - [`${classBase}-selectedStart`]: selectionStatus & First, - [`${classBase}-selectedEnd`]: selectionStatus & Last, - }); - - const handleRowClick = useCallback( - (evt: MouseEvent) => { - const rangeSelect = evt.shiftKey; - const keepExistingSelection = evt.ctrlKey || evt.metaKey; /* mac only */ - onClick?.(row, rangeSelect, keepExistingSelection); - }, - [onClick, row] - ); - - const handleGroupCellClick = useCallback( - (evt: MouseEvent, column: KeyedColumnDescriptor) => { - if (isGroupColumn(column) || isJsonGroup(column, row)) { - evt.stopPropagation(); - onToggleGroup?.(row, column); - } - }, - [onToggleGroup, row] - ); - - return ( -
- {virtualColSpan > 0 ? ( -
- ) : null} - {columns.filter(notHidden).map((column) => { - const isGroup = isGroupColumn(column); - const isJsonCell = isJsonColumn(column); - const Cell = isGroup ? TableGroupCell : TableCell; - return ( - - ); - })} -
- ); -}); diff --git a/vuu-ui/packages/vuu-table/src/table/cell-renderers/index.ts b/vuu-ui/packages/vuu-table/src/table/cell-renderers/index.ts deleted file mode 100644 index 9ef695dbf..000000000 --- a/vuu-ui/packages/vuu-table/src/table/cell-renderers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./json-cell"; diff --git a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.css b/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.css deleted file mode 100644 index b72fdabb4..000000000 --- a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.css +++ /dev/null @@ -1,23 +0,0 @@ -.vuuJsonCell-group { - align-items: center; - display: inline-flex; - height: calc(var(--vuuTable-rowHeight) - 1px); - width: 100%; -} - -.vuuJsonCell-toggle { - --vuu-icon-color: var(--salt-text-primary-foreground); - --vuu-icon-height: calc(var(--vuuTable-rowHeight) - 1px); - --vuu-icon-width: 18px; - flex-shrink: 0; - margin-left: auto; -} - -.vuuJsonCell-name { - font-weight: var(--salt-typography-fontWeight-semiBold); -} - -.vuuJsonCell-value { - overflow: hidden; - text-overflow: ellipsis; -} \ No newline at end of file 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 deleted file mode 100644 index 9418ea6ec..000000000 --- a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/JsonCell.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { TableCellProps } from "@finos/vuu-datagrid-types"; -import cx from "classnames"; -import { - isJsonAttribute, - metadataKeys, - registerComponent, -} from "@finos/vuu-utils"; - -import "./JsonCell.css"; - -const classBase = "vuuJsonCell"; - -const { IS_EXPANDED, KEY } = metadataKeys; - -const localKey = (key: string) => { - const pos = key.lastIndexOf("|"); - if (pos === -1) { - return ""; - } else { - return key.slice(pos + 1); - } -}; - -const JsonCell = ({ column, row }: TableCellProps) => { - const { key: columnKey /*, type, valueFormatter */ } = column; - let value = row[columnKey]; - let isToggle = false; - if (isJsonAttribute(value)) { - value = value.slice(0, -1); - isToggle = true; - } - const rowKey = localKey(row[KEY]); - const className = cx({ - [`${classBase}-name`]: rowKey === value, - [`${classBase}-value`]: rowKey !== value, - [`${classBase}-group`]: isToggle, - }); - - if (isToggle) { - const toggleIcon = row[IS_EXPANDED] ? "minus-box" : "plus-box"; - return ( - - {value} - - - ); - } else if (value) { - return {value}; - } else { - return null; - } -}; - -registerComponent("json", JsonCell, "cell-renderer", { - description: "JSON formatter", - label: "JSON formatter", - serverDataType: "json", -}); diff --git a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/index.ts b/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/index.ts deleted file mode 100644 index 80cffed5f..000000000 --- a/vuu-ui/packages/vuu-table/src/table/cell-renderers/json-cell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./JsonCell"; diff --git a/vuu-ui/packages/vuu-table/src/table/context-menu/buildContextMenuDescriptors.ts b/vuu-ui/packages/vuu-table/src/table/context-menu/buildContextMenuDescriptors.ts deleted file mode 100644 index b992a3225..000000000 --- a/vuu-ui/packages/vuu-table/src/table/context-menu/buildContextMenuDescriptors.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { DataSource } from "@finos/vuu-data"; -import { ContextMenuItemDescriptor, MenuBuilder } from "@finos/vuu-data-types"; -import { KeyedColumnDescriptor, PinLocation } from "@finos/vuu-datagrid-types"; -import { Filter } from "@finos/vuu-filter-types"; -import { isNumericColumn } from "@finos/vuu-utils"; - -export type ContextMenuLocation = "header" | "filter" | "grid"; - -type MaybeColumn = { column?: KeyedColumnDescriptor }; -type MaybeFilter = { filter?: Filter }; - -export const buildContextMenuDescriptors = - (dataSource?: DataSource): MenuBuilder => - (location, options) => { - const descriptors: ContextMenuItemDescriptor[] = []; - if (dataSource === undefined) { - return descriptors; - } - //TODO which should it be ? - if (location === "header" || location === "column-menu") { - descriptors.push( - ...buildSortMenuItems(options as MaybeColumn, dataSource) - ); - descriptors.push( - ...buildGroupMenuItems(options as MaybeColumn, dataSource) - ); - descriptors.push( - ...buildAggregationMenuItems(options as MaybeColumn, dataSource) - ); - descriptors.push(...buildColumnDisplayMenuItems(options as MaybeColumn)); - descriptors.push({ - action: "column-settings", - icon: "cog", - label: `Column Settings`, - options, - }); - descriptors.push({ - action: "table-settings", - icon: "cog", - label: `DataGrid Settings`, - options, - }); - } else if (location === "filter") { - const { column, filter } = options as MaybeFilter & MaybeColumn; - const colIsOnlyFilter = filter?.column === column?.name; - descriptors.push({ - label: "Edit filter", - action: "filter-edit", - options, - }); - - descriptors.push({ - label: "Remove filter", - action: "filter-remove-column", - options, - }); - - if (column && !colIsOnlyFilter) { - // TODO col might still be the only column in the filter if it is - // involved in all clauses - descriptors.push({ - label: `Remove all filters`, - action: "remove-filters", - options, - }); - } - } - - // if (options?.selectedRowCount){ - // // TODO pass the table name - // const rpcActions = getRpcActions(); - // for (let {label, method} of rpcActions){ - // descriptors.push({action: Action.RpcCall, label, options: {method}}) - // } - // } - - return descriptors; - }; - -function buildSortMenuItems( - options: MaybeColumn, - { sort: { sortDefs } }: DataSource -): ContextMenuItemDescriptor[] { - const { column } = options; - const menuItems: ContextMenuItemDescriptor[] = []; - if (column === undefined) { - return menuItems; - } - - const hasSort = sortDefs.length > 0; - - if (column.sorted === "A") { - menuItems.push({ - label: "Reverse Sort (DSC)", - action: "sort-dsc", - options, - }); - } else if (column.sorted === "D") { - menuItems.push({ - label: "Reverse Sort (ASC)", - action: "sort-asc", - options, - }); - } else if (typeof column.sorted === "number") { - if (column.sorted > 0) { - menuItems.push({ - label: "Reverse Sort (DSC)", - action: "sort-add-dsc", - options, - }); - } else { - menuItems.push({ - label: "Reverse Sort (ASC)", - action: "sort-add-asc", - options, - }); - } - - // removing the last column from a sort would be a no-op, so pointless - if (hasSort && Math.abs(column.sorted) < sortDefs.length) { - menuItems.push({ - label: "Remove from sort", - action: "sort-remove", - options, - }); - } - - menuItems.push({ - label: "New Sort", - children: [ - { label: "Ascending", action: "sort-asc", options }, - { label: "Descending", action: "sort-dsc", options }, - ], - }); - } else if (hasSort) { - menuItems.push({ - label: "Add to sort", - children: [ - { label: "Ascending", action: "sort-add-asc", options }, - { label: "Descending", action: "sort-add-dsc", options }, - ], - }); - menuItems.push({ - label: "New Sort", - children: [ - { label: "Ascending", action: "sort-asc", options }, - { label: "Descending", action: "sort-dsc", options }, - ], - }); - } else { - menuItems.push({ - label: "Sort", - children: [ - { label: "Ascending", action: "sort-asc", options }, - { label: "Descending", action: "sort-dsc", options }, - ], - }); - } - return menuItems; -} - -function buildAggregationMenuItems( - options: MaybeColumn, - dataSource: DataSource -): ContextMenuItemDescriptor[] { - const { column } = options; - if (column === undefined || dataSource.groupBy.length === 0) { - return []; - } - const { name, label = name } = column; - - return [ - { - label: `Aggregate ${label}`, - children: [ - { label: "Count", action: "agg-count", options }, - { label: "Distinct", action: "agg-distinct", options }, - ].concat( - isNumericColumn(column) - ? [ - { label: "Sum", action: "agg-sum", options }, - { label: "Avg", action: "agg-avg", options }, - { label: "High", action: "agg-high", options }, - { label: "Low", action: "agg-low", options }, - ] - : [] - ), - }, - ]; -} - -const pinColumn = (options: unknown, pinLocation: PinLocation) => - ({ - label: `Pin ${pinLocation}`, - action: `column-pin-${pinLocation}`, - options, - } as ContextMenuItemDescriptor); - -const pinLeft = (options: unknown) => pinColumn(options, "left"); -const pinFloating = (options: unknown) => pinColumn(options, "floating"); -const pinRight = (options: unknown) => pinColumn(options, "right"); - -function buildColumnDisplayMenuItems( - options: MaybeColumn -): ContextMenuItemDescriptor[] { - const { column } = options; - if (column === undefined) { - return []; - } - const { pin } = column; - - const menuItems: ContextMenuItemDescriptor[] = [ - { - label: `Hide column`, - action: "column-hide", - options, - }, - { - label: `Remove column`, - action: "column-remove", - options, - }, - ]; - - if (pin === undefined) { - menuItems.push({ - label: `Pin column`, - children: [pinLeft(options), pinFloating(options), pinRight(options)], - }); - } else if (pin === "left") { - menuItems.push( - { label: "Unpin column", action: "column-unpin", options }, - { - label: `Pin column`, - children: [pinFloating(options), pinRight(options)], - } - ); - } else if (pin === "right") { - menuItems.push( - { label: "Unpin column", action: "column-unpin", options }, - { - label: `Pin column`, - children: [pinLeft(options), pinFloating(options)], - } - ); - } else if (pin === "floating") { - menuItems.push( - { label: "Unpin column", action: "column-unpin", options }, - { - label: `Pin column`, - children: [pinLeft(options), pinRight(options)], - } - ); - } - - return menuItems; -} - -function buildGroupMenuItems( - options: MaybeColumn, - { groupBy }: DataSource -): ContextMenuItemDescriptor[] { - const { column } = options; - const menuItems: ContextMenuItemDescriptor[] = []; - if (column === undefined) { - return menuItems; - } - - const { name, label = name } = column; - - if (groupBy.length === 0) { - menuItems.push({ - label: `Group by ${label}`, - action: "group", - options, - }); - } else { - menuItems.push({ - label: `Add ${label} to group by`, - action: "group-add", - options, - }); - } - - return menuItems; -} diff --git a/vuu-ui/packages/vuu-table/src/table/context-menu/index.ts b/vuu-ui/packages/vuu-table/src/table/context-menu/index.ts deleted file mode 100644 index 8c6db9865..000000000 --- a/vuu-ui/packages/vuu-table/src/table/context-menu/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./buildContextMenuDescriptors"; -export * from "./useTableContextMenu"; diff --git a/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts deleted file mode 100644 index fc3813a85..000000000 --- a/vuu-ui/packages/vuu-table/src/table/context-menu/useTableContextMenu.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable no-sequences */ -import { DataSource } from "@finos/vuu-data"; -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { Filter } from "@finos/vuu-filter-types"; -import { removeColumnFromFilter } from "@finos/vuu-utils"; -import { VuuFilter } from "@finos/vuu-protocol-types"; -import { DataSourceFilter, MenuActionHandler } from "@finos/vuu-data-types"; -import { PersistentColumnAction } from "../useTableModel"; -import { - addGroupColumn, - addSortColumn, - AggregationType, - setAggregations, - setSortColumn, -} from "@finos/vuu-utils"; - -export interface ContextMenuOptions { - column?: KeyedColumnDescriptor; - filter?: Filter; - sort?: VuuFilter; -} -export interface ContextMenuHookProps { - dataSource?: DataSource; - /** - * A persistent Column Operation is any manipulation of a table column that should be - * persisted across user sessions. e.g. if user pins a column, column should still be - * pinned next time user opens app. - */ - onPersistentColumnOperation: (action: PersistentColumnAction) => void; -} - -const removeFilterColumn = ( - dataSourceFilter: DataSourceFilter, - column: KeyedColumnDescriptor -) => { - if (dataSourceFilter.filterStruct && column) { - const [filterStruct, filter] = removeColumnFromFilter( - column, - dataSourceFilter.filterStruct - ); - return { - filter, - filterStruct, - }; - } else { - return dataSourceFilter; - } -}; - -const { Average, Count, Distinct, High, Low, Sum } = AggregationType; - -export const useTableContextMenu = ({ - dataSource, - onPersistentColumnOperation, -}: ContextMenuHookProps) => { - /** return {boolean} used by caller to determine whether to forward to additional installed context menu handlers */ - const handleContextMenuAction: MenuActionHandler = (action): boolean => { - const gridOptions = action.options as ContextMenuOptions; - if (gridOptions.column && dataSource) { - const { column } = gridOptions; - // prettier-ignore - switch(action.menuId){ - case "sort-asc": return (dataSource.sort = setSortColumn(dataSource.sort, column, "A")), true; - case "sort-dsc": return (dataSource.sort = setSortColumn(dataSource.sort, column, "D")), true; - case "sort-add-asc": return (dataSource.sort = addSortColumn(dataSource.sort, column, "A")), true; - case "sort-add-dsc": return (dataSource.sort = addSortColumn(dataSource.sort, column, "D")), true; - case "group": return (dataSource.groupBy = addGroupColumn(dataSource.groupBy, column)), true; - case "group-add": return (dataSource.groupBy = addGroupColumn(dataSource.groupBy, column)), true; - case "column-hide": return onPersistentColumnOperation({type: "hideColumns", columns: [column]}), true; - case "column-remove": return (dataSource.columns = dataSource.columns.filter(name => name !== column.name)), true - case "filter-remove-column": return (dataSource.filter = removeFilterColumn(dataSource.filter, column)), true; - case "remove-filters": return (dataSource.filter = {filter:""}), true; - case "agg-avg": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, Average)), true; - case "agg-high": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, High)), true; - case "agg-low": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, Low)), true; - case "agg-count": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, Count)), true; - case "agg-distinct": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, Distinct)), true; - case "agg-sum": return dataSource.aggregations = (setAggregations(dataSource.aggregations, column, Sum)), true; - case "column-pin-floating": return onPersistentColumnOperation({type: "pinColumn", column, pin: "floating"}), true; - case "column-pin-left": return onPersistentColumnOperation({type: "pinColumn", column, pin: "left"}), true; - case "column-pin-right": return onPersistentColumnOperation({type: "pinColumn", column, pin: "right"}), true; - case "column-unpin": return onPersistentColumnOperation({type: "pinColumn", column, pin: undefined}), true - case "column-settings": return onPersistentColumnOperation({type: "columnSettings", column}), true - case "table-settings": return onPersistentColumnOperation({type: "tableSettings"}), true - default: - } - } - return false; - }; - - return handleContextMenuAction; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts b/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts deleted file mode 100644 index 2468fdb3d..000000000 --- a/vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - DataSource, - SchemaColumn, - VuuFeatureInvocationMessage, -} from "@finos/vuu-data"; -import { DataSourceRow } from "@finos/vuu-data-types"; -import { - KeyedColumnDescriptor, - RowClickHandler, - SelectionChangeHandler, - TableConfig, - TableHeadings, - TableRowClickHandler, - TableSelectionModel, -} from "@finos/vuu-datagrid-types"; -import { MeasuredContainerProps } from "@finos/vuu-layout"; -import { DragStartHandler, dragStrategy } from "@finos/vuu-ui-controls"; -import { FC, MouseEvent } from "react"; -import { RowProps } from "../table-next/Row"; - -// TODO implement a Model object to represent a row data for better API -export type TableRowSelectHandler = (row: DataSourceRow) => void; - -export type TableNavigationStyle = "none" | "cell" | "row"; - -export interface TableProps - extends Omit { - Row?: FC; - allowConfigEditing?: boolean; - allowDragDrop?: boolean | dragStrategy; - /** - * required if a fully featured column picker is to be available - */ - availableColumns?: SchemaColumn[]; - config: TableConfig; - dataSource: DataSource; - disableFocus?: boolean; - headerHeight?: number; - /** - * Defined how focus navigation within data cells will be handled by table. - * Default is cell. - */ - highlightedIndex?: number; - navigationStyle?: TableNavigationStyle; - /** - * required if a fully featured column picker is to be available. - * Available columns can be changed by the addition or removal of - * one or more calculated columns. - */ - onAvailableColumnsChange?: (columns: SchemaColumn[]) => void; - /** - * This callback will be invoked any time a config attribute of TableConfig - * is changed. By persisting this value and providing it to the Table as a - * prop, table state can be persisted across sessions. - */ - onConfigChange?: (config: TableConfig) => void; - onDragStart?: DragStartHandler; - onDrop?: () => void; - /** - * When a Vuu feature e.g. context menu action, has been invoked, the Vuu server - * response must be handled. This callback provides that response. - */ - onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; - - onHighlight?: (idx: number) => void; - /** - * callback invoked when user 'clicks' a table row. CLick triggered either - * via mouse click or keyboard (default ENTER); - */ - onRowClick?: TableRowClickHandler; - onShowConfigEditor?: () => void; - onSelect?: TableRowSelectHandler; - onSelectionChange?: SelectionChangeHandler; - renderBufferSize?: number; - rowHeight?: number; - /** - * Selection Bookends style the left and right edge of a selection block. - * They are optional, value defaults to zero. - * TODO this should just live in CSS - */ - selectionBookendWidth?: number; - selectionModel?: TableSelectionModel; - /** - * if false, table rendered without headers. Useful when table is being included in a - * composite component. - */ - showColumnHeaders?: boolean; -} - -export type TableColumnResizeHandler = ( - phase: "begin" | "resize" | "end", - columnName: string, - width?: number -) => void; - -export interface TableImplementationProps { - columns: KeyedColumnDescriptor[]; - columnsWithinViewport: KeyedColumnDescriptor[]; - data: DataSourceRow[]; - getRowOffset: (row: DataSourceRow) => number; - headerHeight: number; - headings: TableHeadings; - onColumnResize?: TableColumnResizeHandler; - onHeaderCellDragEnd?: () => void; - onHeaderCellDragStart?: (evt: MouseEvent) => void; - onContextMenu?: (evt: MouseEvent) => void; - onRemoveColumnFromGroupBy?: (column?: KeyedColumnDescriptor) => void; - onRowClick?: RowClickHandler; - onSort: (column: KeyedColumnDescriptor, isAdditive: boolean) => void; - onToggleGroup?: (row: DataSourceRow, column: KeyedColumnDescriptor) => void; - tableId: string; - virtualColSpan?: number; - rowCount: number; -} - -type MeasureStatus = "unmeasured" | "measured"; - -export interface TableMeasurements { - contentHeight: number; - left: number; - right: number; - scrollbarSize: number; - scrollContentHeight: number; - status: MeasureStatus; - top: number; -} - -export interface Viewport { - maxScrollContainerScrollHorizontal: number; - maxScrollContainerScrollVertical: number; - pinnedWidthLeft: number; - rowCount: number; - // contentWidth: number; -} diff --git a/vuu-ui/packages/vuu-table/src/table/filter-indicator.css b/vuu-ui/packages/vuu-table/src/table/filter-indicator.css deleted file mode 100644 index e0b220220..000000000 --- a/vuu-ui/packages/vuu-table/src/table/filter-indicator.css +++ /dev/null @@ -1,15 +0,0 @@ -.vuuFilterIndicator { - --menu-icon-size: 12px; - --menu-item-icon-color: black; - align-items: center; - cursor: pointer; - display: flex; - flex: 0 0 18px; - flex-direction: column; - justify-content: center; - position: relative; -} - -.vuuFilterIndicator + .vuuTable-headerCell-inner { - padding-left: 0; -} diff --git a/vuu-ui/packages/vuu-table/src/table/filter-indicator.tsx b/vuu-ui/packages/vuu-table/src/table/filter-indicator.tsx deleted file mode 100644 index 32dd74d83..000000000 --- a/vuu-ui/packages/vuu-table/src/table/filter-indicator.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { Filter } from "@finos/vuu-filter-types"; -import { useContextMenu } from "@finos/vuu-popups"; -import cx from "classnames"; -import { HTMLAttributes, useCallback } from "react"; - -import "./filter-indicator.css"; - -export const Direction = { - ASC: "asc", - DSC: "dsc", -}; - -export interface FilterIndicatorProps extends HTMLAttributes { - column: KeyedColumnDescriptor; - filter?: Filter; -} - -export const FilterIndicator = ({ column, filter }: FilterIndicatorProps) => { - //TODO handle this at header level - const [showContextMenu] = useContextMenu(); - - const handleClick = useCallback( - (evt) => { - // if we do this through keyboard, need to get co-ords - evt.stopPropagation(); - showContextMenu(evt, "filter", { column, filter }); - }, - [column, filter, showContextMenu] - ); - - if (!column.filter) { - return null; - } - - return ( -
- ); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/index.ts b/vuu-ui/packages/vuu-table/src/table/index.ts deleted file mode 100644 index 27c881808..000000000 --- a/vuu-ui/packages/vuu-table/src/table/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "./ColumnResizer"; -export * from "./context-menu"; -export * from "./dataTableTypes"; -export * from "./Table"; -export * from "./useMeasuredContainer"; -export * from "./useSelection"; -export * from "./useTableColumnResize"; -export * from "./useTableModel"; -export * from "../table-next/useTableViewport"; -export * from "./cell-renderers"; diff --git a/vuu-ui/packages/vuu-table/src/table/keyUtils.ts b/vuu-ui/packages/vuu-table/src/table/keyUtils.ts deleted file mode 100644 index fcbd5b1e7..000000000 --- a/vuu-ui/packages/vuu-table/src/table/keyUtils.ts +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; - -function union(set1: Set, ...sets: Set[]) { - const result = new Set(set1); - for (let set of sets) { - for (let element of set) { - result.add(element); - } - } - return result; -} - -export const ArrowUp = "ArrowUp"; -export const ArrowDown = "ArrowDown"; -export const ArrowLeft = "ArrowLeft"; -export const ArrowRight = "ArrowRight"; -export const Enter = "Enter"; -export const Escape = "Escape"; -export const Home = "Home"; -export const End = "End"; -export const PageUp = "PageUp"; -export const PageDown = "PageDown"; -export const Space = " "; -export const Tab = "Tab"; - -const actionKeys = new Set(["Enter", "Delete", " "]); -const focusKeys = new Set(["Tab"]); -const arrowLeftRightKeys = new Set(["ArrowRight", "ArrowLeft"]); -const navigationKeys = new Set([ - Home, - End, - PageUp, - PageDown, - ArrowDown, - ArrowLeft, - ArrowRight, - ArrowUp, -]); -const functionKeys = new Set([ - "F1", - "F2", - "F3", - "F4", - "F5", - "F6", - "F7", - "F8", - "F9", - "F10", - "F11", - "F12", -]); -const specialKeys = union( - actionKeys, - navigationKeys, - arrowLeftRightKeys, - functionKeys, - focusKeys -); -export const isCharacterKey = (evt: React.KeyboardEvent): boolean => { - if (specialKeys.has(evt.key)) { - return false; - } - return evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey && !evt.altKey; -}; - -export type ArrowKey = "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"; -export type PageKey = "Home" | "End" | "PageUp" | "PageDown"; -export type NavigationKey = PageKey | ArrowKey; -const PageKeys = ["Home", "End", "PageUp", "PageDown"]; -export const isPagingKey = (key: string): key is PageKey => - PageKeys.includes(key); - -export const isNavigationKey = (key: string): key is NavigationKey => { - return navigationKeys.has(key as NavigationKey); -}; diff --git a/vuu-ui/packages/vuu-table/src/table/test.svg b/vuu-ui/packages/vuu-table/src/table/test.svg deleted file mode 100644 index f7bc211c9..000000000 --- a/vuu-ui/packages/vuu-table/src/table/test.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts b/vuu-ui/packages/vuu-table/src/table/useDataSource.ts deleted file mode 100644 index 8599987bf..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useDataSource.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { - DataSource, - DataSourceConfigMessage, - DataSourceSubscribedMessage, - isVuuFeatureAction, - isVuuFeatureInvocation, - SubscribeCallback, - VuuFeatureInvocationMessage, - VuuFeatureMessage, -} from "@finos/vuu-data"; -import { DataSourceRow } from "@finos/vuu-data-types"; - -import { VuuRange, VuuSortCol } from "@finos/vuu-protocol-types"; -import { - getFullRange, - isRowSelectedLast, - metadataKeys, - WindowRange, -} from "@finos/vuu-utils"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -const { SELECTED } = metadataKeys; - -export type SubscriptionDetails = { - columnNames?: string[]; - range: { from: number; to: number }; - sort?: VuuSortCol[]; -}; - -export interface DataSourceHookProps { - dataSource: DataSource; - onConfigChange?: (message: DataSourceConfigMessage) => void; - onFeatureEnabled?: (message: VuuFeatureMessage) => void; - onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; - onSizeChange: (size: number) => void; - onSubscribed: (subscription: DataSourceSubscribedMessage) => void; - range?: VuuRange; - renderBufferSize?: number; - viewportRowCount: number; -} - -//TODO allow subscription details to be set before subscribe call -export function useDataSource({ - dataSource, - onConfigChange, - onFeatureEnabled, - onFeatureInvocation, - onSizeChange, - onSubscribed, - range = { from: 0, to: 0 }, - renderBufferSize = 0, - viewportRowCount, -}: DataSourceHookProps) { - const [, forceUpdate] = useState(null); - const isMounted = useRef(true); - const hasUpdated = useRef(false); - const rangeRef = useRef({ from: 0, to: 0 }); - const rafHandle = useRef(null); - const data = useRef([]); - - const dataWindow = useMemo( - () => new MovingWindow(getFullRange(range)), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const setData = useCallback( - (updates: DataSourceRow[]) => { - for (const row of updates) { - dataWindow.add(row); - } - data.current = dataWindow.data; - hasUpdated.current = true; - }, - [dataWindow] - ); - - const datasourceMessageHandler: SubscribeCallback = useCallback( - (message) => { - if (message.type === "subscribed") { - onSubscribed?.(message); - } else if (message.type === "viewport-update") { - if (typeof message.size === "number") { - onSizeChange?.(message.size); - dataWindow.setRowCount(message.size); - } - if (message.rows) { - setData(message.rows); - } else if (typeof message.size === "number") { - data.current = dataWindow.data; - hasUpdated.current = true; - } - } else if (isVuuFeatureAction(message)) { - onFeatureEnabled?.(message); - } else if (isVuuFeatureInvocation(message)) { - onFeatureInvocation?.(message); - } else { - console.log(`useDataSource unexpected message ${message.type}`); - } - }, - [ - dataWindow, - onFeatureEnabled, - onFeatureInvocation, - onSizeChange, - onSubscribed, - setData, - ] - ); - - useEffect( - () => () => { - if (rafHandle.current) { - cancelAnimationFrame(rafHandle.current); - rafHandle.current = null; - } - isMounted.current = false; - }, - [] - ); - - const refreshIfUpdated = useCallback(() => { - if (isMounted.current) { - if (hasUpdated.current) { - forceUpdate({}); - hasUpdated.current = false; - } - rafHandle.current = requestAnimationFrame(refreshIfUpdated); - } - }, [forceUpdate]); - - useEffect(() => { - rafHandle.current = requestAnimationFrame(refreshIfUpdated); - }, [refreshIfUpdated]); - - const adjustRange = useCallback( - (rowCount: number) => { - const { from } = dataSource.range; - const rowRange = { from, to: from + rowCount }; - const fullRange = getFullRange(rowRange, renderBufferSize); - dataWindow.setRange(fullRange); - dataSource.range = rangeRef.current = fullRange; - // seems a bit naughty to emit from outside, but the datasource doesn't - // know about the buffer size we add to the base range - dataSource.emit("range", rowRange); - }, - [dataSource, dataWindow, renderBufferSize] - ); - - const setRange = useCallback( - (range: VuuRange) => { - const fullRange = getFullRange(range, renderBufferSize); - dataWindow.setRange(fullRange); - dataSource.range = rangeRef.current = fullRange; - dataSource.emit("range", range); - }, - [dataSource, dataWindow, renderBufferSize] - ); - - const getSelectedRows = useCallback(() => { - return dataWindow.getSelectedRows(); - }, [dataWindow]); - - // Note: we do not call unsubscribe in a cleanup function here. - // Thats because we do not want to unsubscribe in the event that - // our view is unmounts due to a layout drag drop operation. In - // that scenario, we disable the viewport. This is handles at the - // View level. Might need to revisit this - what if Table is not - // nested within a View ? - - useEffect(() => { - dataSource?.subscribe( - { - range: rangeRef.current, - }, - datasourceMessageHandler - ); - }, [dataSource, datasourceMessageHandler, onConfigChange]); - - useEffect(() => { - console.log(`adjust range as rowCount chnaged ${viewportRowCount}`); - adjustRange(viewportRowCount); - }, [adjustRange, viewportRowCount]); - - return { - data: data.current, - getSelectedRows, - range: rangeRef.current, - setRange, - dataSource, - }; -} - -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(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; - - // assign 'pre-selected' selection state. This allows us to assign a className - // to a non selected row that immediately precedes a selected row. Useful for - // styling. This cannot be achieved any other way as document order of row - // elements does not necessarily reflect data order. - const isSelected = data[SELECTED]; - const preSelected = this.data[internalIndex - 1]?.[SELECTED]; - if (preSelected === 0 && isSelected) { - this.data[internalIndex - 1][SELECTED] = 2; - } else if (preSelected === 2 && !isSelected) { - this.data[internalIndex - 1][SELECTED] = 0; - } else if ( - isRowSelectedLast(this.data[internalIndex - 1]) && - isSelected - ) { - this.data[internalIndex - 1][SELECTED] -= 4; - } - } - } - - 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-table/src/table/useDraggableColumn.ts b/vuu-ui/packages/vuu-table/src/table/useDraggableColumn.ts deleted file mode 100644 index 44c461f6f..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useDraggableColumn.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useDragDropNext as useDragDrop } from "@finos/vuu-ui-controls"; -import { MouseEvent, useCallback, useRef } from "react"; - -type MousePos = { - clientX: number; - clientY: number; - idx: string; -}; - -export interface DraggableColumnHookProps { - onDrop: (fromIndex: number, toIndex: number) => void; -} - -export const useDraggableColumn = ({ onDrop }: DraggableColumnHookProps) => { - const mousePosRef = useRef(); - const containerRef = useRef(null); - - const handleDropSettle = useCallback(() => { - console.log(`handleDropSettle`); - mousePosRef.current = undefined; - containerRef.current = null; - }, []); - - const { draggable, draggedItemIndex, onMouseDown } = useDragDrop({ - // allowDragDrop: "drop-indicator", - allowDragDrop: true, - draggableClassName: "vuuTable-headerCell", - orientation: "horizontal", - containerRef, - itemQuery: ".vuuTable-headerCell", - onDrop, - onDropSettle: handleDropSettle, - }); - - const onHeaderCellDragStart = useCallback( - (evt: MouseEvent) => { - const { clientX, clientY } = evt; - console.log( - `useDraggableColumn handleHeaderCellDragStart means mouseDown fired on a column in RowBasedTable` - ); - const sourceElement = evt.target as HTMLElement; - const columnHeaderCell = sourceElement.closest(".vuuTable-headerCell"); - containerRef.current = columnHeaderCell?.closest( - "[role='row']" - ) as HTMLDivElement; - const { - dataset: { idx = "-1" }, - } = columnHeaderCell as HTMLElement; - mousePosRef.current = { - clientX, - clientY, - idx, - }; - onMouseDown?.(evt); - }, - [onMouseDown] - ); - - // useLayoutEffect(() => { - // if (tableLayout === "column" && mousePosRef.current && !draggable) { - // const { clientX, clientY, idx } = mousePosRef.current; - // const target = tableContainerRef.current?.querySelector( - // `.vuuTable-table[data-idx="${idx}"]` - // ) as HTMLElement; - // if (target) { - // const evt = { - // persist: () => undefined, - // nativeEvent: { - // clientX, - // clientY, - // target, - // }, - // }; - // onMouseDown?.(evt as unknown as MouseEvent); - // } - // } - // }, [draggable, onMouseDown, tableContainerRef, tableLayout]); - - return { - draggable, - draggedItemIndex, - onHeaderCellDragStart, - }; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/table/useKeyboardNavigation.ts deleted file mode 100644 index c7b4f3d8a..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useKeyboardNavigation.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { DataSourceRow } from "@finos/vuu-data-types"; -import { VuuRange } from "@finos/vuu-protocol-types"; -import { withinRange } from "@finos/vuu-utils"; -import { - KeyboardEvent, - MouseEvent, - RefObject, - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, -} from "react"; -import { - ArrowDown, - ArrowKey, - ArrowLeft, - ArrowRight, - ArrowUp, - End, - Home, - isNavigationKey, - isPagingKey, - NavigationKey, - PageDown, - PageUp, -} from "./keyUtils"; -import { ScrollRequestHandler } from "./useTableScroll"; - -export type CellPos = [number, number]; - -const headerCellQuery = (colIdx: number) => - `.vuuTable-headers .vuuTable-headerCell:nth-child(${colIdx + 1})`; -const dataCellQuery = (rowIdx: number, colIdx: number) => - `.vuuTable-body > [aria-rowindex='${rowIdx}'] > [role='cell']:nth-child(${ - colIdx + 1 - })`; - -const NULL_CELL_POS: CellPos = [-1, -1]; - -function nextCellPos( - key: ArrowKey, - [rowIdx, colIdx]: CellPos, - columnCount: number, - rowCount: number -): CellPos { - if (key === ArrowUp) { - if (rowIdx > -1) { - return [rowIdx - 1, colIdx]; - } else { - return [rowIdx, colIdx]; - } - } else if (key === ArrowDown) { - if (rowIdx === -1) { - return [0, colIdx]; - } else if (rowIdx === rowCount - 1) { - return [rowIdx, colIdx]; - } else { - return [rowIdx + 1, colIdx]; - } - } else if (key === ArrowRight) { - if (colIdx < columnCount - 1) { - return [rowIdx, colIdx + 1]; - } else { - return [rowIdx, colIdx]; - } - } else if (key === ArrowLeft) { - if (colIdx > 0) { - return [rowIdx, colIdx - 1]; - } else { - return [rowIdx, colIdx]; - } - } - return [rowIdx, colIdx]; -} - -export interface NavigationHookProps { - containerRef: RefObject; - columnCount?: number; - data: DataSourceRow[]; - disableHighlightOnFocus?: boolean; - label?: string; - viewportRange: VuuRange; - requestScroll?: ScrollRequestHandler; - restoreLastFocus?: boolean; - rowCount?: number; - selected?: unknown; -} - -export const useKeyboardNavigation = ({ - columnCount = 0, - containerRef, - disableHighlightOnFocus, - data, - requestScroll, - rowCount = 0, - viewportRange, -}: NavigationHookProps) => { - const { from: viewportFirstRow, to: viewportLastRow } = viewportRange; - const focusedCellPos = useRef([-1, -1]); - const focusableCell = useRef(); - const activeCellPos = useRef([-1, 0]); - - const getTableCell = useCallback( - ([rowIdx, colIdx]: CellPos) => { - const cssQuery = - rowIdx === -1 ? headerCellQuery(colIdx) : dataCellQuery(rowIdx, colIdx); - return containerRef.current?.querySelector( - cssQuery - ) as HTMLTableCellElement; - }, - [containerRef] - ); - - const getFocusedCell = (element: HTMLElement | Element | null) => - element?.closest( - "[role='columnHeader'],[role='cell']" - ) as HTMLTableCellElement | null; - - const getTableCellPos = (tableCell: HTMLTableCellElement): CellPos => { - if (tableCell.role === "columnHeader") { - const colIdx = parseInt(tableCell.dataset.idx ?? "-1", 10); - return [-1, colIdx]; - } else { - const focusedRow = tableCell.closest("[role='row']"); - if (focusedRow) { - const rowIdx = parseInt(focusedRow.ariaRowIndex ?? "-1", 10); - // TODO will get trickier when we introduce horizontal virtualisation - const colIdx = Array.from(focusedRow.childNodes).indexOf(tableCell); - return [rowIdx, colIdx]; - } - } - return NULL_CELL_POS; - }; - - const focusCell = useCallback( - (cellPos: CellPos) => { - if (containerRef.current) { - const activeCell = getTableCell(cellPos); - if (activeCell) { - if (activeCell !== focusableCell.current) { - focusableCell.current?.setAttribute("tabindex", ""); - focusableCell.current = activeCell; - activeCell.setAttribute("tabindex", "0"); - } - activeCell.focus(); - } else if (!withinRange(cellPos[0], viewportRange)) { - focusableCell.current = undefined; - requestScroll?.({ type: "scroll-page", direction: "up" }); - } - } - }, - // TODO we recreate this function whenever viewportRange changes, which will - // be often whilst scrolling - store range in a a ref ? - [containerRef, getTableCell, requestScroll, viewportRange] - ); - - const setActiveCell = useCallback( - (rowIdx: number, colIdx: number, fromKeyboard = false) => { - const pos: CellPos = [rowIdx, colIdx]; - activeCellPos.current = pos; - focusCell(pos); - if (fromKeyboard) { - focusedCellPos.current = pos; - } - }, - [focusCell] - ); - - const virtualizeActiveCell = useCallback(() => { - focusableCell.current?.setAttribute("tabindex", ""); - focusableCell.current = undefined; - }, []); - - const nextPageItemIdx = useCallback( - async ( - key: "PageDown" | "PageUp" | "Home" | "End", - cellPos: CellPos - ): Promise => { - switch (key) { - case PageDown: - requestScroll?.({ type: "scroll-page", direction: "down" }); - break; - case PageUp: - requestScroll?.({ type: "scroll-page", direction: "up" }); - break; - case Home: - requestScroll?.({ type: "scroll-end", direction: "home" }); - break; - case End: - requestScroll?.({ type: "scroll-end", direction: "end" }); - break; - } - // TODO set up a scroll listener here, reset focused cell once scroll completes - return cellPos; - }, - [requestScroll] - ); - - const handleFocus = useCallback(() => { - if (disableHighlightOnFocus !== true) { - if (containerRef.current?.contains(document.activeElement)) { - // IF focus arrives via keyboard, a cell will have received focus, - // we handle that here. If focus arrives via click on a cell with - // no tabindex (i.e all cells except one) we leave that to the - // click handler. - const focusedCell = getFocusedCell(document.activeElement); - if (focusedCell) { - focusedCellPos.current = getTableCellPos(focusedCell); - } - } - } - }, [disableHighlightOnFocus, containerRef]); - - const navigateChildItems = useCallback( - async (key: NavigationKey) => { - const [nextRowIdx, nextColIdx] = isPagingKey(key) - ? await nextPageItemIdx(key, activeCellPos.current) - : nextCellPos(key, activeCellPos.current, columnCount, rowCount); - - const [rowIdx, colIdx] = activeCellPos.current; - if (nextRowIdx !== rowIdx || nextColIdx !== colIdx) { - setActiveCell(nextRowIdx, nextColIdx, true); - } - }, - [columnCount, nextPageItemIdx, rowCount, setActiveCell] - ); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (data.length > 0 && isNavigationKey(e.key)) { - e.preventDefault(); - e.stopPropagation(); - void navigateChildItems(e.key); - } - }, - [data, navigateChildItems] - ); - - const handleClick = useCallback( - // Might not be a cell e.g the Settings button - (evt: MouseEvent) => { - const target = evt.target as HTMLElement; - const focusedCell = getFocusedCell(target); - if (focusedCell) { - const [rowIdx, colIdx] = getTableCellPos(focusedCell); - setActiveCell(rowIdx, colIdx); - } - }, - [setActiveCell] - ); - - const containerProps = useMemo(() => { - return { - onClick: handleClick, - onFocus: handleFocus, - onKeyDown: handleKeyDown, - }; - }, [handleClick, handleFocus, handleKeyDown]); - - useLayoutEffect(() => { - const { current: cellPos } = activeCellPos; - const withinViewport = - cellPos[0] >= viewportFirstRow && cellPos[0] <= viewportLastRow; - - if (focusableCell.current && !withinViewport) { - virtualizeActiveCell(); - } else if (!focusableCell.current && withinViewport) { - focusCell(cellPos); - } - }, [focusCell, viewportFirstRow, viewportLastRow, virtualizeActiveCell]); - - // First render will only render the outer container when explicit - // sizing has not been provided. Outer container is measured and - // only then, on second render, is content rendered. - const fullyRendered = containerRef.current?.firstChild != null; - useEffect(() => { - if (fullyRendered && focusableCell.current === undefined) { - const headerCell = containerRef.current?.querySelector( - headerCellQuery(0) - ) as HTMLTableCellElement; - if (headerCell) { - headerCell.setAttribute("tabindex", "0"); - focusableCell.current = headerCell; - } - } - }, [containerRef, fullyRendered]); - - return containerProps; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useMeasuredContainer.ts b/vuu-ui/packages/vuu-table/src/table/useMeasuredContainer.ts deleted file mode 100644 index cefa2c344..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useMeasuredContainer.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { isValidNumber } from "@finos/vuu-utils"; -import { RefObject, useCallback, useMemo, useRef, useState } from "react"; -import { useResizeObserver, ResizeHandler } from "./useResizeObserver"; - -const ClientWidthHeight = ["clientHeight", "clientWidth"]; - -export interface ClientSize { - clientHeight: number; - clientWidth: number; -} - -export interface MeasuredProps { - defaultHeight?: number; - defaultWidth?: number; - height?: number; - width?: number; -} - -export interface Size { - height: number | "100%"; - width: number | "100%"; -} - -export interface MeasuredSize { - height: number; - width: number; -} - -interface MeasuredState { - css: CssSize; - outer: Size; - inner?: MeasuredSize; -} - -const isNumber = (val: unknown): val is number => Number.isFinite(val); - -export type CssSize = { - height: string; - width: string; -}; -const FULL_SIZE: CssSize = { height: "100%", width: "100%" }; - -export interface MeasuredContainerHookResult { - containerRef: RefObject; - cssSize: CssSize; - outerSize: Size; - innerSize?: MeasuredSize; -} - -// 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, -// inner values will be updated once measured. -const getInitialCssSize = (height: unknown, width: unknown): CssSize => { - if (isValidNumber(height) && isValidNumber(width)) { - return { - height: `${height}px`, - width: `${width}px`, - }; - } else { - return FULL_SIZE; - } -}; - -const getInitialInnerSize = ( - height: unknown, - width: unknown -): MeasuredSize | undefined => { - if (isValidNumber(height) && isValidNumber(width)) { - return { - height, - width, - }; - } -}; - -export const useMeasuredContainer = ({ - defaultHeight = 0, - defaultWidth = 0, - height, - width, -}: MeasuredProps): MeasuredContainerHookResult => { - const containerRef = useRef(null); - const [size, setSize] = useState({ - css: getInitialCssSize(height, width), - inner: getInitialInnerSize(height, width), - outer: { - height: height ?? "100%", - width: width ?? "100%", - }, - }); - - useMemo(() => { - setSize((currentSize) => { - const { inner, outer } = currentSize; - if (isValidNumber(height) && isValidNumber(width) && inner && outer) { - const { height: innerHeight, width: innerWidth } = inner; - const { height: outerHeight, width: outerWidth } = outer; - - if (outerHeight !== height || outerWidth !== width) { - const heightDiff = isValidNumber(outerHeight) - ? outerHeight - innerHeight - : 0; - const widthDiff = isValidNumber(outerWidth) - ? outerWidth - innerWidth - : 0; - return { - ...currentSize, - outer: { height, width }, - inner: { height: height - heightDiff, width: width - widthDiff }, - }; - } - } - return currentSize; - }); - }, [height, width]); - - const onResize: ResizeHandler = useCallback( - ({ clientWidth, clientHeight }: Partial) => { - setSize((currentSize) => { - const { css, inner, outer } = currentSize; - return isNumber(clientHeight) && - isNumber(clientWidth) && - (clientWidth !== inner?.width || clientHeight !== inner?.height) - ? { - css, - outer, - inner: { - width: Math.floor(clientWidth) || defaultWidth, - height: Math.floor(clientHeight) || defaultHeight, - }, - } - : currentSize; - }); - }, - [defaultHeight, defaultWidth] - ); - - useResizeObserver(containerRef, ClientWidthHeight, onResize, true); - - return { - containerRef, - cssSize: size.css, - outerSize: size.outer, - innerSize: size.inner, - }; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useMeasuredSize.ts b/vuu-ui/packages/vuu-table/src/table/useMeasuredSize.ts deleted file mode 100644 index 0bb5d1b01..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useMeasuredSize.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { RefObject, useCallback, useMemo, useState } from "react"; -import { useResizeObserver, ResizeHandler } from "./useResizeObserver"; - -const FullAndClientWidthHeight = [ - "clientHeight", - "clientWidth", - "height", - "width", -]; - -export type Size = { - pixelHeight: number; - pixelWidth: number; - clientHeight?: number; - clientWidth?: number; - height: number | "100%"; - width: number | "100%"; -}; - -export type FullSize = { - clientHeight?: number; - clientWidth?: number; - height: "100%"; - width: "100%"; -}; - -export type ClientSize = { - clientHeight: number; - clientWidth: number; -}; - -export type MeasuredSize = ClientSize & { - height: number | "100%"; - width: number | "100%"; -}; - -export const isMeasured = (size: Size | MeasuredSize): size is MeasuredSize => - typeof size.clientHeight === "number" && typeof size.clientWidth === "number"; - -export const isFullSize = ( - size: Size | MeasuredSize | FullSize -): size is FullSize => size.height === "100%" && size.width === "100%"; - -const isNumber = (val: unknown): val is number => Number.isFinite(val); - -export const useMeasuredSize = ( - containerRef: RefObject, - height?: number | "100%", - width?: number | "100%" -): Size => { - const [size, setSize] = useState({ - pixelHeight: typeof height === "number" ? height : 0, - pixelWidth: typeof width === "number" ? width : 0, - height: height ?? "100%", - width: width ?? "100%", - }); - const onResize: ResizeHandler = useCallback( - ({ clientWidth, clientHeight }: Partial) => { - console.log(`setSize ${clientWidth}`); - setSize((currentSize) => - isNumber(clientHeight) && - isNumber(clientWidth) && - (clientWidth !== currentSize.clientWidth || - clientHeight !== currentSize.clientHeight) - ? { - ...currentSize, - pixelWidth: Math.floor(clientWidth), - pixelHeight: Math.floor(clientHeight), - clientWidth: Math.floor(clientWidth), - clientHeight: Math.floor(clientHeight), - } - : currentSize - ); - }, - [setSize] - ); - - useResizeObserver(containerRef, FullAndClientWidthHeight, onResize, true); - - return size; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useResizeObserver.ts b/vuu-ui/packages/vuu-table/src/table/useResizeObserver.ts deleted file mode 100644 index ac1084cc9..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useResizeObserver.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { RefObject, useCallback, useEffect, useRef } from "react"; - -export const WidthHeight = ["height", "width"]; -export const WidthOnly = ["width"]; - -export type measurements = { - height?: T; - clientHeight?: number; - clientWidth?: number; - contentHeight?: number; - contentWidth?: number; - scrollHeight?: number; - scrollWidth?: number; - width?: T; -}; -type measuredDimension = keyof measurements; - -export type ResizeHandler = (measurements: measurements) => void; - -type observedDetails = { - onResize?: ResizeHandler; - measurements: measurements; -}; -const observedMap = new Map(); - -const getTargetSize = ( - element: HTMLElement, - size: { - height: number; - width: number; - contentHeight: number; - contentWidth: number; - }, - dimension: measuredDimension -): number => { - switch (dimension) { - case "height": - return size.height; - case "clientHeight": - return element.clientHeight; - case "clientWidth": - return element.clientWidth; - case "contentHeight": - return size.contentHeight; - case "contentWidth": - return size.contentWidth; - case "scrollHeight": - return Math.ceil(element.scrollHeight); - case "scrollWidth": - return Math.ceil(element.scrollWidth); - case "width": - return size.width; - default: - return 0; - } -}; - -// TODO should we make this create-on-demand -const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => { - for (const entry of entries) { - const { target, borderBoxSize, contentBoxSize } = entry; - const observedTarget = observedMap.get(target as HTMLElement); - if (observedTarget) { - const [{ blockSize: height, inlineSize: width }] = borderBoxSize; - const [{ blockSize: contentHeight, inlineSize: contentWidth }] = - contentBoxSize; - const { onResize, measurements } = observedTarget; - let sizeChanged = false; - for (const [dimension, size] of Object.entries(measurements)) { - const newSize = getTargetSize( - target as HTMLElement, - { height, width, contentHeight, contentWidth }, - dimension as measuredDimension - ); - - if (newSize !== size) { - sizeChanged = true; - measurements[dimension as measuredDimension] = newSize; - } - } - if (sizeChanged) { - // TODO only return measured sizes - onResize && onResize(measurements); - } - } - } -}); - -// TODO use an optional lag (default to false) to ask to fire onResize -// with initial size -export function useResizeObserver( - ref: RefObject, - dimensions: string[], - onResize: ResizeHandler, - reportInitialSize = false -) { - const dimensionsRef = useRef(dimensions); - - const measure = useCallback((target: HTMLElement): measurements => { - const { width, height } = target.getBoundingClientRect(); - const { clientWidth: contentWidth, clientHeight: contentHeight } = target; - return dimensionsRef.current.reduce( - (map: { [key: string]: number }, dim) => { - map[dim] = getTargetSize( - target, - { width, height, contentHeight, contentWidth }, - dim as measuredDimension - ); - return map; - }, - {} - ); - }, []); - - // TODO use ref to store resizeHandler here - // resize handler registered with REsizeObserver will never change - // use ref to store user onResize callback here - // resizeHandler will call user callback.current - - // Keep this effect separate in case user inadvertently passes different - // dimensions or callback instance each time - we only ever want to - // initiate new observation when ref changes. - useEffect(() => { - const target = ref.current as HTMLElement; - async function registerObserver() { - // Create the map entry immediately. useEffect may fire below - // before fonts are ready and attempt to update entry - observedMap.set(target, { measurements: {} as measurements }); - await document.fonts.ready; - const observedTarget = observedMap.get(target); - if (observedTarget) { - const measurements = measure(target); - observedTarget.measurements = measurements; - resizeObserver.observe(target); - if (reportInitialSize) { - onResize(measurements); - } - } else { - console.log( - `%cuseResizeObserver an target expected to be under observation wa snot found. This warrants investigation`, - "font-weight:bold; color:red;" - ); - } - } - - if (target) { - // TODO might we want multiple callers to attach a listener to the same element ? - if (observedMap.has(target)) { - throw Error( - "useResizeObserver attemping to observe same element twice" - ); - } - // TODO set a pending entry on map - registerObserver(); - } - return () => { - if (target && observedMap.has(target)) { - resizeObserver.unobserve(target); - observedMap.delete(target); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [measure, ref]); - - useEffect(() => { - const target = ref.current as HTMLElement; - const record = observedMap.get(target); - if (record) { - if (dimensionsRef.current !== dimensions) { - dimensionsRef.current = dimensions; - const measurements = measure(target); - record.measurements = measurements; - } - // Might not have changed, but no harm ... - record.onResize = onResize; - } - }, [dimensions, measure, ref, onResize]); -} diff --git a/vuu-ui/packages/vuu-table/src/table/useSelection.ts b/vuu-ui/packages/vuu-table/src/table/useSelection.ts deleted file mode 100644 index c459ac9a5..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useSelection.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - RowClickHandler, - Selection, - SelectionChangeHandler, - TableSelectionModel, -} from "@finos/vuu-datagrid-types"; -import { - deselectItem, - isRowSelected, - metadataKeys, - selectItem, -} from "@finos/vuu-utils"; -import { DataSourceRow } from "@finos/vuu-data-types"; -import { useCallback, useRef } from "react"; - -const { IDX } = metadataKeys; - -const NO_SELECTION: Selection = []; - -export interface SelectionHookProps { - selectionModel: TableSelectionModel; - onSelect?: (row: DataSourceRow) => void; - onSelectionChange: SelectionChangeHandler; -} - -export const useSelection = ({ - selectionModel, - onSelect, - onSelectionChange, -}: SelectionHookProps) => { - selectionModel === "extended" || selectionModel === "checkbox"; - const lastActiveRef = useRef(-1); - const selectedRef = useRef(NO_SELECTION); - - const handleSelectionChange: RowClickHandler = useCallback( - (row, rangeSelect, keepExistingSelection) => { - const { [IDX]: idx } = row; - const { current: active } = lastActiveRef; - const { current: selected } = selectedRef; - - const selectOperation = isRowSelected(row) ? deselectItem : selectItem; - - const newSelected = selectOperation( - selectionModel, - selected, - idx, - rangeSelect, - keepExistingSelection, - active - ); - - selectedRef.current = newSelected; - lastActiveRef.current = idx; - - onSelect?.(row); - onSelectionChange?.(newSelected); - }, - [onSelect, onSelectionChange, selectionModel] - ); - - return handleSelectionChange; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useTable.ts b/vuu-ui/packages/vuu-table/src/table/useTable.ts deleted file mode 100644 index f79664d63..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useTable.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { - DataSource, - DataSourceSubscribedMessage, - JsonDataSource, - VuuFeatureInvocationMessage, - VuuFeatureMessage, -} from "@finos/vuu-data"; -import { DataSourceRow } from "@finos/vuu-data-types"; -import { - GridConfig, - KeyedColumnDescriptor, - SelectionChangeHandler, - TableSelectionModel, -} from "@finos/vuu-datagrid-types"; -import { useContextMenu as usePopupContextMenu } from "@finos/vuu-popups"; -import { VuuSortType } from "@finos/vuu-protocol-types"; -import { - applySort, - buildColumnMap, - isJsonGroup, - metadataKeys, - moveItemDeprecated, -} from "@finos/vuu-utils"; -import { - MouseEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useTableContextMenu } from "./context-menu"; -import { TableColumnResizeHandler } from "./dataTableTypes"; -import { useDataSource } from "./useDataSource"; -import { useDraggableColumn } from "./useDraggableColumn"; -import { useKeyboardNavigation } from "./useKeyboardNavigation"; -import { MeasuredProps, useMeasuredContainer } from "./useMeasuredContainer"; -import { useSelection } from "./useSelection"; -import { PersistentColumnAction, useTableModel } from "./useTableModel"; -import { useTableScroll } from "./useTableScroll"; -import { useTableViewport } from "../table-next/useTableViewport"; -import { useVirtualViewport } from "./useVirtualViewport"; - -const NO_ROWS = [] as const; - -export interface TableHookProps extends MeasuredProps { - config: Omit; - dataSource: DataSource; - headerHeight: number; - onConfigChange?: (config: Omit) => void; - onFeatureEnabled?: (message: VuuFeatureMessage) => void; - onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; - renderBufferSize?: number; - rowHeight: number; - onSelectionChange?: SelectionChangeHandler; - selectionModel: TableSelectionModel; -} - -const { KEY, IS_EXPANDED, IS_LEAF } = metadataKeys; - -export const useTable = ({ - config, - dataSource, - headerHeight, - onConfigChange, - onFeatureEnabled, - onFeatureInvocation, - onSelectionChange, - renderBufferSize = 0, - rowHeight, - selectionModel, - ...measuredProps -}: TableHookProps) => { - const [rowCount, setRowCount] = useState(dataSource.size); - const expectConfigChangeRef = useRef(false); - - // When we detect and respond to changes to config below, we need - // to include current dataSource config when we refresh the model. - const dataSourceRef = useRef(); - dataSourceRef.current = dataSource; - - if (dataSource === undefined) { - throw Error("no data source provided to Vuu Table"); - } - - const containerMeasurements = useMeasuredContainer(measuredProps); - - const onDataRowcountChange = useCallback((size: number) => { - setRowCount(size); - }, []); - - const { columns, dispatchColumnAction, headings } = useTableModel( - config, - dataSource.config - ); - - const { - getRowAtPosition, - getRowOffset, - setPctScrollTop, - ...viewportMeasurements - } = useTableViewport({ - columns, - headerHeight, - headings, - rowCount, - rowHeight, - size: containerMeasurements.innerSize, - }); - - const onSubscribed = useCallback( - ({ tableSchema }: DataSourceSubscribedMessage) => { - if (tableSchema) { - expectConfigChangeRef.current = true; - dispatchColumnAction({ - type: "setTableSchema", - tableSchema, - }); - } else { - console.log("usbscription message with no schema"); - } - }, - [dispatchColumnAction] - ); - - const handleSelectionChange: SelectionChangeHandler = useCallback( - (selected) => { - dataSource.select(selected); - onSelectionChange?.(selected); - }, - [dataSource, onSelectionChange] - ); - - const handleRowClick = useSelection({ - onSelectionChange: handleSelectionChange, - selectionModel, - }); - - const { data, getSelectedRows, range, setRange } = useDataSource({ - dataSource, - onFeatureEnabled, - onFeatureInvocation, - onSubscribed, - onSizeChange: onDataRowcountChange, - renderBufferSize, - viewportRowCount: viewportMeasurements.rowCount, - }); - - // Keep a ref to current data. We use it to provide row for context menu actions. - // We don't want to introduce data as a dependency on the context menu handler, just - // needs to be correct at runtime when the row is right clicked. - const dataRef = useRef(); - dataRef.current = data; - - const onPersistentColumnOperation = useCallback( - (action: PersistentColumnAction) => { - expectConfigChangeRef.current = true; - console.log(`onPersistentColumnOperation, dispatchColumnAction`, { - action, - }); - dispatchColumnAction(action as any); - }, - [dispatchColumnAction] - ); - - const handleContextMenuAction = useTableContextMenu({ - dataSource, - onPersistentColumnOperation, - }); - - const handleSort = useCallback( - ( - column: KeyedColumnDescriptor, - extendSort = false, - sortType?: VuuSortType - ) => { - if (dataSource) { - dataSource.sort = applySort( - dataSource.sort, - column, - extendSort, - sortType - ); - } - }, - [dataSource] - ); - - const handleColumnResize: TableColumnResizeHandler = useCallback( - (phase, columnName, width) => { - const column = columns.find((column) => column.name === columnName); - if (column) { - if (phase === "end") { - expectConfigChangeRef.current = true; - } - dispatchColumnAction({ - type: "resizeColumn", - phase, - column, - width, - }); - } else { - throw Error( - `useDataTable.handleColumnResize, column ${columnName} not found` - ); - } - }, - [columns, dispatchColumnAction] - ); - - const handleToggleGroup = useCallback( - (row: DataSourceRow, column: KeyedColumnDescriptor) => { - const isJson = isJsonGroup(column, row); - const key = row[KEY]; - - if (row[IS_EXPANDED]) { - (dataSource as JsonDataSource).closeTreeNode(key, true); - if (isJson) { - const idx = columns.indexOf(column); - const rows = (dataSource as JsonDataSource).getRowsAtDepth(idx + 1); - if (!rows.some((row) => row[IS_EXPANDED] || row[IS_LEAF])) { - dispatchColumnAction({ - type: "hideColumns", - columns: columns.slice(idx + 2), - }); - } - } - } else { - dataSource.openTreeNode(key); - if (isJson) { - const childRows = (dataSource as JsonDataSource).getChildRows(key); - const idx = columns.indexOf(column) + 1; - const columnsToShow = [columns[idx]]; - if (childRows.some((row) => row[IS_LEAF])) { - columnsToShow.push(columns[idx + 1]); - } - if (columnsToShow.some((col) => col.hidden)) { - dispatchColumnAction({ - type: "showColumns", - columns: columnsToShow, - }); - } - } - } - }, - [columns, dataSource, dispatchColumnAction] - ); - - const { - onVerticalScroll, - onHorizontalScroll, - columnsWithinViewport, - virtualColSpan, - } = useVirtualViewport({ - columns, - getRowAtPosition, - setRange, - viewportMeasurements, - }); - - const handleVerticalScroll = useCallback( - (scrollTop: number, pctScrollTop: number) => { - setPctScrollTop(pctScrollTop); - onVerticalScroll(scrollTop); - }, - [onVerticalScroll, setPctScrollTop] - ); - - const { requestScroll, ...scrollProps } = useTableScroll({ - onHorizontalScroll, - onVerticalScroll: handleVerticalScroll, - viewport: viewportMeasurements, - viewportHeight: - (containerMeasurements.innerSize?.height ?? 0) - headerHeight, - }); - - const containerProps = useKeyboardNavigation({ - columnCount: columns.length, - containerRef: containerMeasurements.containerRef, - data, - requestScroll, - rowCount: dataSource?.size, - viewportRange: range, - }); - - const handleRemoveColumnFromGroupBy = useCallback( - (column?: KeyedColumnDescriptor) => { - if (column) { - if (dataSource && dataSource.groupBy.includes(column.name)) { - dataSource.groupBy = dataSource.groupBy.filter( - (columnName) => columnName !== column.name - ); - } - } else { - dataSource.groupBy = []; - } - }, - [dataSource] - ); - - const handleDropColumn = useCallback( - (fromIndex: number, toIndex: number) => { - const column = dataSource.columns[fromIndex]; - const columns = moveItemDeprecated(dataSource.columns, column, toIndex); - if (columns !== dataSource.columns) { - dataSource.columns = columns; - dispatchColumnAction({ type: "tableConfig", columns }); - } - }, - [dataSource, dispatchColumnAction] - ); - - const draggableHook = useDraggableColumn({ - onDrop: handleDropColumn, - }); - - useEffect(() => { - // External config has changed - if (dataSourceRef.current) { - expectConfigChangeRef.current = true; - dispatchColumnAction({ - type: "init", - tableConfig: config, - dataSourceConfig: dataSourceRef.current.config, - }); - } - }, [config, dispatchColumnAction]); - - useEffect(() => { - dataSource.on("config", (config, confirmed) => { - expectConfigChangeRef.current = true; - dispatchColumnAction({ - type: "tableConfig", - ...config, - confirmed, - }); - }); - }, [dataSource, dispatchColumnAction]); - - useMemo(() => { - if (expectConfigChangeRef.current) { - onConfigChange?.({ - ...config, - columns, - }); - expectConfigChangeRef.current = false; - } - }, [columns, config, onConfigChange]); - - const [showContextMenu] = usePopupContextMenu(); - - const onContextMenu = useCallback( - (evt: MouseEvent) => { - const { current: currentData } = dataRef; - const { current: currentDataSource } = dataSourceRef; - const target = evt.target as HTMLElement; - const cellEl = target?.closest("div[role='cell']"); - const rowEl = target?.closest(".vuuTableRow"); - - if (cellEl && rowEl && currentData && currentDataSource) { - const { columns, selectedRowsCount } = currentDataSource; - const columnMap = buildColumnMap(columns); - const rowIndex = parseInt(rowEl.ariaRowIndex ?? "-1"); - const cellIndex = Array.from(rowEl.childNodes).indexOf(cellEl); - const row = currentData.find(([idx]) => idx === rowIndex); - const columnName = columns[cellIndex]; - - showContextMenu(evt, "grid", { - columnMap, - columnName, - row, - selectedRows: selectedRowsCount === 0 ? NO_ROWS : getSelectedRows(), - viewport: dataSource?.viewport, - }); - } - }, - [dataSource?.viewport, getSelectedRows, showContextMenu] - ); - - return { - columns, - columnsWithinViewport, - containerMeasurements, - containerProps, - data, - dispatchColumnAction, - getRowOffset, - handleContextMenuAction, - headings, - onColumnResize: handleColumnResize, - onContextMenu, - onRemoveColumnFromGroupBy: handleRemoveColumnFromGroupBy, - onRowClick: handleRowClick, - onSort: handleSort, - onToggleGroup: handleToggleGroup, - virtualColSpan, - scrollProps, - rowCount, - viewportMeasurements, - ...draggableHook, - }; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useTableColumnResize.tsx b/vuu-ui/packages/vuu-table/src/table/useTableColumnResize.tsx deleted file mode 100644 index bea84ad5f..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useTableColumnResize.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Heading, KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { RefObject, useCallback, useRef } from "react"; - -export type ResizeHandler = (evt: MouseEvent, moveBy: number) => void; -export interface CellResizeHookProps { - column: KeyedColumnDescriptor | Heading; - onResize?: (phase: resizePhase, columnName: string, width?: number) => void; - rootRef: RefObject; -} - -type resizePhase = "begin" | "resize" | "end"; - -export interface CellResizeHookResult { - isResizing: boolean; - onDrag: (evt: MouseEvent, moveBy: number) => void; - onDragStart: (evt: React.MouseEvent) => void; - onDragEnd: (evt: MouseEvent) => void; -} - -export const useTableColumnResize = ({ - column, - onResize, - rootRef, -}: CellResizeHookProps): CellResizeHookResult => { - const widthRef = useRef(0); - const isResizing = useRef(false); - const { name } = column; - - const handleResizeStart = useCallback(() => { - if (onResize && rootRef.current) { - console.log("handleResizeStart"); - const { width } = rootRef.current.getBoundingClientRect(); - widthRef.current = Math.round(width); - isResizing.current = true; - onResize?.("begin", name); - } - }, [name, onResize, rootRef]); - - const handleResize = useCallback( - (_evt: MouseEvent, moveBy: number) => { - if (rootRef.current) { - if (onResize) { - const { width } = rootRef.current.getBoundingClientRect(); - const newWidth = Math.round(width) + moveBy; - if (newWidth !== widthRef.current && newWidth > 0) { - onResize("resize", name, newWidth); - widthRef.current = newWidth; - } - } - } - }, - [name, onResize, rootRef] - ); - - const handleResizeEnd = useCallback(() => { - if (onResize) { - onResize("end", name, widthRef.current); - setTimeout(() => { - // set in a timeout to prevent the click event from firing and triggering a sort - isResizing.current = false; - }, 100); - } - }, [name, onResize]); - - return { - isResizing: isResizing.current, - onDrag: handleResize, - onDragStart: handleResizeStart, - onDragEnd: handleResizeEnd, - }; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts b/vuu-ui/packages/vuu-table/src/table/useTableModel.ts deleted file mode 100644 index 2d9cf212e..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useTableModel.ts +++ /dev/null @@ -1,519 +0,0 @@ -import { - ColumnDescriptor, - GridConfig, - KeyedColumnDescriptor, - PinLocation, - TableConfig, -} from "@finos/vuu-datagrid-types"; -import { - applyFilterToColumns, - applyGroupByToColumns, - applySortToColumns, - findColumn, - getCellRenderer, - getColumnName, - getTableHeadings, - getValueFormatter, - isFilteredColumn, - isGroupColumn, - isPinned, - isTypeDescriptor, - metadataKeys, - updateColumn, - sortPinnedColumns, - stripFilterFromColumns, - moveItemDeprecated, - getDefaultAlignment, - isCalculatedColumn, - getCalculatedColumnName, -} from "@finos/vuu-utils"; - -import { Reducer, useReducer } from "react"; -import { VuuColumnDataType } from "@finos/vuu-protocol-types"; -import { DataSourceConfig, TableSchema } from "@finos/vuu-data"; - -const DEFAULT_COLUMN_WIDTH = 100; -const KEY_OFFSET = metadataKeys.count; - -const columnWithoutDataType = ({ serverDataType }: ColumnDescriptor) => - serverDataType === undefined; - -const getCellRendererForColumn = (column: ColumnDescriptor) => { - if (isTypeDescriptor(column.type)) { - return getCellRenderer(column); - } -}; - -const getServerDataTypeForColumn = ( - column: ColumnDescriptor, - tableSchema?: TableSchema -): VuuColumnDataType => { - if (column.serverDataType) { - return column.serverDataType; - } else if (tableSchema) { - const schemaColumn = tableSchema.columns.find( - (col) => col.name === column.name - ); - if (schemaColumn) { - return schemaColumn.serverDataType; - } - } - return "string"; -}; - -export interface TableModel extends Omit { - columns: KeyedColumnDescriptor[]; - tableSchema?: Readonly; -} - -export interface ColumnActionInit { - type: "init"; - tableConfig: TableConfig; - dataSourceConfig?: DataSourceConfig; -} - -export interface ColumnActionHide { - type: "hideColumns"; - columns: KeyedColumnDescriptor[]; -} - -export interface ColumnActionShow { - type: "showColumns"; - columns: KeyedColumnDescriptor[]; -} -export interface ColumnActionMove { - type: "moveColumn"; - column: KeyedColumnDescriptor; - moveBy?: 1 | -1; - moveTo?: number; -} - -export interface ColumnActionPin { - type: "pinColumn"; - column: ColumnDescriptor; - pin?: PinLocation; -} -export interface ColumnActionResize { - type: "resizeColumn"; - column: KeyedColumnDescriptor; - phase: "begin" | "resize" | "end"; - width?: number; -} - -export interface ColumnActionSetTableSchema { - type: "setTableSchema"; - tableSchema: TableSchema; -} - -export interface ColumnActionUpdate { - type: "updateColumn"; - column: ColumnDescriptor; -} - -export interface ColumnActionUpdateProp { - align?: ColumnDescriptor["align"]; - column: KeyedColumnDescriptor; - hidden?: ColumnDescriptor["hidden"]; - label?: ColumnDescriptor["label"]; - resizing?: KeyedColumnDescriptor["resizing"]; - type: "updateColumnProp"; - width?: ColumnDescriptor["width"]; -} - -export interface ColumnActionTableConfig extends DataSourceConfig { - confirmed?: boolean; - type: "tableConfig"; -} -export interface ColumnActionColumnSettings extends DataSourceConfig { - type: "columnSettings"; - column: KeyedColumnDescriptor; -} - -export interface ColumnActionTableSettings extends DataSourceConfig { - type: "tableSettings"; -} - -/** - * PersistentColumnActions are those actions that require us to persist user changes across sessions - */ -export type PersistentColumnAction = - | ColumnActionPin - | ColumnActionHide - | ColumnActionColumnSettings - | ColumnActionTableSettings; - -export const isShowColumnSettings = ( - action: PersistentColumnAction -): action is ColumnActionColumnSettings => action.type === "columnSettings"; - -export const isShowTableSettings = ( - action: PersistentColumnAction -): action is ColumnActionTableSettings => action.type === "tableSettings"; - -export type GridModelAction = - | ColumnActionColumnSettings - | ColumnActionHide - | ColumnActionInit - | ColumnActionMove - | ColumnActionPin - | ColumnActionResize - | ColumnActionSetTableSchema - | ColumnActionShow - | ColumnActionUpdate - | ColumnActionUpdateProp - | ColumnActionTableConfig; - -export type GridModelReducer = Reducer; - -export type ColumnActionDispatch = (action: GridModelAction) => void; - -const columnReducer: GridModelReducer = (state, action) => { - // info?.(`GridModelReducer ${action.type}`); - switch (action.type) { - case "init": - return init(action); - case "moveColumn": - return moveColumn(state, action); - case "resizeColumn": - return resizeColumn(state, action); - case "setTableSchema": - return setTableSchema(state, action); - case "hideColumns": - return hideColumns(state, action); - case "showColumns": - return showColumns(state, action); - case "pinColumn": - return pinColumn(state, action); - case "updateColumnProp": - return updateColumnProp(state, action); - case "tableConfig": - return updateTableConfig(state, action); - default: - console.log(`unhandled action ${action.type}`); - return state; - } -}; - -export const useTableModel = ( - tableConfig: Omit, - dataSourceConfig?: DataSourceConfig -) => { - const [state, dispatchColumnAction] = useReducer< - GridModelReducer, - InitialConfig - >(columnReducer, { tableConfig, dataSourceConfig }, init); - - return { - columns: state.columns, - dispatchColumnAction, - headings: state.headings, - }; -}; - -type InitialConfig = { - dataSourceConfig?: DataSourceConfig; - tableConfig: TableConfig; -}; - -function init({ dataSourceConfig, tableConfig }: InitialConfig): TableModel { - const columns = tableConfig.columns.map( - toKeyedColumWithDefaults(tableConfig) - ); - const maybePinnedColumns = columns.some(isPinned) - ? sortPinnedColumns(columns) - : columns; - const state = { - columns: maybePinnedColumns, - headings: getTableHeadings(maybePinnedColumns), - }; - if (dataSourceConfig) { - const { columns, ...rest } = dataSourceConfig; - return updateTableConfig(state, { - type: "tableConfig", - ...rest, - }); - } else { - return state; - } -} - -const labelFromName = (column: ColumnDescriptor) => { - if (isCalculatedColumn(column.name)) { - return getCalculatedColumnName(column); - } else { - return column.name; - } -}; - -const getLabel = ( - label: string, - columnFormatHeader?: "uppercase" | "capitalize" -): string => { - if (columnFormatHeader === "uppercase") { - return label.toUpperCase(); - } else if (columnFormatHeader === "capitalize") { - return label[0].toUpperCase() + label.slice(1).toLowerCase(); - } - return label; -}; - -const toKeyedColumWithDefaults = - (options: Partial | Partial) => - ( - column: ColumnDescriptor & { key?: number }, - index: number - ): KeyedColumnDescriptor => { - const serverDataType = getServerDataTypeForColumn( - column, - (options as Partial).tableSchema - ); - const { columnDefaultWidth = DEFAULT_COLUMN_WIDTH, columnFormatHeader } = - options; - const { - align = getDefaultAlignment(serverDataType), - key, - name, - label = labelFromName(column), - width = columnDefaultWidth, - ...rest - } = column; - - const keyedColumnWithDefaults = { - ...rest, - align, - CellRenderer: getCellRendererForColumn(column), - label: getLabel(label, columnFormatHeader), - key: key ?? index + KEY_OFFSET, - name, - originalIdx: index, - serverDataType, - valueFormatter: getValueFormatter(column), - width: width, - }; - - if (isGroupColumn(keyedColumnWithDefaults)) { - keyedColumnWithDefaults.columns = keyedColumnWithDefaults.columns.map( - (col) => toKeyedColumWithDefaults(options)(col, col.key) - ); - } - - return keyedColumnWithDefaults; - }; - -function moveColumn( - state: TableModel, - { column, moveBy, moveTo }: ColumnActionMove -) { - const { columns } = state; - if (typeof moveBy === "number") { - const idx = columns.indexOf(column); - const newColumns = columns.slice(); - const [movedColumns] = newColumns.splice(idx, 1); - newColumns.splice(idx + moveBy, 0, movedColumns); - return { - ...state, - columns: newColumns, - }; - } else if (typeof moveTo === "number") { - return { - ...state, - columns: moveItemDeprecated(columns, column, moveTo), - }; - } - return state; -} - -function hideColumns(state: TableModel, { columns }: ColumnActionHide) { - if (columns.some((col) => col.hidden !== true)) { - return columns.reduce((s, c) => { - if (c.hidden !== true) { - return updateColumnProp(s, { - type: "updateColumnProp", - column: c, - hidden: true, - }); - } else { - return s; - } - }, state); - } else { - return state; - } -} -function showColumns(state: TableModel, { columns }: ColumnActionShow) { - if (columns.some((col) => col.hidden)) { - return columns.reduce((s, c) => { - if (c.hidden) { - return updateColumnProp(s, { - type: "updateColumnProp", - column: c, - hidden: false, - }); - } else { - return s; - } - }, state); - } else { - return state; - } -} - -function resizeColumn( - state: TableModel, - { column, phase, width }: ColumnActionResize -) { - const type = "updateColumnProp"; - const resizing = phase !== "end"; - - switch (phase) { - case "begin": - return updateColumnProp(state, { type, column, resizing }); - case "end": - return updateColumnProp(state, { type, column, resizing, width }); - case "resize": - return updateColumnProp(state, { type, column, width }); - default: - throw Error(`useTableModel.resizeColumn, invalid resizePhase ${phase}`); - } -} - -function setTableSchema( - state: TableModel, - { tableSchema }: ColumnActionSetTableSchema -) { - const { columns } = state; - if (columns.some(columnWithoutDataType)) { - const cols = columns.map((column) => { - const serverDataType = getServerDataTypeForColumn(column, tableSchema); - return { - ...column, - align: column.align ?? getDefaultAlignment(serverDataType), - serverDataType, - }; - }); - - return { - ...state, - columns: cols, - tableSchema, - }; - } else { - return { - ...state, - tableSchema, - }; - } -} - -function pinColumn(state: TableModel, action: ColumnActionPin) { - let { columns } = state; - const { column, pin } = action; - columns = updateColumn(columns, column.name, { pin }); - columns = sortPinnedColumns(columns); - return { - ...state, - columns, - }; -} -function updateColumnProp(state: TableModel, action: ColumnActionUpdateProp) { - let { columns } = state; - const { align, column, hidden, label, resizing, width } = action; - const options: Partial = {}; - - if (align === "left" || align === "right") { - options.align = align; - } - if (typeof label === "string") { - options.label = label; - } - if (typeof resizing === "boolean") { - options.resizing = resizing; - } - if (typeof hidden === "boolean") { - options.hidden = hidden; - } - if (typeof width === "number") { - options.width = width; - } - - columns = updateColumn(columns, column.name, options); - - return { - ...state, - columns, - }; -} - -function updateTableConfig( - state: TableModel, - { columns, confirmed, filter, groupBy, sort }: ColumnActionTableConfig -) { - const hasColumns = columns && columns.length > 0; - const hasGroupBy = groupBy !== undefined; - const hasFilter = typeof filter?.filter === "string"; - const hasSort = sort && sort.sortDefs.length > 0; - - //TODO check if just confirmed has changed - - let result = state; - - if (hasColumns) { - result = { - ...state, - columns: columns.map((colName, index) => { - const columnName = getColumnName(colName); - const key: number = index + KEY_OFFSET; - const col = findColumn(result.columns, columnName); - if (col) { - if (col.key === key) { - return col; - } else { - return { - ...col, - key, - }; - } - } else { - // we have a column which was not previously included. - // TODO How do we get the serverDataType - // TODO it needs to be available in availableCOlumns or allColumns in state - return toKeyedColumWithDefaults(state)( - { - name: colName, - }, - index - ); - } - throw Error(`useTableModel column ${colName} not found`); - }), - }; - } - - if (hasGroupBy) { - result = { - ...state, - columns: applyGroupByToColumns(result.columns, groupBy, confirmed), - }; - } - - if (hasSort) { - result = { - ...state, - columns: applySortToColumns(result.columns, sort), - }; - } - - if (hasFilter) { - result = { - ...state, - columns: applyFilterToColumns(result.columns, filter), - }; - } else if (result.columns.some(isFilteredColumn)) { - result = { - ...state, - columns: stripFilterFromColumns(result.columns), - }; - } - - return result; -} diff --git a/vuu-ui/packages/vuu-table/src/table/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/table/useTableScroll.ts deleted file mode 100644 index 2fe7f10d0..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useTableScroll.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { useCallback, useRef } from "react"; -import { Viewport } from "./dataTableTypes"; - -export interface ScrollRequestEnd { - type: "scroll-end"; - direction: "home" | "end"; -} - -export interface ScrollRequestPage { - type: "scroll-page"; - direction: "up" | "down"; -} - -export interface ScrollRequestDistance { - type: "scroll-distance"; - distance: number; -} - -export type ScrollRequest = - | ScrollRequestPage - | ScrollRequestDistance - | ScrollRequestEnd; - -export type ScrollRequestHandler = (request: ScrollRequest) => void; - -const getPctScroll = (container: HTMLElement) => { - const { scrollLeft, scrollTop } = container; - const { clientHeight, clientWidth, scrollHeight, scrollWidth } = container; - const pctScrollLeft = scrollLeft / (scrollWidth - clientWidth); - const pctScrollTop = scrollTop / (scrollHeight - clientHeight); - - return [pctScrollLeft, pctScrollTop]; -}; - -const getMaxScroll = (container: HTMLElement) => { - const { clientHeight, clientWidth, scrollHeight, scrollWidth } = container; - return [scrollWidth - clientWidth, scrollHeight - clientHeight]; -}; - -interface CallbackRefHookProps { - onAttach?: (el: T) => void; - onDetach: (el: T) => void; - label?: string; -} - -const useCallbackRef = ({ - onAttach, - onDetach, -}: CallbackRefHookProps) => { - const ref = useRef(null); - const callbackRef = useCallback( - (el: T | null) => { - if (el) { - ref.current = el; - onAttach?.(el); - } else if (ref.current) { - const { current: originalRef } = ref; - ref.current = el; - onDetach?.(originalRef); - } - }, - [onAttach, onDetach] - ); - return callbackRef; -}; - -export interface TableScrollHookProps { - onHorizontalScroll?: (scrollLeft: number) => void; - onVerticalScroll?: (scrollTop: number, pctScrollTop: number) => void; - viewportHeight: number; - viewport: Viewport; -} - -export const useTableScroll = ({ - onHorizontalScroll, - onVerticalScroll, - viewport, -}: TableScrollHookProps) => { - const contentContainerScrolledRef = useRef(false); - - const scrollPosRef = useRef({ scrollTop: 0, scrollLeft: 0 }); - const scrollbarContainerRef = useRef(null); - const contentContainerRef = useRef(null); - const { - maxScrollContainerScrollHorizontal: maxScrollLeft, - maxScrollContainerScrollVertical: maxScrollTop, - } = viewport; - - const handleScrollbarContainerScroll = useCallback(() => { - const { current: contentContainer } = contentContainerRef; - const { current: scrollbarContainer } = scrollbarContainerRef; - const { current: contentContainerScrolled } = contentContainerScrolledRef; - if (contentContainerScrolled) { - contentContainerScrolledRef.current = false; - } else if (contentContainer && scrollbarContainer) { - const [pctScrollLeft, pctScrollTop] = getPctScroll(scrollbarContainer); - const [maxScrollLeft, maxScrollTop] = getMaxScroll(contentContainer); - const rootScrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - const rootScrollTop = Math.round(pctScrollTop * maxScrollTop); - console.log( - `pctScrollTop ${pctScrollTop}, maxScrollTop ${maxScrollTop} rootScrollTop ${rootScrollTop}` - ); - - contentContainer.scrollTo({ - left: rootScrollLeft, - top: rootScrollTop, - behavior: "auto", - }); - } - }, []); - - const handleContentContainerScroll = useCallback(() => { - const { current: contentContainer } = contentContainerRef; - const { current: scrollbarContainer } = scrollbarContainerRef; - const { current: scrollPos } = scrollPosRef; - - if (contentContainer && scrollbarContainer) { - const { scrollLeft, scrollTop } = contentContainer; - const [pctScrollLeft, pctScrollTop] = getPctScroll(contentContainer); - contentContainerScrolledRef.current = true; - - scrollbarContainer.scrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - scrollbarContainer.scrollTop = Math.round(pctScrollTop * maxScrollTop); - - if (scrollPos.scrollTop !== scrollTop) { - scrollPos.scrollTop = scrollTop; - onVerticalScroll?.(scrollTop, pctScrollTop); - } - if (scrollPos.scrollLeft !== scrollLeft) { - scrollPos.scrollLeft = scrollLeft; - onHorizontalScroll?.(scrollLeft); - } - } - }, [maxScrollLeft, maxScrollTop, onHorizontalScroll, onVerticalScroll]); - - const handleAttachScrollbarContainer = useCallback( - (el: HTMLDivElement) => { - scrollbarContainerRef.current = el; - el.addEventListener("scroll", handleScrollbarContainerScroll, { - passive: true, - }); - }, - [handleScrollbarContainerScroll] - ); - - const handleDetachScrollbarContainer = useCallback( - (el: HTMLDivElement) => { - scrollbarContainerRef.current = null; - el.removeEventListener("scroll", handleScrollbarContainerScroll); - }, - [handleScrollbarContainerScroll] - ); - - const handleAttachContentContainer = useCallback( - (el: HTMLDivElement) => { - contentContainerRef.current = el; - el.addEventListener("scroll", handleContentContainerScroll, { - passive: true, - }); - }, - [handleContentContainerScroll] - ); - - const handleDetachContentContainer = useCallback( - (el: HTMLDivElement) => { - contentContainerRef.current = null; - el.removeEventListener("scroll", handleContentContainerScroll); - }, - [handleContentContainerScroll] - ); - - const contentContainerCallbackRef = useCallbackRef({ - onAttach: handleAttachContentContainer, - onDetach: handleDetachContentContainer, - }); - - const scrollbarContainerCallbackRef = useCallbackRef({ - onAttach: handleAttachScrollbarContainer, - onDetach: handleDetachScrollbarContainer, - }); - - const requestScroll: ScrollRequestHandler = useCallback( - (scrollRequest) => { - const { current: scrollbarContainer } = contentContainerRef; - if (scrollbarContainer) { - contentContainerScrolledRef.current = false; - if (scrollRequest.type === "scroll-page") { - const { clientHeight, scrollLeft, scrollTop } = scrollbarContainer; - const { direction } = scrollRequest; - const scrollBy = direction === "down" ? clientHeight : -clientHeight; - const newScrollTop = Math.min( - Math.max(0, scrollTop + scrollBy), - maxScrollTop - ); - scrollbarContainer.scrollTo({ - top: newScrollTop, - left: scrollLeft, - behavior: "auto", - }); - } else if (scrollRequest.type === "scroll-end") { - const { direction } = scrollRequest; - const scrollTo = direction === "end" ? maxScrollTop : 0; - scrollbarContainer.scrollTo({ - top: scrollTo, - left: scrollbarContainer.scrollLeft, - behavior: "auto", - }); - } - } - }, - [maxScrollTop] - ); - - return { - /** Ref to be assigned to ScrollbarContainer */ - scrollbarContainerRef: scrollbarContainerCallbackRef, - /** Ref to be assigned to ContentContainer */ - contentContainerRef: contentContainerCallbackRef, - /** Scroll the table */ - requestScroll, - }; -}; diff --git a/vuu-ui/packages/vuu-table/src/table/useVirtualViewport.ts b/vuu-ui/packages/vuu-table/src/table/useVirtualViewport.ts deleted file mode 100644 index 877f93a7b..000000000 --- a/vuu-ui/packages/vuu-table/src/table/useVirtualViewport.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { - getColumnsInViewport, - itemsChanged, - RowAtPositionFunc, -} from "@finos/vuu-utils"; -import { VuuRange } from "@finos/vuu-protocol-types"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ViewportMeasurements } from "../table-next/useTableViewport"; - -export interface VirtualViewportHookProps { - columns: KeyedColumnDescriptor[]; - getRowAtPosition: RowAtPositionFunc; - setRange: (range: VuuRange) => void; - viewportMeasurements: ViewportMeasurements; -} -export interface VirtualViewportHookResult { - onHorizontalScroll: (scrollLeft: number) => void; - onVerticalScroll: (scrollTop: number) => void; - columnsWithinViewport: KeyedColumnDescriptor[]; - virtualColSpan: number; -} - -export const useVirtualViewport = ({ - columns, - getRowAtPosition, - setRange, - viewportMeasurements, -}: VirtualViewportHookProps): VirtualViewportHookResult => { - const firstRowRef = useRef(-1); - const { - rowCount: viewportRowCount, - contentWidth, - maxScrollContainerScrollHorizontal, - } = viewportMeasurements; - // double check this ... - const availableWidth = contentWidth - maxScrollContainerScrollHorizontal; - const scrollLeftRef = useRef(0); - - const [visibleColumns, preSpan] = useMemo( - () => - getColumnsInViewport( - columns, - scrollLeftRef.current, - scrollLeftRef.current + availableWidth - ), - [availableWidth, columns] - ); - - const preSpanRef = useRef(preSpan); - - useEffect(() => { - setColumnsWithinViewport(visibleColumns); - }, [visibleColumns]); - - const [columnsWithinViewport, setColumnsWithinViewport] = - useState(visibleColumns); - - const handleHorizontalScroll = useCallback( - (scrollLeft: number) => { - scrollLeftRef.current = scrollLeft; - const [visibleColumns, pre] = getColumnsInViewport( - columns, - scrollLeft, - scrollLeft + availableWidth - ); - if (itemsChanged(columnsWithinViewport, visibleColumns)) { - preSpanRef.current = pre; - - setColumnsWithinViewport(visibleColumns); - } - }, - [availableWidth, columns, columnsWithinViewport] - ); - - const handleVerticalScroll = useCallback( - (scrollTop: number) => { - const firstRow = getRowAtPosition(scrollTop); - if (firstRow !== firstRowRef.current) { - firstRowRef.current = firstRow; - console.log("setRange from handleVerticalScroll"); - setRange({ from: firstRow, to: firstRow + viewportRowCount }); - } - }, - [getRowAtPosition, setRange, viewportRowCount] - ); - - return { - columnsWithinViewport, - onHorizontalScroll: handleHorizontalScroll, - onVerticalScroll: handleVerticalScroll, - /** number of leading columns not rendered because of virtualization */ - virtualColSpan: preSpanRef.current, - }; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx index 8f94ce721..8fef54eec 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx @@ -631,6 +631,9 @@ export const useDragDropNext: DragDropHook = ({ ...dragResult, ...draggableStatus, isScrolling, - onMouseDown: allowDragDrop ? mouseDownHandler : undefined, + onMouseDown: + allowDragDrop && allowDragDrop !== "drop-only" + ? mouseDownHandler + : undefined, }; }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts index 8af1b4f7f..513687bbc 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/editable/useEditableText.ts @@ -54,6 +54,7 @@ export const useEditableText = ({ setMessage(warningMessage); } else { setMessage(undefined); + console.log(`commit value ${value}`); onCommit(value as T).then((response) => { if (response === true) { isDirtyRef.current = false; @@ -96,7 +97,9 @@ export const useEditableText = ({ const handleBlur = useCallback>( (evt) => { - commit(evt.target as HTMLElement); + if (isDirtyRef.current) { + commit(evt.target as HTMLElement); + } }, [commit] ); diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx index 0eb457070..dc6276f79 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/Tab.tsx @@ -156,6 +156,7 @@ export const Tab = forwardRef(function Tab( allowClose={closeable} allowRename={editable} controlledComponentId={ariaControls} + controlledComponentTitle={label} location={location} onMenuAction={onMenuAction as MenuActionHandler} onMenuClose={onMenuClose} diff --git a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabMenu.tsx b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabMenu.tsx index c9dd0241d..e4d1bfe94 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabMenu.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tabstrip/TabMenu.tsx @@ -23,12 +23,17 @@ export interface TabMenuProps { * The id of associated component, if available */ controlledComponentId?: string; + /** + * The label of Tab, if available + */ + controlledComponentTitle?: string; } export const TabMenu = ({ allowClose, allowRename, controlledComponentId, + controlledComponentTitle, location, onMenuAction, onMenuClose, @@ -48,6 +53,7 @@ export const TabMenu = ({ }, { controlledComponentId, + controlledComponentTitle, tabIndex: index, }, ], diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index 89eb46036..fc9fcbd6d 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -8,7 +8,7 @@ import type { ColumnTypeRendering, ColumnTypeWithValidationRules, GroupColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, MappedValueTypeRenderer, PinLocation, TableHeading, @@ -89,8 +89,8 @@ export const isValidPinLocation = (v: string): v is PinLocation => export const isKeyedColumn = ( column: ColumnDescriptor -): column is KeyedColumnDescriptor => { - return typeof (column as KeyedColumnDescriptor).key === "number"; +): column is RuntimeColumnDescriptor => { + return typeof (column as RuntimeColumnDescriptor).key === "number"; }; export const fromServerDataType = ( @@ -141,7 +141,7 @@ export const isPinned = (column: ColumnDescriptor) => export const hasHeadings = (column: ColumnDescriptor) => Array.isArray(column.heading) && column.heading.length > 0; -export const isResizing = (column: KeyedColumnDescriptor) => column.resizing; +export const isResizing = (column: RuntimeColumnDescriptor) => column.resizing; export const isTextColumn = ({ serverDataType }: ColumnDescriptor) => serverDataType === undefined @@ -209,7 +209,7 @@ export const isMappedValueTypeRenderer = ( typeof (renderer as MappedValueTypeRenderer)?.map !== "undefined"; export function buildColumnMap( - columns?: (KeyedColumnDescriptor | SchemaColumn | string)[] + columns?: (RuntimeColumnDescriptor | SchemaColumn | string)[] ): ColumnMap { const start = metadataKeys.count; if (columns) { @@ -258,8 +258,8 @@ export const metadataKeys = { // This method mutates the passed columns array const insertColumn = ( - columns: KeyedColumnDescriptor[], - column: KeyedColumnDescriptor + columns: RuntimeColumnDescriptor[], + column: RuntimeColumnDescriptor ) => { const { originalIdx } = column; if (typeof originalIdx === "number") { @@ -276,12 +276,12 @@ const insertColumn = ( }; export const flattenColumnGroup = ( - columns: KeyedColumnDescriptor[] -): KeyedColumnDescriptor[] => { + columns: RuntimeColumnDescriptor[] +): RuntimeColumnDescriptor[] => { if (columns[0]?.isGroup) { const [groupColumn, ...nonGroupedColumns] = columns as [ GroupColumnDescriptor, - ...KeyedColumnDescriptor[] + ...RuntimeColumnDescriptor[] ]; groupColumn.columns.forEach((groupColumn) => { insertColumn(nonGroupedColumns, groupColumn); @@ -293,10 +293,10 @@ export const flattenColumnGroup = ( }; export function extractGroupColumn( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], groupBy?: VuuGroupBy, confirmed = true -): [GroupColumnDescriptor | null, KeyedColumnDescriptor[]] { +): [GroupColumnDescriptor | null, RuntimeColumnDescriptor[]] { if (groupBy && groupBy.length > 0) { const flattenedColumns = flattenColumnGroup(columns); // Note: groupedColumns will be in column order, not groupBy order @@ -314,7 +314,7 @@ export function extractGroupColumn( return result; }, - [[], []] as [KeyedColumnDescriptor[], KeyedColumnDescriptor[]] + [[], []] as [RuntimeColumnDescriptor[], RuntimeColumnDescriptor[]] ); if (groupedColumns.length !== groupBy.length) { throw Error( @@ -324,11 +324,11 @@ export function extractGroupColumn( ); } const groupCount = groupBy.length; - const groupCols: KeyedColumnDescriptor[] = groupBy.map((name, idx) => { + const groupCols: RuntimeColumnDescriptor[] = groupBy.map((name, idx) => { // Keep the cols in same order defined on groupBy const column = groupedColumns.find( (col) => col.name === name - ) as KeyedColumnDescriptor; + ) as RuntimeColumnDescriptor; return { ...column, groupLevel: groupCount - idx, @@ -351,25 +351,25 @@ export function extractGroupColumn( } export const isGroupColumn = ( - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ): column is GroupColumnDescriptor => column.isGroup === true; export const isJsonAttribute = (value: unknown) => typeof value === "string" && value.endsWith("+"); -export const isJsonGroup = (column: KeyedColumnDescriptor, row: VuuDataRow) => +export const isJsonGroup = (column: RuntimeColumnDescriptor, row: VuuDataRow) => (column.type as ColumnTypeDescriptor)?.name === "json" && isJsonAttribute(row[column.key]); -export const isJsonColumn = (column: KeyedColumnDescriptor) => +export const isJsonColumn = (column: RuntimeColumnDescriptor) => (column.type as ColumnTypeDescriptor)?.name === "json"; export const sortPinnedColumns = ( - columns: KeyedColumnDescriptor[] -): KeyedColumnDescriptor[] => { - const leftPinnedColumns: KeyedColumnDescriptor[] = []; - const rightPinnedColumns: KeyedColumnDescriptor[] = []; - const restColumns: KeyedColumnDescriptor[] = []; + columns: RuntimeColumnDescriptor[] +): RuntimeColumnDescriptor[] => { + const leftPinnedColumns: RuntimeColumnDescriptor[] = []; + const rightPinnedColumns: RuntimeColumnDescriptor[] = []; + const restColumns: RuntimeColumnDescriptor[] = []; // let pinnedWidthLeft = 0; let pinnedWidthLeft = 4; for (const column of columns) { @@ -392,7 +392,7 @@ export const sortPinnedColumns = ( if (leftPinnedColumns.length) { leftPinnedColumns.push({ - ...(leftPinnedColumns.pop() as KeyedColumnDescriptor), + ...(leftPinnedColumns.pop() as RuntimeColumnDescriptor), endPin: true, }); } @@ -402,7 +402,7 @@ export const sortPinnedColumns = ( : restColumns; if (rightPinnedColumns.length) { - const measuredRightPinnedColumns: KeyedColumnDescriptor[] = []; + const measuredRightPinnedColumns: RuntimeColumnDescriptor[] = []; let pinnedWidthRight = 0; for (const column of rightPinnedColumns) { measuredRightPinnedColumns.unshift({ @@ -419,7 +419,7 @@ export const sortPinnedColumns = ( }; export const getTableHeadings = ( - columns: KeyedColumnDescriptor[] + columns: RuntimeColumnDescriptor[] ): TableHeadings => { if (columns.some(hasHeadings)) { const maxHeadingDepth = columns.reduce( @@ -453,7 +453,7 @@ export const getColumnStyle = ({ pin, pinnedOffset = pin === "left" ? 0 : 4, width, -}: KeyedColumnDescriptor) => +}: RuntimeColumnDescriptor) => pin === "left" ? ({ left: pinnedOffset, @@ -470,7 +470,7 @@ export const getColumnStyle = ({ export const setAggregations = ( aggregations: VuuAggregation[], - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, aggType: VuuAggType ) => { return aggregations @@ -515,7 +515,7 @@ const collectFiltersForColumn = ( }; export const applyGroupByToColumns = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], groupBy: VuuGroupBy, confirmed = true ) => { @@ -526,7 +526,7 @@ export const applyGroupByToColumns = ( confirmed ); if (groupColumn) { - return [groupColumn as KeyedColumnDescriptor].concat(nonGroupedColumns); + return [groupColumn as RuntimeColumnDescriptor].concat(nonGroupedColumns); } } else if (columns[0]?.isGroup) { return flattenColumnGroup(columns); @@ -535,7 +535,7 @@ export const applyGroupByToColumns = ( }; export const applySortToColumns = ( - colunms: KeyedColumnDescriptor[], + colunms: RuntimeColumnDescriptor[], sort: VuuSort ) => colunms.map((column) => { @@ -556,7 +556,7 @@ export const applySortToColumns = ( }); export const applyFilterToColumns = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], { filterStruct }: DataSourceFilter ) => columns.map((column) => { @@ -577,10 +577,10 @@ export const applyFilterToColumns = ( } }); -export const isFilteredColumn = (column: KeyedColumnDescriptor) => +export const isFilteredColumn = (column: RuntimeColumnDescriptor) => column.filter !== undefined; -export const stripFilterFromColumns = (columns: KeyedColumnDescriptor[]) => +export const stripFilterFromColumns = (columns: RuntimeColumnDescriptor[]) => columns.map((col) => { const { filter, ...rest } = col; return filter ? rest : col; @@ -616,9 +616,9 @@ export const getColumnLabel = (column: ColumnDescriptor) => { }; export const findColumn = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], columnName: string -): KeyedColumnDescriptor | undefined => { +): RuntimeColumnDescriptor | undefined => { const column = columns.find((col) => col.name === columnName); if (column) { return column; @@ -637,13 +637,13 @@ export function updateColumn( column: T ): T[]; export function updateColumn( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], column: string, options: Partial -): KeyedColumnDescriptor[]; +): RuntimeColumnDescriptor[]; export function updateColumn( - columns: KeyedColumnDescriptor[], - column: string | KeyedColumnDescriptor, + columns: RuntimeColumnDescriptor[], + column: string | RuntimeColumnDescriptor, options?: Partial ) { const targetColumn = @@ -677,16 +677,16 @@ export const getRowRecord = ( ); }; -export const isDataLoading = (columns: KeyedColumnDescriptor[]) => { +export const isDataLoading = (columns: RuntimeColumnDescriptor[]) => { return isGroupColumn(columns[0]) && columns[0].groupConfirmed === false; }; export const getColumnsInViewport = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], vpStart: number, vpEnd: number -): [KeyedColumnDescriptor[], number] => { - const visibleColumns: KeyedColumnDescriptor[] = []; +): [RuntimeColumnDescriptor[], number] => { + const visibleColumns: RuntimeColumnDescriptor[] = []; let preSpan = 0; for (let offset = 0, i = 0; i < columns.length; i++) { @@ -714,10 +714,10 @@ export const getColumnsInViewport = ( return [visibleColumns, preSpan]; }; -const isNotHidden = (column: KeyedColumnDescriptor) => column.hidden !== true; +const isNotHidden = (column: RuntimeColumnDescriptor) => column.hidden !== true; export const visibleColumnAtIndex = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], index: number ) => { if (columns.every(isNotHidden)) { @@ -730,7 +730,7 @@ export const visibleColumnAtIndex = ( const { DEPTH, IS_LEAF } = metadataKeys; // Get the value for a specific columns within a grouped column export const getGroupValueAndOffset = ( - columns: KeyedColumnDescriptor[], + columns: RuntimeColumnDescriptor[], row: DataSourceRow ): [unknown, number] => { const { [DEPTH]: depth, [IS_LEAF]: isLeaf } = row; @@ -960,8 +960,8 @@ export const moveColumnTo = ( }; export function replaceColumn( - state: KeyedColumnDescriptor[], - column: KeyedColumnDescriptor + state: RuntimeColumnDescriptor[], + column: RuntimeColumnDescriptor ) { return state.map((col) => (col.name === column.name ? column : col)); } diff --git a/vuu-ui/packages/vuu-utils/src/component-registry.ts b/vuu-ui/packages/vuu-utils/src/component-registry.ts index 8e38011b9..9fcb13646 100644 --- a/vuu-ui/packages/vuu-utils/src/component-registry.ts +++ b/vuu-ui/packages/vuu-utils/src/component-registry.ts @@ -11,6 +11,7 @@ import { VuuRowDataItemType, } from "@finos/vuu-protocol-types"; import { isTypeDescriptor, isColumnTypeRenderer } from "./column-utils"; +import { HeaderCellProps } from "packages/vuu-datagrid/src"; export interface CellConfigPanelProps extends HTMLAttributes { onConfigChange: () => void; @@ -96,7 +97,8 @@ export function registerComponent< T extends | TableCellRendererProps | CellConfigPanelProps - | EditRuleValidator = TableCellRendererProps + | EditRuleValidator + | HeaderCellProps = TableCellRendererProps >( componentName: string, component: T extends EditRuleValidator ? T : FC, @@ -142,18 +144,27 @@ export const getRegisteredCellRenderers = ( export const getCellRendererOptions = (renderName: string) => optionsMap.get(renderName); -export function getCellRenderer(column: ColumnDescriptor) { - if (isTypeDescriptor(column.type)) { - const { renderer } = column.type; - if (isColumnTypeRenderer(renderer)) { - return cellRenderersMap.get(renderer.name); +export function getCellRenderer( + column: ColumnDescriptor, + cellType: "cell" | "col-content" | "col-label" = "cell" +) { + if (cellType === "cell") { + if (isTypeDescriptor(column.type)) { + const { renderer } = column.type; + if (isColumnTypeRenderer(renderer)) { + return cellRenderersMap.get(renderer.name); + } } - } - if (column.editable) { - // we can only offer a text input edit as a generic editor. - // If a more specialised editor is required, user must configure - // it in column config. - return cellRenderersMap.get("input-cell"); + if (column.editable) { + // we can only offer a text input edit as a generic editor. + // If a more specialised editor is required, user must configure + // it in column config. + return cellRenderersMap.get("input-cell"); + } + } else if (cellType === "col-label" && column.colHeaderLabelRenderer) { + return cellRenderersMap.get(column.colHeaderLabelRenderer); + } else if (cellType === "col-content" && column.colHeaderContentRenderer) { + return cellRenderersMap.get(column.colHeaderContentRenderer); } } diff --git a/vuu-ui/packages/vuu-utils/src/filter-utils.ts b/vuu-ui/packages/vuu-utils/src/filter-utils.ts index a62ba334f..2fdea55af 100644 --- a/vuu-ui/packages/vuu-utils/src/filter-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/filter-utils.ts @@ -1,6 +1,6 @@ //Note these are duplicated in vuu-filter, those should probably be removed. -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { AndFilter, Filter, @@ -81,7 +81,7 @@ export const filterAsQuery = (f: Filter): string => { }; export const removeColumnFromFilter = ( - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, filter: Filter ): [Filter | undefined, string] => { if (isMultiClauseFilter(filter)) { diff --git a/vuu-ui/packages/vuu-utils/src/group-utils.ts b/vuu-ui/packages/vuu-utils/src/group-utils.ts index d24a65bdd..d5af5b775 100644 --- a/vuu-ui/packages/vuu-utils/src/group-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/group-utils.ts @@ -1,9 +1,9 @@ -import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { RuntimeColumnDescriptor } from "@finos/vuu-datagrid-types"; import { VuuGroupBy } from "@finos/vuu-protocol-types"; export function addGroupColumn( groupBy: VuuGroupBy, - column: KeyedColumnDescriptor + column: RuntimeColumnDescriptor ) { if (groupBy) { return groupBy.concat(column.name); diff --git a/vuu-ui/packages/vuu-utils/src/sort-utils.ts b/vuu-ui/packages/vuu-utils/src/sort-utils.ts index 68b10cca3..4546d2d2a 100644 --- a/vuu-ui/packages/vuu-utils/src/sort-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/sort-utils.ts @@ -1,6 +1,6 @@ import { ColumnDescriptor, - KeyedColumnDescriptor, + RuntimeColumnDescriptor, } from "@finos/vuu-datagrid-types"; import { VuuSort, VuuSortCol, VuuSortType } from "@finos/vuu-protocol-types"; @@ -36,7 +36,7 @@ export const applySort = ( export const setSortColumn = ( { sortDefs }: VuuSort, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, sortType?: "A" | "D" ): VuuSort => { if (sortType === undefined) { @@ -57,7 +57,7 @@ export const setSortColumn = ( export const addSortColumn = ( { sortDefs }: VuuSort, - column: KeyedColumnDescriptor, + column: RuntimeColumnDescriptor, sortType: "A" | "D" = "A" ): VuuSort => { const sortEntry: VuuSortCol = { column: column.name, sortType }; diff --git a/vuu-ui/sample-apps/app-vuu-example/index.tsx b/vuu-ui/sample-apps/app-vuu-example/index.tsx index e2cd5be6f..8d3a9bc4d 100644 --- a/vuu-ui/sample-apps/app-vuu-example/index.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/index.tsx @@ -1,8 +1,4 @@ -import { - getAuthDetailsFromCookies, - LayoutManagementProvider, - redirectToLogin, -} from "@finos/vuu-shell"; +import { getAuthDetailsFromCookies, redirectToLogin } from "@finos/vuu-shell"; import React from "react"; import ReactDOM from "react-dom"; import { App } from "./src/App"; @@ -16,9 +12,7 @@ if (!username || !token) { redirectToLogin(); } else { ReactDOM.render( - - - , + , document.getElementById("root") ); } 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 a7f3eb185..b70b8b8e8 100644 --- a/vuu-ui/sample-apps/app-vuu-example/src/App.tsx +++ b/vuu-ui/sample-apps/app-vuu-example/src/App.tsx @@ -1,13 +1,12 @@ +import { registerComponent } from "@finos/vuu-layout"; +import { useDialog } from "@finos/vuu-popups"; import { - registerComponent, - useLayoutContextMenuItems, -} from "@finos/vuu-layout"; -import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; -import { + LayoutManagementProvider, LeftNav, Shell, ShellContextProvider, ShellProps, + SidePanelProps, VuuUser, } from "@finos/vuu-shell"; import { @@ -17,6 +16,7 @@ import { import { getDefaultColumnConfig } from "./columnMetaData"; import { createPlaceholder } from "./createPlaceholder"; import { useFeatures } from "./useFeatures"; +import { NotificationsProvider } from "@finos/vuu-popups"; import { DragDropProvider } from "@finos/vuu-ui-controls"; import "./App.css"; @@ -48,8 +48,6 @@ export const App = ({ user }: { user: VuuUser }) => { const { dialog, setDialogState } = useDialog(); const { handleRpcResponse } = useRpcResponseHandler(setDialogState); - const { buildMenuOptions, handleMenuAction } = - useLayoutContextMenuItems(setDialogState); const dragSource = useMemo( () => ({ @@ -58,35 +56,35 @@ export const App = ({ user }: { user: VuuUser }) => { [] ); - // TODO get Context from Shell + const leftSidePanelProps = useMemo( + () => ({ + children: , + sizeOpen: 240, + }), + [features, tableFeatures] + ); + return ( - - - - - } - saveUrl="https://localhost:8443/api/vui" - serverUrl={serverUrl} - user={user} + + + + - {dialog} - - - - + + {dialog} + + + + + ); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/package.json b/vuu-ui/sample-apps/feature-basket-trading/package.json index 1048ffa0d..af623fc3b 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/package.json +++ b/vuu-ui/sample-apps/feature-basket-trading/package.json @@ -56,8 +56,8 @@ "table": "basketTradingConstituentJoin" }, { - "module": "SIMUL", - "table": "instruments" + "module": "BASKET", + "table": "basketConstituent" } ] }, diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx index abf5e53da..b677fd5a6 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/VuuBasketTradingFeature.tsx @@ -18,7 +18,7 @@ export interface BasketTradingFeatureProps { basketSchema: TableSchema; basketTradingSchema: TableSchema; basketTradingConstituentJoinSchema: TableSchema; - instrumentsSchema: TableSchema; + basketConstituentSchema: TableSchema; } const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { @@ -26,7 +26,7 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, } = props; const { @@ -46,7 +46,7 @@ const VuuBasketTradingFeature = (props: BasketTradingFeatureProps) => { basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, }); 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 b1496820e..7b309f74e 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 @@ -1,21 +1,25 @@ .vuuBasketSelector { - --col-gap: 32px; - --basket-selector-height: 61px; + --basket-selector-height: var(--vuuBasketSelector-height, 61px); align-items: center; border: solid 1px var(--vuu-color-gray-45); border-radius: 6px; - display: inline-flex; + container-name: basket-selector; + container-type: size; + display: inline-block; height: var(--basket-selector-height); - min-width: 400px; - padding: 8px; + min-width: 450px; } .vuuBasketSelector-basketDetails { + --col-gap: 32px; display: grid; flex: 1 1 auto; gap: 0px var(--col-gap); - grid-template-columns: max-content min-content min-content 24px; + grid-template-columns: auto auto auto 1fr; grid-template-rows: 18px 1fr; + height: 100%; + padding: 8px; + max-width: 100%; } .vuuBasketSelector-label { color: var(--vuu-color-gray-50); @@ -48,7 +52,7 @@ font-size: 20px; font-weight: 400; line-height: normal; - min-width: 50px; + min-width: 70px; overflow: hidden; position: relative; text-overflow: ellipsis; @@ -74,6 +78,7 @@ --vuu-icon-size: 24px; --vuu-icon-width: 24px; align-self: center; + justify-self: end; grid-column: 4; grid-row: 1 / span 2; } @@ -95,4 +100,15 @@ display: flex; flex: 0 0 36px; justify-content: center; +} + +@container basket-selector (height < 59px) { + .vuuBasketSelector-basketDetails { + grid-template-columns: max-content min-content min-content 1fr; + grid-template-rows: 1fr; + padding: 4px 8px; + } + label { + display: none; + } } \ No newline at end of file diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts index 291288c34..702af2900 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-edit/basketConstituentEditColumns.ts @@ -29,7 +29,7 @@ export default [ }, { name: "ric", pin: "left" }, { name: "description", label: "Name", width: 220 }, - { name: "quantity", editable }, + { name: "quantity" }, { name: "weighting", editable }, { name: "last" }, { name: "bid", type: ticking }, diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx index f27cb1aaa..feb0fd86f 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/basket-table-live/BasketTableLive.tsx @@ -5,13 +5,14 @@ import { useMemo } from "react"; import { ProgressCell, SpreadCell, StatusCell } from "../cell-renderers"; import columns from "./basketConstituentLiveColumns"; -console.log( - `component loaded - ProgressCell ${typeof ProgressCell} - SpreadCell ${typeof SpreadCell} - StatusCell ${typeof StatusCell} - ` -); +if ( + typeof ProgressCell !== "function" || + typeof SpreadCell !== "function" || + typeof StatusCell !== "function" +) { + console.warn("BasketTableLive not all cusatom cell renderers are available"); +} + import "./BasketTableLive.css"; const classBase = "vuuBasketTableLive"; @@ -32,8 +33,6 @@ export const BasketTableLive = ({ [] ); - console.log({ columns }); - return ( ); const inputSide = ( - + Side + Units ); const notionalUSD = ( - + Total USD Not {formatNotional(basket?.totalNotional)} @@ -145,7 +145,7 @@ export const BasketToolbar = ({ ); const notional = ( - + Total Not {formatNotional(basket?.totalNotionalUsd)} @@ -210,5 +210,9 @@ export const BasketToolbar = ({ return toolbarItems; }; - return
{getToolbarItems()}
; + return ( +
+
{getToolbarItems()}
+
+ ); }; 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 572a1bcc6..61178ca34 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 @@ -56,6 +56,7 @@ export const useNewBasketPanel = ({ if (rpcCommand) { basketDataSource .rpcCall?.({ + namedParams: {}, params: [basketId, basketName], rpcName: "createBasket", type: "VIEW_PORT_RPC_CALL", diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts index b370a1d1b..2339e4735 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketContextMenus.ts @@ -9,9 +9,9 @@ import { DataSource } from "@finos/vuu-data"; import { useMemo } from "react"; export const useBasketContextMenus = ({ - dataSourceInstruments, + dataSourceBasketConstituent, }: { - dataSourceInstruments: DataSource; + dataSourceBasketConstituent: DataSource; }) => { const dispatchLayoutAction = useLayoutProviderDispatch(); @@ -47,7 +47,7 @@ export const useBasketContextMenus = ({ allowDragDrop: "drag-copy", id: "basket-instruments", }, - dataSource: dataSourceInstruments, + dataSource: dataSourceBasketConstituent, }, }, title: "Add Ticker", @@ -57,5 +57,5 @@ export const useBasketContextMenus = ({ return false; }, ]; - }, [dataSourceInstruments, dispatchLayoutAction]); + }, [dataSourceBasketConstituent, dispatchLayoutAction]); }; diff --git a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx index d2a4b5f21..28aae02e1 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx @@ -46,7 +46,7 @@ export type BasketTradingHookProps = Pick< | "basketSchema" | "basketTradingSchema" | "basketTradingConstituentJoinSchema" - | "instrumentsSchema" + | "basketConstituentSchema" >; const toDataDto = (dataSourceRow: VuuDataRow, columnMap: ColumnMap) => { @@ -67,7 +67,7 @@ export const useBasketTrading = ({ basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, }: BasketTradingHookProps) => { const { load, save } = useViewContext(); @@ -81,7 +81,7 @@ export const useBasketTrading = ({ dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, - dataSourceInstruments, + dataSourceBasketConstituent, onSendToMarket, onTakeOffMarket, } = useBasketTradingDataSources({ @@ -89,7 +89,7 @@ export const useBasketTrading = ({ basketSchema, basketTradingSchema, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, }); const [basket, setBasket] = useState(); @@ -121,9 +121,6 @@ export const useBasketTrading = ({ ); useMemo(() => { - console.log( - `subscribe to BT COntrol ${dataSourceBasketTradingControl.status}` - ); dataSourceBasketTradingControl.subscribe( { range: { from: 0, to: 1 }, @@ -215,7 +212,7 @@ export const useBasketTrading = ({ ); const [menuBuilder, menuActionHandler] = useBasketContextMenus({ - dataSourceInstruments, + dataSourceBasketConstituent, }); const handleRpcResponse = useCallback((response) => { 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 b9e3ca59e..a9e0f4c78 100644 --- a/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts +++ b/vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts @@ -1,25 +1,32 @@ import { useViewContext } from "@finos/vuu-layout"; -import { DataSource, RemoteDataSource, TableSchema } from "@finos/vuu-data"; +import { + DataSource, + DataSourceConfig, + RemoteDataSource, + TableSchema, + ViewportRpcResponse, +} from "@finos/vuu-data"; import { useCallback, useMemo } from "react"; import { BasketTradingFeatureProps } from "./VuuBasketTradingFeature"; -import { VuuFilter } from "@finos/vuu-protocol-types"; +import { NotificationLevel, useNotifications } from "@finos/vuu-popups"; export type basketDataSourceKey = | "data-source-basket" | "data-source-basket-trading-control" | "data-source-basket-trading-search" | "data-source-basket-trading-constituent-join" - | "data-source-instruments"; + | "data-source-basket-constituent"; -const NO_FILTER = { filter: "" }; +const NO_CONFIG = {}; export const useBasketTradingDataSources = ({ basketSchema, basketInstanceId, basketTradingSchema, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, }: BasketTradingFeatureProps & { basketInstanceId: string }) => { + const { notify } = useNotifications(); const { id, loadSession, saveSession, title } = useViewContext(); const [ @@ -27,18 +34,21 @@ export const useBasketTradingDataSources = ({ dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, - dataSourceInstruments, + dataSourceBasketConstituent, ] = useMemo(() => { - const basketFilter: VuuFilter = basketInstanceId + const basketFilter: DataSourceConfig = basketInstanceId ? { - filter: `instanceId = "${basketInstanceId}"`, + filter: { + filter: `instanceId = "${basketInstanceId}"`, + }, } - : NO_FILTER; + : NO_CONFIG; + const dataSourceConfig: [ basketDataSourceKey, TableSchema, number, - VuuFilter? + DataSourceConfig? ][] = [ ["data-source-basket", basketSchema, 100], [ @@ -54,16 +64,23 @@ export const useBasketTradingDataSources = ({ 100, basketFilter, ], - ["data-source-instruments", instrumentsSchema, 100], + [ + "data-source-basket-constituent", + basketConstituentSchema, + 100, + // { + // sort: { sortDefs: [{ column: "description", sortType: "A" }] }, + // }, + ], ]; const dataSources: DataSource[] = []; - for (const [key, schema, bufferSize, filter] of dataSourceConfig) { + for (const [key, schema, bufferSize, config] of dataSourceConfig) { let dataSource = loadSession?.(key) as RemoteDataSource; if (dataSource === undefined) { dataSource = new RemoteDataSource({ + ...config, bufferSize, - filter, viewport: `${id}-${key}`, table: schema.table, columns: schema.columns.map((col) => col.name), @@ -79,7 +96,7 @@ export const useBasketTradingDataSources = ({ basketTradingSchema, basketInstanceId, basketTradingConstituentJoinSchema, - instrumentsSchema, + basketConstituentSchema, loadSession, id, title, @@ -89,19 +106,24 @@ export const useBasketTradingDataSources = ({ const handleSendToMarket = useCallback( (basketInstanceId: string) => { dataSourceBasketTradingControl - .rpcCall?.({ + .rpcCall?.({ namedParams: {}, params: [basketInstanceId], rpcName: "sendToMarket", type: "VIEW_PORT_RPC_CALL", }) .then((response) => { - console.log(`response from sendToMarket call`, { - response, - }); + if (response?.action.type === "VP_RPC_FAILURE") { + notify({ + type: NotificationLevel.Error, + header: "Failed to Send to market", + body: "Please contact your support team", + }); + console.error(response.action.msg); + } }); }, - [dataSourceBasketTradingControl] + [dataSourceBasketTradingControl, notify] ); const handleTakeOffMarket = useCallback(() => { @@ -113,7 +135,7 @@ export const useBasketTradingDataSources = ({ dataSourceBasketTradingControl, dataSourceBasketTradingSearch, dataSourceBasketTradingConstituentJoin, - dataSourceInstruments, + dataSourceBasketConstituent, onSendToMarket: handleSendToMarket, onTakeOffMarket: handleTakeOffMarket, }; diff --git a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx index 6639b420a..3659e1630 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx @@ -147,7 +147,8 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { onAvailableColumnsChange: handleAvailableColumnsChange, onConfigChange: handleTableConfigChange, onFeatureInvocation: handleVuuFeatureInvoked, - renderBufferSize: 50, + // renderBufferSize: 50, + renderBufferSize: 0, }; // It is important that these values are not assigned in advance. They diff --git a/vuu-ui/sample-apps/feature-filter-table/src/useSessionDataSource.ts b/vuu-ui/sample-apps/feature-filter-table/src/useSessionDataSource.ts index e690e0c93..f18120147 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/useSessionDataSource.ts +++ b/vuu-ui/sample-apps/feature-filter-table/src/useSessionDataSource.ts @@ -66,7 +66,8 @@ export const useSessionDataSource = ({ tableSchema.columns.map((col) => col.name); ds = new RemoteDataSource({ - bufferSize: 200, + bufferSize: 0, + // bufferSize: 200, viewport: id, table: tableSchema.table, ...dataSourceConfigFromState, diff --git a/vuu-ui/sample-apps/feature-instrument-tiles/package.json b/vuu-ui/sample-apps/feature-instrument-tiles/package.json index 6227baecb..f13325c2d 100644 --- a/vuu-ui/sample-apps/feature-instrument-tiles/package.json +++ b/vuu-ui/sample-apps/feature-instrument-tiles/package.json @@ -42,10 +42,12 @@ }, "vuu": { "featureProps": { - "schemas": { - "module": "SIMUL", - "table": "instrumentPrices" - } + "schemas": [ + { + "module": "SIMUL", + "table": "instrumentPrices" + } + ] }, "leftNavLocation": "vuu-features" } diff --git a/vuu-ui/sample-apps/feature-instrument-tiles/src/VuuInstrumentTilesFeature.tsx b/vuu-ui/sample-apps/feature-instrument-tiles/src/VuuInstrumentTilesFeature.tsx index df4488fd9..7ec85c666 100644 --- a/vuu-ui/sample-apps/feature-instrument-tiles/src/VuuInstrumentTilesFeature.tsx +++ b/vuu-ui/sample-apps/feature-instrument-tiles/src/VuuInstrumentTilesFeature.tsx @@ -10,7 +10,6 @@ import { buildColumnMap, metadataKeys } from "@finos/vuu-utils"; import { useCallback, useEffect, useMemo } from "react"; import { InstrumentTile } from "./InstrumentTile"; import { InstrumentTileContainer } from "./InstrumentTileContainer"; -// import { useDataSource } from "@finos/vuu-data-react"; import { useDataSource } from "./useDataSource"; import "./VuuInstrumentTilesFeature.css"; @@ -18,13 +17,13 @@ import "./VuuInstrumentTilesFeature.css"; const classBase = "VuuInstrumentTilesFeature"; export interface InstrumentTilesFeatureProps { - tableSchema: TableSchema; + instrumentPricesSchema: TableSchema; } const { KEY } = metadataKeys; const VuuInstrumentTilesFeature = ({ - tableSchema, + instrumentPricesSchema, }: InstrumentTilesFeatureProps) => { const { id, save, loadSession, saveSession, title } = useViewContext(); @@ -65,8 +64,8 @@ const VuuInstrumentTilesFeature = ({ ds = new RemoteDataSource({ bufferSize: 200, viewport: id, - table: tableSchema.table, - columns: tableSchema.columns.map((col) => col.name), + table: instrumentPricesSchema.table, + columns: instrumentPricesSchema.columns.map((col) => col.name), filter, title, }); @@ -79,8 +78,8 @@ const VuuInstrumentTilesFeature = ({ id, loadSession, saveSession, - tableSchema.columns, - tableSchema.table, + instrumentPricesSchema.columns, + instrumentPricesSchema.table, title, ]); diff --git a/vuu-ui/sample-apps/feature-vuu-blotter/index.ts b/vuu-ui/sample-apps/feature-vuu-blotter/index.ts deleted file mode 100644 index 1e94b6afe..000000000 --- a/vuu-ui/sample-apps/feature-vuu-blotter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import VuuBlotter from "./src/VuuBlotter"; -export default VuuBlotter; diff --git a/vuu-ui/sample-apps/feature-vuu-blotter/package.json b/vuu-ui/sample-apps/feature-vuu-blotter/package.json deleted file mode 100644 index 964636da1..000000000 --- a/vuu-ui/sample-apps/feature-vuu-blotter/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "feature-vuu-blotter", - "version": "0.0.26", - "description": "Vuu Blotter", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "node ../../scripts/build-feature.mjs", - "start": "serve -p 5002 ../../deployed_apps/app-vuu-example" - }, - "private": true, - "keywords": [], - "author": "heswell", - "license": "Apache-2.0", - "sideEffects": [ - "**/*.css" - ], - "devDependencies": {}, - "dependencies": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid": "0.0.26", - "@finos/vuu-filters": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-theme": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", - "@salt-ds/lab": "1.0.0-alpha.15" - }, - "peerDependencies": { - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "engines": { - "node": ">=16.0.0" - } -} diff --git a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.css b/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.css deleted file mode 100644 index ba9ccedce..000000000 --- a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.css +++ /dev/null @@ -1,19 +0,0 @@ -.vuDialog, /* until we correctly set the className on FilteredGrid view in a dialog */ -.vuuBlotter { - --hwDialog-margin: 200px auto 0 auto; - --hwParsedInput-background: white; - --hwParsedInput-input-font-size: 12px; - --hwParsedInput-height: 28px; - --hwParsedInput-border-style: none none none solid; - --hwParsedInput-width: calc(100% - var(--hwParsedInput-height)); - --vuuDataGrid-flex: 100% 0 0; - --vuuDataGrid-font-size: 10px; - --vuuDataGridCell-border-style: none; - --vuuDataGridRow-background-odd: var(--salt-palette-neutral-background-high); - max-height: 100%; -} - -.vuuBlotter-gridContainer { - height: calc(100% - 22px); - inset: var(--hwParsedInput-height) 0 0 0; -} diff --git a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx b/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx deleted file mode 100644 index dbd16ab7a..000000000 --- a/vuu-ui/sample-apps/feature-vuu-blotter/src/VuuBlotter.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { - ConfigChangeMessage, - DataSourceVisualLinkCreatedMessage, - isViewportMenusAction, - isVisualLinkCreatedAction, - isVisualLinkRemovedAction, - isVisualLinksAction, - RemoteDataSource, - TableSchema, -} from "@finos/vuu-data"; -import { MenuActionConfig, useVuuMenuActions } from "@finos/vuu-data-react"; -import { Grid, GridProvider } from "@finos/vuu-datagrid"; -import { GridAction, KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { Filter, FilterState } from "@finos/vuu-filter-types"; -import { - addFilter, - FilterInput, - useFilterSuggestionProvider, -} from "@finos/vuu-filters"; -import { useViewContext } from "@finos/vuu-layout"; -import { ContextMenuProvider } from "@finos/vuu-popups"; -import { - LinkDescriptorWithLabel, - VuuGroupBy, - VuuMenu, - VuuSort, -} from "@finos/vuu-protocol-types"; -import { - FeatureProps, - ShellContextProps, - useShellContext, -} from "@finos/vuu-shell"; -import { filterAsQuery } from "@finos/vuu-utils"; -import { Button } from "@salt-ds/core"; -import { LinkedIcon } from "@salt-ds/icons"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -import "./VuuBlotter.css"; - -const classBase = "vuuBlotter"; - -const CONFIG_KEYS = ["filter", "filterQuery", "groupBy", "sort"]; - -type BlotterConfig = { - columns?: KeyedColumnDescriptor[]; - groupBy?: VuuGroupBy; - sort?: VuuSort; - "visual-link"?: DataSourceVisualLinkCreatedMessage; -}; -export interface FilteredGridProps extends FeatureProps { - schema: TableSchema; -} - -const applyDefaults = ( - { columns, table }: TableSchema, - getDefaultColumnConfig?: ShellContextProps["getDefaultColumnConfig"] -) => { - if (typeof getDefaultColumnConfig === "function") { - return columns.map((column) => { - const config = getDefaultColumnConfig(table.table, column.name); - if (config) { - return { - ...column, - ...config, - }; - } else { - return column; - } - }); - } else { - return columns; - } -}; - -const VuuBlotter = ({ schema, ...props }: FilteredGridProps) => { - const { id, dispatch, load, purge, save, loadSession, saveSession, title } = - useViewContext(); - const config = useMemo(() => load?.() as BlotterConfig | undefined, [load]); - const { getDefaultColumnConfig, handleRpcResponse } = useShellContext(); - const [filterState, setFilterState] = useState({ - filter: undefined, - filterQuery: "", - }); - - const suggestionProvider = useFilterSuggestionProvider({ - columns: schema.columns, - table: schema.table, - }); - - const dataSource: RemoteDataSource = useMemo(() => { - let ds = loadSession?.("data-source") as RemoteDataSource; - if (ds) { - return ds; - } - const columns = schema.columns.map((col) => col.name); - ds = new RemoteDataSource({ - viewport: id, - table: schema.table, - ...config, - columns, - title, - }); - saveSession?.(ds, "data-source"); - return ds; - // Note: despite the dependency array, because we load dataStore from session - // after initial load, changes to the following dependencies will not cause - // us to create a new dataSource, which is correct. - }, [config, id, loadSession, saveSession, schema, title]); - - useEffect(() => { - dataSource.enable(); - return () => { - dataSource.disable(); - }; - }, [dataSource]); - - useEffect(() => { - if (title !== dataSource.title) { - dataSource.title = title; - } - }, [dataSource, title]); - - const removeVisualLink = useCallback(() => { - dataSource.visualLink = undefined; - }, [dataSource]); - - const dispatchGridAction = useCallback( - (action: GridAction) => { - if (isVisualLinksAction(action)) { - saveSession?.(action.links, "visual-links"); - return true; - } else if (isVisualLinkCreatedAction(action)) { - dispatch?.({ - type: "add-toolbar-contribution", - location: "post-title", - content: ( - - ), - }); - save?.(action, "visual-link"); - return true; - } else if (isVisualLinkRemovedAction(action)) { - dispatch?.({ - type: "remove-toolbar-contribution", - location: "post-title", - }); - purge?.("visual-link"); - return true; - } else if (isViewportMenusAction(action)) { - saveSession?.(action.menu, "vs-context-menu"); - return true; - } - return false; - }, - [dispatch, purge, removeVisualLink, save, saveSession] - ); - - const handleConfigChange = useCallback( - (update: ConfigChangeMessage) => { - switch (update.type) { - default: - for (const [key, state] of Object.entries(update)) { - if (CONFIG_KEYS.includes(key)) { - save?.(state, key); - } - } - } - }, - [save] - ); - - // It is important that these values are not assigned in advance. They - // are accessed at the point of construction of ContextMenu - const menuActionConfig: MenuActionConfig = useMemo( - () => ({ - get visualLink() { - return load?.("visual-link") as DataSourceVisualLinkCreatedMessage; - }, - get visualLinks() { - return loadSession?.("visual-links") as LinkDescriptorWithLabel[]; - }, - get vuuMenu() { - return loadSession?.("vs-context-menu") as VuuMenu; - }, - }), - [load, loadSession] - ); - - const { buildViewserverMenuOptions, handleMenuAction } = useVuuMenuActions({ - dataSource, - menuActionConfig, - onRpcResponse: handleRpcResponse, - }); - - const namedFilters = useMemo(() => new Map(), []); - - const handleSubmitFilter = useCallback( - ( - newFilter: Filter | undefined, - filterQuery: string, - mode = "add", - filterName?: string - ) => { - let newFilterState: FilterState; - if (newFilter && (mode === "and" || mode === "or")) { - const fullFilter = addFilter(filterState.filter, newFilter, { - combineWith: mode, - }) as Filter; - newFilterState = { - filter: fullFilter, - filterQuery: filterAsQuery(fullFilter), - filterName, - }; - } else { - newFilterState = { - filter: newFilter, - filterQuery, - filterName, - }; - } - - dataSource.filter = { - filter: newFilterState.filterQuery, - filterStruct: newFilterState.filter, - }; - setFilterState(newFilterState); - if (filterName && newFilterState.filter) { - namedFilters.set(filterName, newFilterState.filterQuery); - } - }, - [dataSource, filterState.filter, namedFilters] - ); - - const configColumns = config?.columns; - - const columns = useMemo(() => { - return configColumns || applyDefaults(schema, getDefaultColumnConfig); - }, [configColumns, getDefaultColumnConfig, schema]); - - return ( - -
- - -
- -
-
-
-
- ); -}; - -VuuBlotter.displayName = "FilteredGrid"; - -export default VuuBlotter; diff --git a/vuu-ui/sample-apps/feature-vuu-table/index.ts b/vuu-ui/sample-apps/feature-vuu-table/index.ts deleted file mode 100644 index f039814e9..000000000 --- a/vuu-ui/sample-apps/feature-vuu-table/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import VuuTable from "./src/vuuTable"; -export default VuuTable; diff --git a/vuu-ui/sample-apps/feature-vuu-table/package.json b/vuu-ui/sample-apps/feature-vuu-table/package.json deleted file mode 100644 index a35d4c398..000000000 --- a/vuu-ui/sample-apps/feature-vuu-table/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "feature-vuu-table", - "version": "0.0.26", - "description": "Vuu Table", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "node ../../scripts/build-feature.mjs", - "start": "serve -p 5002 ../../deployed_apps/app-vuu-example" - }, - "private": true, - "keywords": [], - "author": "heswell", - "license": "Apache-2.0", - "sideEffects": [ - "**/*.css" - ], - "devDependencies": {}, - "dependencies": { - "@finos/vuu-data": "0.0.26", - "@finos/vuu-data-react": "0.0.26", - "@finos/vuu-datagrid-types": "0.0.26", - "@finos/vuu-filters": "0.0.26", - "@finos/vuu-filter-types": "0.0.26", - "@finos/vuu-layout": "0.0.26", - "@finos/vuu-popups": "0.0.26", - "@finos/vuu-protocol-types": "0.0.26", - "@finos/vuu-shell": "0.0.26", - "@finos/vuu-table": "0.0.26", - "@finos/vuu-table-extras": "0.0.26", - "@finos/vuu-theme": "0.0.26", - "@finos/vuu-utils": "0.0.26", - "@salt-ds/core": "1.8.0", - "@salt-ds/icons": "1.5.1", - "@salt-ds/lab": "1.0.0-alpha.15" - }, - "peerDependencies": { - "classnames": "^2.3.1", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "engines": { - "node": ">=16.0.0" - } -} diff --git a/vuu-ui/sample-apps/feature-vuu-table/src/ConfigurableDataTable.tsx b/vuu-ui/sample-apps/feature-vuu-table/src/ConfigurableDataTable.tsx deleted file mode 100644 index cc743d6fa..000000000 --- a/vuu-ui/sample-apps/feature-vuu-table/src/ConfigurableDataTable.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { GridConfig } from "@finos/vuu-datagrid-types"; -import { Dialog } from "@finos/vuu-popups"; -import { Table, TablePropsDeprecated as TableProps } from "@finos/vuu-table"; -import { ReactElement, useCallback, useState } from "react"; - -export const ConfigurableDataTable = ({ - config, - dataSource, - ...restProps -}: TableProps) => { - const [dialogContent, setDialogContent] = useState(null); - const [tableConfig] = useState>(config); - - const hideSettings = useCallback(() => { - setDialogContent(null); - }, []); - - return ( - <> -
- - {dialogContent} - - - ); -}; diff --git a/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.css b/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.css deleted file mode 100644 index 1dc4f41e7..000000000 --- a/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.css +++ /dev/null @@ -1,35 +0,0 @@ -.vuDialog, /* until we correctly set the className on FilteredGrid view in a dialog */ -.vuuTable { - --hwDialog-margin: 200px auto 0 auto; - --hwParsedInput-background: white; - --hwParsedInput-input-font-size: 12px; - --hwParsedInput-height: 28px; - --hwParsedInput-border-style: none none none solid; - --hwParsedInput-width: calc(100% - var(--hwParsedInput-height)); - --vuuDataGrid-flex: 100% 0 0; - --vuuDataGrid-font-size: 10px; - --vuuDataGridCell-border-style: none; - --vuuDataGridRow-background-odd: var(--salt-palette-neutral-background-high); - display: flex; - flex-direction: column; -} - -.vuuTable .vuuFilterInput { - flex: 22px 0 0; -} - -.vuuTable .vuuTable-footer { - flex: 22px 0 0; - --saltToolbar-height: 20px; - --saltToolbar-background: var(--salt-container-primary-background); - border-top: solid 1px var(--salt-container-primary-borderColor); - color: var(--salt-text-secondary-foreground); - -} - - -.vuuTable-gridContainer { - height: calc(100% - 22px); - inset: var(--hwParsedInput-height) 0 0 0; -} - diff --git a/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.tsx b/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.tsx deleted file mode 100644 index de7da7910..000000000 --- a/vuu-ui/sample-apps/feature-vuu-table/src/vuuTable.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { - DataSource, - DataSourceConfig, - DataSourceVisualLinkCreatedMessage, - RemoteDataSource, - TableSchema, - VuuFeatureInvocationMessage, -} from "@finos/vuu-data"; -import { MenuActionConfig, useVuuMenuActions } from "@finos/vuu-data-react"; -import { GridConfig } from "@finos/vuu-datagrid-types"; -import { Filter, FilterState } from "@finos/vuu-filter-types"; -import { - addFilter, - FilterInput, - useFilterSuggestionProvider, -} from "@finos/vuu-filters"; -import { useViewContext } from "@finos/vuu-layout"; -import { ContextMenuProvider } from "@finos/vuu-popups"; -import { LinkDescriptorWithLabel, VuuMenu } from "@finos/vuu-protocol-types"; -import { - FeatureProps, - ShellContextProps, - useShellContext, -} from "@finos/vuu-shell"; -import { DataSourceStats } from "@finos/vuu-table-extras"; -import { filterAsQuery } from "@finos/vuu-utils"; -import { Button } from "@salt-ds/core"; -import { LinkedIcon } from "@salt-ds/icons"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ConfigurableDataTable } from "./ConfigurableDataTable"; - -import "./vuuTable.css"; - -const classBase = "vuuTable"; - -type BlotterConfig = { - "datasource-config"?: DataSourceConfig; - "table-config"?: Omit; -}; - -const NO_CONFIG: BlotterConfig = {}; - -export interface FilteredTableProps extends FeatureProps { - schema: TableSchema; -} - -const applyDefaults = ( - { columns, table }: TableSchema, - getDefaultColumnConfig?: ShellContextProps["getDefaultColumnConfig"] -) => { - if (typeof getDefaultColumnConfig === "function") { - return columns.map((column) => { - const config = getDefaultColumnConfig(table.table, column.name); - if (config) { - return { - ...column, - ...config, - }; - } else { - return column; - } - }); - } else { - return columns; - } -}; - -const VuuTable = ({ schema, ...props }: FilteredTableProps) => { - const { id, dispatch, load, save, loadSession, saveSession, title } = - useViewContext(); - const { - "datasource-config": dataSourceConfigFromState, - "table-config": tableConfigFromState, - } = useMemo(() => (load?.() ?? NO_CONFIG) as BlotterConfig, [load]); - - const { getDefaultColumnConfig, handleRpcResponse } = useShellContext(); - const [filterState, setFilterState] = useState({ - filter: undefined, - filterQuery: "", - }); - - const configColumns = tableConfigFromState?.columns; - - const tableConfig = useMemo( - () => ({ - columns: configColumns || applyDefaults(schema, getDefaultColumnConfig), - }), - [configColumns, getDefaultColumnConfig, schema] - ); - - const tableConfigRef = useRef>(tableConfig); - - const suggestionProvider = useFilterSuggestionProvider({ - columns: schema.columns, - table: schema.table, - }); - - const handleDataSourceConfigChange = useCallback( - (config: DataSourceConfig | undefined, confirmed?: boolean) => { - // confirmed / unconfirmed messages are used for UI updates, not state saving - if (confirmed === undefined) { - save?.(config, "datasource-config"); - } - }, - [save] - ); - - const handleTableConfigChange = useCallback( - (config: Omit) => { - save?.(config, "table-config"); - tableConfigRef.current = config; - }, - [save] - ); - - const dataSource: DataSource = useMemo(() => { - let ds = loadSession?.("data-source") as RemoteDataSource; - if (ds) { - return ds; - } - const columns = - dataSourceConfigFromState?.columns ?? - schema.columns.map((col) => col.name); - - ds = new RemoteDataSource({ - bufferSize: 200, - viewport: id, - table: schema.table, - ...dataSourceConfigFromState, - columns, - title, - }); - ds.on("config", handleDataSourceConfigChange); - saveSession?.(ds, "data-source"); - return ds; - }, [ - dataSourceConfigFromState, - handleDataSourceConfigChange, - id, - loadSession, - saveSession, - schema.columns, - schema.table, - title, - ]); - - useEffect(() => { - dataSource.resume?.(); - return () => { - // suspend activity on the dataSource when component is unmounted - dataSource.suspend?.(); - }; - }, [dataSource]); - - const removeVisualLink = useCallback(() => { - dataSource.visualLink = undefined; - }, [dataSource]); - - const handleVuuFeatureInvoked = useCallback( - (message: VuuFeatureInvocationMessage) => { - if (message.type === "vuu-link-created") { - dispatch?.({ - type: "add-toolbar-contribution", - location: "post-title", - content: ( - - ), - }); - } else { - dispatch?.({ - type: "remove-toolbar-contribution", - location: "post-title", - }); - } - }, - [dispatch, removeVisualLink] - ); - - // It is important that these values are not assigned in advance. They - // are accessed at the point of construction of ContextMenu - const menuActionConfig: MenuActionConfig = useMemo( - () => ({ - get visualLink() { - return load?.("visual-link") as DataSourceVisualLinkCreatedMessage; - }, - get visualLinks() { - return loadSession?.("vuu-links") as LinkDescriptorWithLabel[]; - }, - get vuuMenu() { - return loadSession?.("vuu-menu") as VuuMenu; - }, - }), - [load, loadSession] - ); - - const { buildViewserverMenuOptions, handleMenuAction } = useVuuMenuActions({ - dataSource, - menuActionConfig, - onRpcResponse: handleRpcResponse, - }); - - useEffect(() => { - if (title !== dataSource.title) { - dataSource.title = title; - } - }, [dataSource, title]); - - const namedFilters = useMemo(() => new Map(), []); - - const handleSubmitFilter = useCallback( - ( - newFilter: Filter | undefined, - filterQuery: string, - mode = "add", - filterName?: string - ) => { - let newFilterState: FilterState; - if (newFilter && (mode === "and" || mode === "or")) { - const fullFilter = addFilter(filterState.filter, newFilter, { - combineWith: mode, - }) as Filter; - newFilterState = { - filter: fullFilter, - filterQuery: filterAsQuery(fullFilter), - filterName, - }; - } else { - newFilterState = { - filter: newFilter, - filterQuery, - filterName, - }; - } - - dataSource.filter = { - filter: newFilterState.filterQuery, - filterStruct: newFilterState.filter, - }; - setFilterState(newFilterState); - if (filterName && newFilterState.filter) { - namedFilters.set(filterName, newFilterState.filterQuery); - } - }, - [dataSource, filterState.filter, namedFilters] - ); - - return ( - -
- -
- -
-
- -
-
-
- ); -}; - -VuuTable.displayName = "VuuTable"; - -export default VuuTable; diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index 29f89b913..f0d83bf9e 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -1,15 +1,13 @@ import { byModule } from "@finos/vuu-data"; -import { - registerComponent, - useLayoutContextMenuItems, -} from "@finos/vuu-layout"; -import { ContextMenuProvider, useDialog } from "@finos/vuu-popups"; +import { registerComponent } from "@finos/vuu-layout"; +import { NotificationsProvider, useDialog } from "@finos/vuu-popups"; import { FeatureConfig, FeatureProps, LayoutManagementProvider, LeftNav, Shell, + SidePanelProps, } from "@finos/vuu-shell"; import { ColumnSettingsPanel, @@ -76,7 +74,7 @@ const features: FeatureProps[] = [ basketSchema: schemas.basket, basketTradingSchema: schemas.basketTrading, basketTradingConstituentJoinSchema: schemas.basketTradingConstituentJoin, - instrumentsSchema: schemas.instruments, + basketConstituentSchema: schemas.basketConstituent, }, }, ]; @@ -94,9 +92,7 @@ const tableFeatures: FeatureProps[] = Object.values( })); const ShellWithNewTheme = () => { - const { dialog, setDialogState } = useDialog(); - const { buildMenuOptions, handleMenuAction } = - useLayoutContextMenuItems(setDialogState); + const { dialog } = useDialog(); const dragSource = useMemo( () => ({ @@ -105,45 +101,44 @@ const ShellWithNewTheme = () => { [] ); + const leftSidePanelProps = useMemo( + () => ({ + children: , + sizeOpen: 240, + }), + [] + ); + return ( - - - - } - loginUrl={window.location.toString()} - user={user} - style={ - { - "--vuuShell-height": "100vh", - "--vuuShell-width": "100vw", - } as CSSProperties - } - > - {dialog} - - - + + + {dialog} + + ); }; export const ShellWithNewThemeAndLayoutManagement = () => { return ( - - - + + + + + ); }; diff --git a/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.css b/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.css deleted file mode 100644 index 7a8b43bb1..000000000 --- a/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.css +++ /dev/null @@ -1,7 +0,0 @@ -#drag-canvas { - position: absolute; -} - -.Steve { - --grid-row-height: 32px; -} diff --git a/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.tsx b/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.tsx deleted file mode 100644 index 951aade1d..000000000 --- a/vuu-ui/showcase/src/examples/DataGrid/Grid.examples.tsx +++ /dev/null @@ -1,673 +0,0 @@ -import { ConfigChangeHandler } from "@finos/vuu-data"; -import { Grid } from "@finos/vuu-datagrid"; -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { Flexbox, View } from "@finos/vuu-layout"; -import { Dialog } from "@finos/vuu-popups"; -import { getAllSchemas } from "@finos/vuu-data-test"; -import { - Button, - FormField, - Input, - ToggleButton, - ToggleButtonGroup, -} from "@salt-ds/core"; -import { FormLabel } from "@salt-ds/lab"; -import { - ChangeEvent, - ReactElement, - SyntheticEvent, - useCallback, - useMemo, - useRef, - useState, -} from "react"; -import { ErrorDisplay, useTestDataSource } from "../utils"; -import { instrumentSchema } from "./columnMetaData"; - -import "./Grid.examples.css"; - -let displaySequence = 1; - -type GridBufferOptions = { - bufferSize: number; - renderBufferSize: number; -}; - -export const DefaultGrid = () => { - const tables = useMemo( - () => ["instruments", "orders", "parentOrders", "prices"], - [] - ); - - const calculatedColumns: ColumnDescriptor[] = useMemo( - () => [ - { - name: "notional", - expression: "=price*quantity", - serverDataType: "double", - type: { - name: "number", - formatting: { - decimals: 2, - }, - }, - }, - { - name: "isBuy", - expression: '=if(side="Sell","N","Y")', - serverDataType: "char", - }, - { - name: "CcySort", - expression: '=if(ccy="Gbp",1,if(ccy="USD",2,3))', - serverDataType: "char", - width: 60, - }, - { - name: "CcyLower", - expression: "=lower(ccy)", - serverDataType: "string", - width: 60, - }, - { - name: "AccountUpper", - expression: "=upper(account)", - label: "ACCOUNT", - serverDataType: "string", - }, - { - name: "ExchangeCcy", - expression: '=concatenate("---", exchange,"...",ccy, "---")', - serverDataType: "string", - }, - { - name: "ExchangeIsNY", - expression: '=starts(exchange,"N")', - serverDataType: "boolean", - }, - // { - // name: "Text", - // expression: "=text(quantity)", - // serverDataType: "string", - // }, - ], - [] - ); - - const [renderBufferSize, setRenderBufferSize] = useState( - 0 - ); - const [bufferSize, setBufferSize] = useState(0); - const [gridBufferOptions, setGridBufferOptions] = useState( - { - bufferSize: 100, - renderBufferSize: 0, - } - ); - const [selectedIndex, setSelectedIndex] = useState(0); - const [dialogContent, setDialogContent] = useState(null); - const schemas = getAllSchemas(); - const { columns, dataSource, error } = useTestDataSource({ - bufferSize: gridBufferOptions.renderBufferSize, - calculatedColumns: selectedIndex === 2 ? calculatedColumns : undefined, - schemas, - tablename: tables[selectedIndex], - }); - - const handleChange = (evt: SyntheticEvent) => { - const { value } = evt.target as HTMLButtonElement; - setSelectedIndex(parseInt(value)); - }; - - const hideSettings = useCallback(() => { - setDialogContent(null); - }, []); - - const handleRenderBufferSizeChange = useCallback( - (evt: ChangeEvent) => { - const value = parseInt((evt.target as HTMLInputElement).value || "-1"); - if (Number.isFinite(value) && value > 0) { - setRenderBufferSize(value); - } else { - setBufferSize(undefined); - } - }, - [] - ); - - // const handleBufferSizeChange = useCallback((evt: ChangeEvent) => { - // const value = parseInt((evt.target as HTMLInputElement).value || "-1"); - // if (Number.isFinite(value) && value > 0) { - // setBufferSize(value); - // } else { - // setBufferSize(undefined); - // } - // }, []); - - const applyBufferSizes = useCallback(() => { - setGridBufferOptions({ - bufferSize: bufferSize ?? 100, - renderBufferSize: renderBufferSize ?? 0, - }); - }, [bufferSize, renderBufferSize]); - - if (error) { - return {error}; - } - - return ( - <> -
- - Instruments - Orders - Parent Orders - Prices - -
- - - - {dialogContent} - -
-
- - {/* - - */} - -
-
- - ); -}; -DefaultGrid.displaySequence = displaySequence++; - -export const BasicGrid = () => { - const schemas = getAllSchemas(); - const { columns, dataSource, error } = useTestDataSource({ - schemas, - tablename: "instruments", - }); - const gridRef = useRef(null); - const [rowHeight, setRowHeight] = useState(18); - - const incrementProp = () => { - setRowHeight((value) => value + 1); - }; - - const decrementProp = () => { - setRowHeight((value) => value - 1); - }; - - const incrementCssProperty = () => { - if (gridRef.current) { - const rowHeight = parseInt( - getComputedStyle(gridRef.current).getPropertyValue( - "--hw-grid-row-height" - ) - ); - gridRef.current.style.setProperty( - "--grid-row-height", - `${rowHeight + 1}px` - ); - } - }; - - const decrementCssProperty = () => { - if (gridRef.current) { - const rowHeight = parseInt( - getComputedStyle(gridRef.current).getPropertyValue( - "--hw-grid-row-height" - ) - ); - gridRef.current?.style.setProperty( - "--grid-row-height", - `${rowHeight - 1}px` - ); - } - }; - - const setLowDensity = () => { - gridRef.current?.style.setProperty("--grid-row-height", `32px`); - }; - const setHighDensity = () => { - gridRef.current?.style.setProperty("--grid-row-height", `20px`); - }; - - const handleConfigChange: ConfigChangeHandler = (config) => { - console.log(`handleConfigChange ${JSON.stringify(config, null, 2)}`); - }; - - if (error) { - return {error}; - } - - return ( - <> - -
- - - - -
- - - - ); -}; - -BasicGrid.displaySequence = displaySequence++; - -export const PersistConfig = () => { - const configRef = useRef({ - columns: instrumentSchema.columns, - }); - const [configDisplay, setConfigDisplay] = useState(() => configRef.current); - const [config, setConfig] = useState(() => configRef.current); - - const applyConfig = () => { - setConfig(configRef.current); - }; - - const schemas = getAllSchemas(); - const { columns, dataSource, error } = useTestDataSource({ - schemas, - tablename: "instruments", - }); - - const handleConfigChange = useCallback( - (updates) => { - configRef.current = { ...config, ...updates }; - setConfigDisplay(configRef.current); - }, - [config] - ); - - const gridStyles = ` - .StoryGrid { - --hwDataGridCell-border-style: none; - --hwDataGridRow-background-odd: var(--surface1); - --hwDataGrid-font-size: 10px; - } - `; - - console.log(`render`, config); - - if (error) { - return {error}; - } - - return ( - <> - -
- -
- -