diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts index c9e09d18c..d8cf894b8 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts @@ -299,7 +299,19 @@ export class ArrayDataSource this.emit("row-selection", selected, this.#selectedRowsCount); } - openTreeNode(key: string) { + private getRowKey(keyOrIndex: string | number) { + if (typeof keyOrIndex === "string") { + return keyOrIndex; + } + const row = this.getRowAtIndex(keyOrIndex); + if (row === undefined) { + throw Error(`row not found at index ${keyOrIndex}`); + } + return row?.[KEY]; + } + + openTreeNode(keyOrIndex: string | number) { + const key = this.getRowKey(keyOrIndex); this.openTreeNodes.push(key); this.processedData = expandGroup( this.openTreeNodes, @@ -312,7 +324,8 @@ export class ArrayDataSource this.setRange(resetRange(this.#range), true); } - closeTreeNode(key: string) { + closeTreeNode(keyOrIndex: string | number) { + const key = this.getRowKey(keyOrIndex); this.openTreeNodes = this.openTreeNodes.filter((value) => value !== key); if (this.processedData) { this.processedData = collapseGroup(key, this.processedData); @@ -357,7 +370,7 @@ export class ArrayDataSource return this._config; } - set config(config: WithBaseFilter) { + set config(config: WithBaseFilter) { const configChanges = this.applyConfig(config); if (configChanges) { if (config) { @@ -418,6 +431,7 @@ export class ArrayDataSource config.groupBy, this.#columnMap, ); + console.log({ groupedData }); this.groupMap = groupMap; processedData = groupedData; @@ -502,6 +516,10 @@ export class ArrayDataSource this.setRange(range); } + getRowAtIndex(rowIndex: number) { + return this.processedData?.[rowIndex]; + } + protected delete(row: VuuRowDataItemType[]) { console.log(`delete row ${row.join(",")}`); } diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/group-utils.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/group-utils.ts index 931d94c3b..714dd4a8a 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/group-utils.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/group-utils.ts @@ -9,7 +9,7 @@ const { DEPTH, IS_EXPANDED, KEY } = metadataKeys; export const collapseGroup = ( key: string, - groupedRows: readonly DataSourceRow[] + groupedRows: readonly DataSourceRow[], ): DataSourceRow[] => { const rows: DataSourceRow[] = []; @@ -50,7 +50,7 @@ export const expandGroup = ( groupBy: VuuGroupBy, columnMap: ColumnMap, groupMap: GroupMap, - processedData: readonly DataSourceRow[] + processedData: readonly DataSourceRow[], ): DataSourceRow[] => { const groupIndices = groupBy.map((column) => columnMap[column]); return dataRowsFromGroups2( @@ -61,7 +61,7 @@ export const expandGroup = ( undefined, undefined, undefined, - processedData + processedData, ); }; @@ -73,14 +73,23 @@ const dataRowsFromGroups2 = ( root = "$root", depth = 1, rows: DataSourceRow[] = [], - processedData: readonly DataSourceRow[] + processedData: readonly DataSourceRow[], ) => { console.log(`dataRowsFromGroups2 1)`); const keys = Object.keys(groupMap).sort(); for (const key of keys) { const idx = rows.length; const groupKey = `${root}|${key}`; - const row: DataSourceRow = [idx, idx, false, false, depth, 0, groupKey, 0]; + const row: DataSourceRow = [ + idx, + idx, + false, + false, + depth, + childCount(groupMap[key]), + groupKey, + 0, + ]; // TODO whats this row[groupIndices[depth - 1]] = key; rows.push(row); @@ -93,7 +102,7 @@ const dataRowsFromGroups2 = ( groupMap[key] as KeyList, sourceRows, groupKey, - depth + 1 + depth + 1, ); } else { dataRowsFromGroups2( @@ -104,7 +113,7 @@ const dataRowsFromGroups2 = ( groupKey, depth + 1, rows, - processedData + processedData, ); } } @@ -120,8 +129,8 @@ const dataRowsFromGroups2 = ( rows[key] = rows[key].splice(0, 8).concat( processedData[index].slice( 8, // groupIndices[0] + 1, - processedData[index].length - ) + processedData[index].length, + ), ) as DataSourceRow; break; } @@ -138,7 +147,7 @@ const pushChildren = ( tree: KeyList, sourceRows: readonly DataSourceRow[], parentKey: string, - depth: number + depth: number, ) => { for (const rowIdx of tree) { const idx = rows.length; @@ -154,7 +163,7 @@ const pushChildren = ( export const groupRows = ( rows: readonly DataSourceRow[], groupBy: VuuGroupBy, - columnMap: ColumnMap + columnMap: ColumnMap, ): [DataSourceRow[], GroupMap] => { const groupIndices = groupBy.map((column) => columnMap[column]); const groupTree = groupLeafRows(rows, groupIndices); @@ -176,7 +185,7 @@ const dataRowsFromGroups = (groupTree: GroupMap, groupIndices: number[]) => { false, false, 1, - 0, + childCount(groupTree[key]), `$root|${key}`, 0, ]; @@ -187,6 +196,14 @@ const dataRowsFromGroups = (groupTree: GroupMap, groupIndices: number[]) => { return rows; }; +function childCount(list: GroupMap | KeyList) { + if (Array.isArray(list)) { + return list.length; + } else { + return Object.keys(list).length; + } +} + function groupLeafRows(leafRows: readonly DataSourceRow[], groupby: number[]) { const groups: GroupMap = {}; const levels = groupby.length; @@ -214,6 +231,5 @@ function groupLeafRows(leafRows: readonly DataSourceRow[], groupby: number[]) { } } } - console.log("!! groups", groups); return groups; } diff --git a/vuu-ui/packages/vuu-data-local/src/index.ts b/vuu-ui/packages/vuu-data-local/src/index.ts index 59509f61a..54627fe5b 100644 --- a/vuu-ui/packages/vuu-data-local/src/index.ts +++ b/vuu-ui/packages/vuu-data-local/src/index.ts @@ -1,2 +1,3 @@ export * from "./array-data-source/array-data-source"; -export * from "./json-data-source/json-data-source"; +export * from "./json-data-source/JsonDataSource"; +export * from "./tree-data-source/TreeDataSource"; diff --git a/vuu-ui/packages/vuu-data-local/src/json-data-source/json-data-source.ts b/vuu-ui/packages/vuu-data-local/src/json-data-source/JsonDataSource.ts similarity index 94% rename from vuu-ui/packages/vuu-data-local/src/json-data-source/json-data-source.ts rename to vuu-ui/packages/vuu-data-local/src/json-data-source/JsonDataSource.ts index 9b2459f6c..c44951578 100644 --- a/vuu-ui/packages/vuu-data-local/src/json-data-source/json-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/json-data-source/JsonDataSource.ts @@ -32,6 +32,8 @@ import { KeySet, metadataKeys, NO_CONFIG_CHANGES, + NULL_RANGE, + rangesAreSame, uuid, vanillaConfig, } from "@finos/vuu-utils"; @@ -88,7 +90,6 @@ export class JsonDataSource viewport, }: JsonDataSourceConstructorProps) { super(); - if (!data) { throw Error("JsonDataSource constructor called without data"); } @@ -151,9 +152,6 @@ export class JsonDataSource if (groupBy) { this.#groupBy = groupBy; } - if (range) { - this.#range = range; - } if (sort) { this.#sort = sort; } @@ -185,6 +183,12 @@ export class JsonDataSource type: "viewport-update", size: this.visibleRows.length, }); + + if (range && !rangesAreSame(this.#range, range)) { + this.range = range; + } else if (this.#range !== NULL_RANGE) { + this.sendRowsToClient(); + } } unsubscribe() { @@ -211,7 +215,6 @@ export class JsonDataSource return this; } set data(data: JsonData) { - console.log(`set JsonDataSource data`); [this.columnDescriptors, this.#data] = jsonToDataSourceRows(data); this.visibleRows = this.#data .filter((row) => row[DEPTH] === 0) @@ -248,7 +251,20 @@ export class JsonDataSource } } - openTreeNode(key: string) { + private getRowKey(keyOrIndex: string | number) { + if (typeof keyOrIndex === "string") { + return keyOrIndex; + } + const row = this.visibleRows[keyOrIndex]; + if (row === undefined) { + throw Error(`row not found at index ${keyOrIndex}`); + } + return row?.[KEY]; + } + + openTreeNode(keyOrIndex: string | number) { + const key = this.getRowKey(keyOrIndex); + this.expandedRows.add(key); this.visibleRows = getVisibleRows(this.#data, this.expandedRows); const { from, to } = this.#range; @@ -264,7 +280,9 @@ export class JsonDataSource }); } - closeTreeNode(key: string, cascade = false) { + closeTreeNode(keyOrIndex: string | number, cascade = false) { + const key = this.getRowKey(keyOrIndex); + this.expandedRows.delete(key); if (cascade) { for (const rowKey of this.expandedRows.keys()) { diff --git a/vuu-ui/packages/vuu-data-local/src/tree-data-source/IconProvider.ts b/vuu-ui/packages/vuu-data-local/src/tree-data-source/IconProvider.ts new file mode 100644 index 000000000..9dc2fccd1 --- /dev/null +++ b/vuu-ui/packages/vuu-data-local/src/tree-data-source/IconProvider.ts @@ -0,0 +1,15 @@ +import { DataSourceRow } from "@finos/vuu-data-types"; +import { metadataKeys } from "@finos/vuu-utils"; + +const { KEY } = metadataKeys; + +export class IconProvider { + #iconMap: Record = {}; + getIcon = (row: DataSourceRow) => { + const key = row[KEY]; + return this.#iconMap[key]; + }; + setIcon(key: string, icon: string) { + this.#iconMap[key] = icon; + } +} diff --git a/vuu-ui/packages/vuu-data-local/src/tree-data-source/TreeDataSource.ts b/vuu-ui/packages/vuu-data-local/src/tree-data-source/TreeDataSource.ts new file mode 100644 index 000000000..a9183708f --- /dev/null +++ b/vuu-ui/packages/vuu-data-local/src/tree-data-source/TreeDataSource.ts @@ -0,0 +1,419 @@ +import type { ColumnDescriptor } from "@finos/vuu-table-types"; +import type { + LinkDescriptorWithLabel, + VuuAggregation, + VuuRange, + VuuRowDataItemType, + VuuRpcResponse, + VuuRpcRequest, +} from "@finos/vuu-protocol-types"; +import type { + DataSourceRow, + DataSourceConstructorProps, + DataSourceStatus, + SubscribeCallback, + SubscribeProps, + Selection, + MenuRpcResponse, + VuuUIMessageInRPCEditReject, + VuuUIMessageInRPCEditResponse, +} from "@finos/vuu-data-types"; +import { + BaseDataSource, + isSelected, + KeySet, + metadataKeys, + NULL_RANGE, + rangesAreSame, + TreeSourceNode, + treeToDataSourceRows, + uuid, +} from "@finos/vuu-utils"; +import { IconProvider } from "./IconProvider"; + +const NULL_SCHEMA = { columns: [], key: "", table: { module: "", table: "" } }; + +type VisibleRowIndex = Record; + +export interface TreeDataSourceConstructorProps + extends Omit { + data: TreeSourceNode[]; +} + +const { COUNT, DEPTH, IDX, IS_EXPANDED, IS_LEAF, KEY, SELECTED } = metadataKeys; + +const toClientRow = (row: DataSourceRow, keys: KeySet) => { + const [rowIndex] = row; + const clientRow = row.slice() as DataSourceRow; + clientRow[1] = keys.keyFor(rowIndex); + return clientRow; +}; + +export class TreeDataSource extends BaseDataSource { + public columnDescriptors: ColumnDescriptor[]; + private clientCallback: SubscribeCallback | undefined; + private expandedRows = new Set(); + private visibleRows: DataSourceRow[] = []; + private visibleRowIndex: VisibleRowIndex = {}; + private selectedRows: Selection = []; + + #aggregations: VuuAggregation[] = []; + #data: DataSourceRow[]; + #iconProvider: IconProvider; + #selectedRowsCount = 0; + #size = 0; + #status: DataSourceStatus = "initialising"; + + public rowCount: number | undefined; + + private keys = new KeySet(this._range); + + constructor({ data, ...props }: TreeDataSourceConstructorProps) { + super(props); + + if (!data) { + throw Error("TreeDataSource constructor called without data"); + } + this.#iconProvider = new IconProvider(); + + [this.columnDescriptors, this.#data] = treeToDataSourceRows( + data, + this.#iconProvider, + ); + + if (this.columnDescriptors) { + const columns = this.columnDescriptors.map((c) => c.name); + this._config = { + ...this._config, + columns, + groupBy: columns, + }; + } + } + + async subscribe( + { + viewport = this.viewport ?? uuid(), + columns, + aggregations, + range, + }: SubscribeProps, + callback: SubscribeCallback, + ) { + this.clientCallback = callback; + + if (aggregations) { + this.#aggregations = aggregations; + } + if (columns) { + this._config = { + ...this._config, + columns, + }; + } + + if (this.#status !== "initialising") { + //TODO check if subscription details are still the same + return; + } + + this.viewport = viewport; + + this.#status = "subscribed"; + + this.clientCallback?.({ + aggregations: this.#aggregations, + type: "subscribed", + clientViewportId: this.viewport, + columns: this.columns, + filterSpec: this.filter, + groupBy: this._config.groupBy, + range: this.range, + sort: this.sort, + tableSchema: NULL_SCHEMA, + }); + + this.clientCallback({ + clientViewportId: this.viewport, + mode: "size-only", + type: "viewport-update", + size: this.visibleRows.length, + }); + + if (range && !rangesAreSame(this._range, range)) { + this.range = range; + } else if (this._range !== NULL_RANGE) { + this.sendRowsToClient(); + } + } + + unsubscribe() { + console.log("noop"); + } + + suspend() { + console.log("noop"); + return this; + } + + resume() { + console.log("noop"); + return this; + } + + disable() { + console.log("noop"); + return this; + } + + enable() { + console.log("noop"); + return this; + } + set data(data: TreeSourceNode[]) { + [this.columnDescriptors, this.#data] = treeToDataSourceRows(data); + // console.table(this.#data.slice(0, 20)); + [this.visibleRows, this.visibleRowIndex] = getVisibleRows( + this.#data, + this.expandedRows, + ); + + // console.table(this.#data); + console.table(this.visibleRows); + + console.log({ visibleRows: this.visibleRows }); + + requestAnimationFrame(() => { + this.sendRowsToClient(); + }); + } + + // Incoming Selection references visibleRow indices + select(selected: Selection) { + // todo get a diff + const updatedRows: DataSourceRow[] = []; + for (const row of this.visibleRows) { + const { [IDX]: rowIndex, [SELECTED]: sel } = row; + const wasSelected = sel === 1; + const nowSelected = isSelected(selected, rowIndex); + if (nowSelected !== wasSelected) { + const selectedRow = row.slice() as DataSourceRow; + const selectedValue = nowSelected ? 1 : 0; + selectedRow[SELECTED] = selectedValue; + const dataRowIdx = this.visibleRowIndex[rowIndex]; + this.visibleRows[rowIndex] = selectedRow; + this.#data[dataRowIdx][SELECTED] = selectedValue; + updatedRows.push(selectedRow); + } + } + + if (updatedRows.length > 0) { + this.clientCallback?.({ + clientViewportId: this.viewport, + mode: "update", + type: "viewport-update", + rows: updatedRows, + }); + } + } + + private getRowKey(keyOrIndex: string | number) { + if (typeof keyOrIndex === "string") { + return keyOrIndex; + } + const row = this.getRowAtIndex(keyOrIndex); + if (row === undefined) { + throw Error(`row not found at index ${keyOrIndex}`); + } + return row[KEY]; + } + + openTreeNode(keyOrIndex: string | number) { + const key = this.getRowKey(keyOrIndex); + this.expandedRows.add(key); + [this.visibleRows, this.visibleRowIndex] = getVisibleRows( + this.#data, + this.expandedRows, + ); + + const { from, to } = this._range; + this.clientCallback?.({ + clientViewportId: this.viewport, + + mode: "batch", + rows: this.visibleRows + .slice(from, to) + .map((row) => toClientRow(row, this.keys)), + size: this.visibleRows.length, + type: "viewport-update", + }); + } + + closeTreeNode(keyOrIndex: string | number, cascade = false) { + const key = this.getRowKey(keyOrIndex); + this.expandedRows.delete(key); + if (cascade) { + for (const rowKey of this.expandedRows.keys()) { + if (rowKey.startsWith(key)) { + this.expandedRows.delete(rowKey); + } + } + } + [this.visibleRows, this.visibleRowIndex] = getVisibleRows( + this.#data, + this.expandedRows, + ); + this.sendRowsToClient(); + } + + get status() { + return this.#status; + } + + get selectedRowsCount() { + return this.#selectedRowsCount; + } + + get size() { + return this.#size; + } + + rangeRequest(range: VuuRange) { + this.keys.reset(range); + requestAnimationFrame(() => { + this.sendRowsToClient(); + }); + } + + private sendRowsToClient() { + const { from, to } = this._range; + this.clientCallback?.({ + clientViewportId: this.viewport, + mode: "batch", + rows: this.visibleRows + .slice(from, to) + .map((row) => toClientRow(row, this.keys)), + size: this.visibleRows.length, + type: "viewport-update", + }); + } + + createLink({ + parentVpId, + link: { fromColumn, toColumn }, + }: LinkDescriptorWithLabel) { + console.log("create link", { + parentVpId, + fromColumn, + toColumn, + }); + } + + removeLink() { + console.log("remove link"); + } + + async remoteProcedureCall() { + return Promise.reject(); + } + + async menuRpcCall( + rpcRequest: Omit, + ): Promise< + | MenuRpcResponse + | VuuUIMessageInRPCEditReject + | VuuUIMessageInRPCEditResponse + | undefined + > { + console.log("rmenuRpcCall", { + rpcRequest, + }); + return undefined; + } + + applyEdit( + rowKey: string, + columnName: string, + value: VuuRowDataItemType, + ): Promise { + console.log(`ArrayDataSource applyEdit ${rowKey} ${columnName} ${value}`); + return Promise.resolve(true); + } + + getChildRows(rowKey: string) { + const parentRow = this.#data.find((row) => row[KEY] === rowKey); + if (parentRow) { + const { [IDX]: parentIdx, [DEPTH]: parentDepth } = parentRow; + let rowIdx = parentIdx + 1; + const childRows = []; + do { + const { [DEPTH]: depth } = this.#data[rowIdx]; + if (depth === parentDepth + 1) { + childRows.push(this.#data[rowIdx]); + } else if (depth <= parentDepth) { + break; + } + rowIdx += 1; + } while (rowIdx < this.#data.length); + return childRows; + } else { + console.warn( + `JsonDataSource getChildRows row not found for key ${rowKey}`, + ); + } + + return []; + } + + getRowsAtDepth(depth: number, visibleOnly = true) { + const rows = visibleOnly ? this.visibleRows : this.#data; + return rows.filter((row) => row[DEPTH] === depth); + } + + getRowAtIndex(rowIdx: number) { + return this.visibleRows[rowIdx]; + } +} + +function getVisibleRows( + rows: DataSourceRow[], + expandedKeys: Set, +): [visibleRows: DataSourceRow[], index: VisibleRowIndex] { + const visibleRows: DataSourceRow[] = []; + const visibleRowIndex: VisibleRowIndex = {}; + + for (let i = 0, index = 0; i < rows.length; i++) { + const row = rows[i]; + const { + [COUNT]: count, + [DEPTH]: depth, + [KEY]: key, + [IS_LEAF]: isLeaf, + } = row; + const isExpanded = expandedKeys.has(key); + visibleRows.push(cloneRow(row, index, isExpanded)); + visibleRowIndex[index] = i; + index += 1; + const skipNonVisibleRows = !isLeaf && !isExpanded && count > 0; + if (skipNonVisibleRows) { + do { + i += 1; + } while (i < rows.length - 1 && rows[i + 1][DEPTH] > depth); + } + } + return [visibleRows, visibleRowIndex]; +} + +const cloneRow = ( + row: DataSourceRow, + index: number, + isExpanded: boolean, +): DataSourceRow => { + const dolly = row.slice() as DataSourceRow; + dolly[0] = index; + dolly[1] = index; + if (isExpanded) { + dolly[IS_EXPANDED] = true; + } + return dolly; +}; diff --git a/vuu-ui/packages/vuu-data-react/src/data-editing/EditForm.tsx b/vuu-ui/packages/vuu-data-react/src/data-editing/EditForm.tsx index a2d0d3e97..1e7840168 100644 --- a/vuu-ui/packages/vuu-data-react/src/data-editing/EditForm.tsx +++ b/vuu-ui/packages/vuu-data-react/src/data-editing/EditForm.tsx @@ -1,12 +1,11 @@ import { getDataItemEditControl } from "@finos/vuu-data-react"; -import { DataSource, DataValueDescriptor } from "@finos/vuu-data-types"; import { Button, FormField, FormFieldLabel } from "@salt-ds/core"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import cx from "clsx"; import { HTMLAttributes } from "react"; import { registerRules } from "./edit-validation-rules"; -import { useEditForm } from "./useEditForm"; +import { EditFormHookProps, useEditForm } from "./useEditForm"; import editFormCss from "./EditForm.css"; @@ -14,11 +13,9 @@ const classBase = "EditForm"; registerRules(); -export interface EditFormProps extends HTMLAttributes { - dataSource?: DataSource; - formFieldDescriptors: DataValueDescriptor[]; - onSubmit?: () => void; -} +export interface EditFormProps + extends EditFormHookProps, + Omit, "onSubmit"> {} export const EditForm = ({ className, diff --git a/vuu-ui/packages/vuu-data-react/src/data-editing/get-data-item-edit-control.tsx b/vuu-ui/packages/vuu-data-react/src/data-editing/get-data-item-edit-control.tsx index 0fa23e274..1d49a189d 100644 --- a/vuu-ui/packages/vuu-data-react/src/data-editing/get-data-item-edit-control.tsx +++ b/vuu-ui/packages/vuu-data-react/src/data-editing/get-data-item-edit-control.tsx @@ -39,7 +39,6 @@ export const getDataItemEditControl = ({ evt, value, ) => { - console.log(`value`); onCommit(evt, value.toString()); }; diff --git a/vuu-ui/packages/vuu-data-react/src/data-editing/useEditForm.tsx b/vuu-ui/packages/vuu-data-react/src/data-editing/useEditForm.tsx index d51c0c4d6..2cca4f718 100644 --- a/vuu-ui/packages/vuu-data-react/src/data-editing/useEditForm.tsx +++ b/vuu-ui/packages/vuu-data-react/src/data-editing/useEditForm.tsx @@ -1,6 +1,6 @@ -import { DataSource, DataValueDescriptor } from "@finos/vuu-data-types"; +import type { DataSource, DataValueDescriptor } from "@finos/vuu-data-types"; import { useDialogContext } from "@finos/vuu-popups"; -import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import type { VuuRowDataItemType } from "@finos/vuu-protocol-types"; import { Entity, buildColumnMap, @@ -18,7 +18,6 @@ import { useRef, useState, } from "react"; -import { EditFormProps } from "./EditForm"; import { UnsavedChangesReport } from "./UnsavedChangesReport"; import { buildValidationChecker, @@ -30,10 +29,11 @@ import { buildFormEditState, } from "./form-edit-state"; -export type EditFormHookProps = Pick< - EditFormProps, - "dataSource" | "formFieldDescriptors" | "onSubmit" ->; +export interface EditFormHookProps { + dataSource?: DataSource; + formFieldDescriptors: DataValueDescriptor[]; + onSubmit?: () => void; +} type ValidationState = { ok: boolean; diff --git a/vuu-ui/packages/vuu-data-react/src/datasource-provider/index.ts b/vuu-ui/packages/vuu-data-react/src/datasource-provider/index.ts index fab1d6b34..14c68edaf 100644 --- a/vuu-ui/packages/vuu-data-react/src/datasource-provider/index.ts +++ b/vuu-ui/packages/vuu-data-react/src/datasource-provider/index.ts @@ -1 +1,2 @@ +export * from "./RestDataSourceProvider"; export * from "./VuuDataSourceProvider"; diff --git a/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts b/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts similarity index 95% rename from vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts rename to vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts index c52815a0c..a71bbb8b7 100644 --- a/vuu-ui/packages/vuu-data-remote/src/vuu-data-source.ts +++ b/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts @@ -1,7 +1,6 @@ import { DataSource, DataSourceCallbackMessage, - DataSourceConfig, DataSourceConstructorProps, DataSourceStatus, DataSourceVisualLinkCreatedMessage, @@ -11,6 +10,8 @@ import { SubscribeCallback, SubscribeProps, TableSchema, + WithBaseFilter, + WithFullConfig, } from "@finos/vuu-data-types"; import { LinkDescriptorWithLabel, @@ -57,15 +58,13 @@ const { info } = logger("VuuDataSource"); export class VuuDataSource extends BaseDataSource implements DataSource { private bufferSize: number; private server: ServerAPI | null = null; - private configChangePending: DataSourceConfig | undefined; + private configChangePending: WithBaseFilter | undefined; rangeRequest: RangeRequest; - #groupBy: VuuGroupBy = []; #pendingVisualLink?: LinkDescriptorWithLabel; #links: LinkDescriptorWithLabel[] | undefined; #menu: VuuMenu | undefined; #optimize: OptimizeStrategy = "throttle"; - #range: VuuRange = { from: 0, to: 0 }; #selectedRowsCount = 0; #status: DataSourceStatus = "initialising"; #tableSchema: TableSchema | undefined; @@ -282,22 +281,28 @@ export class VuuDataSource extends BaseDataSource implements DataSource { } } - openTreeNode(key: string) { + openTreeNode(keyOrIndex: string | number) { if (this.viewport) { + const [key, index] = + typeof keyOrIndex === "string" ? [keyOrIndex] : [undefined, keyOrIndex]; this.server?.send({ - viewport: this.viewport, - type: "openTreeNode", + index, key, + type: "openTreeNode", + viewport: this.viewport, }); } } - closeTreeNode(key: string) { + closeTreeNode(keyOrIndex: string | number) { if (this.viewport) { + const [key, index] = + typeof keyOrIndex === "string" ? [keyOrIndex] : [undefined, keyOrIndex]; this.server?.send({ - viewport: this.viewport, - type: "closeTreeNode", + index, key, + type: "closeTreeNode", + viewport: this.viewport, }); } } @@ -387,7 +392,7 @@ export class VuuDataSource extends BaseDataSource implements DataSource { return super.config; } - set config(config: DataSourceConfig) { + set config(config: WithBaseFilter) { const previousConfig = this._config; super.config = config; @@ -422,16 +427,16 @@ export class VuuDataSource extends BaseDataSource implements DataSource { rows: [], }); } - this.setConfigPending({ groupBy }); + this.setConfigPending(this.config); } } get title() { - return this._title ?? `${this.table.module} ${this.table.table}`; + return super.title || `${this.table.module} ${this.table.table}`; } set title(title: string) { - this._title = title; + super.title = title; if (this.viewport && title) { // This message doesn't actually trigger a message to Vuu server // it will be used to recompute visual link labels @@ -441,7 +446,6 @@ export class VuuDataSource extends BaseDataSource implements DataSource { viewport: this.viewport, }); } - this.emit("title-changed", this.viewport ?? "'", title); } get visualLink() { @@ -489,13 +493,13 @@ export class VuuDataSource extends BaseDataSource implements DataSource { this.emit("config", this._config); } - private setConfigPending(config?: DataSourceConfig) { + private setConfigPending(config?: WithBaseFilter) { const pendingConfig = this.configChangePending; this.configChangePending = config; if (config !== undefined) { this.emit("config", config, false); - } else { + } else if (pendingConfig) { this.emit("config", pendingConfig, true); } } diff --git a/vuu-ui/packages/vuu-data-remote/src/index.ts b/vuu-ui/packages/vuu-data-remote/src/index.ts index d447c1a91..cd168f8a1 100644 --- a/vuu-ui/packages/vuu-data-remote/src/index.ts +++ b/vuu-ui/packages/vuu-data-remote/src/index.ts @@ -5,5 +5,5 @@ export * from "./constants"; export * from "./data-source"; export * from "./message-utils"; export * from "./rest-data/RestDataSource"; -export * from "./vuu-data-source"; +export * from "./VuuDataSource"; export type { WebSocketConnectionState } from "./WebSocketConnection"; diff --git a/vuu-ui/packages/vuu-data-remote/src/rest-data/RestDataSource.ts b/vuu-ui/packages/vuu-data-remote/src/rest-data/RestDataSource.ts index 8d22d9a93..c52c394db 100644 --- a/vuu-ui/packages/vuu-data-remote/src/rest-data/RestDataSource.ts +++ b/vuu-ui/packages/vuu-data-remote/src/rest-data/RestDataSource.ts @@ -1,11 +1,12 @@ import { DataSource, - DataSourceConfig, DataSourceConstructorProps, DataSourceEditHandler, DataSourceStatus, SubscribeCallback, SubscribeProps, + WithBaseFilter, + WithFullConfig, } from "@finos/vuu-data-types"; import { VuuTable, VuuGroupBy, VuuRange } from "@finos/vuu-protocol-types"; import { @@ -123,7 +124,7 @@ export class RestDataSource extends BaseDataSource implements DataSource { return super.config; } - set config(config: DataSourceConfig) { + set config(config: WithBaseFilter) { const previousConfig = this._config; super.config = config; diff --git a/vuu-ui/packages/vuu-data-remote/src/server-proxy/array-backed-moving-window.ts b/vuu-ui/packages/vuu-data-remote/src/server-proxy/array-backed-moving-window.ts index 40cebe351..471d5801c 100644 --- a/vuu-ui/packages/vuu-data-remote/src/server-proxy/array-backed-moving-window.ts +++ b/vuu-ui/packages/vuu-data-remote/src/server-proxy/array-backed-moving-window.ts @@ -42,7 +42,7 @@ export class ArrayBackedMovingWindow { constructor( { from: clientFrom, to: clientTo }: VuuRange, { from, to }: VuuRange, - bufferSize: number + bufferSize: number, ) { this.bufferSize = bufferSize; this.clientRange = new WindowRange(clientFrom, clientTo); @@ -115,7 +115,7 @@ export class ArrayBackedMovingWindow { return isWithinClientRange; } - getAtIndex(index: number): any { + getAtIndex(index: number): VuuRow | undefined { return this.#range.isWithin(index) && this.internalData[index - this.#range.from] != null ? this.internalData[index - this.#range.from] @@ -173,14 +173,14 @@ export class ArrayBackedMovingWindow { if (from !== this.#range.from || to !== this.#range.to) { log.debug?.(`setRange ${from} - ${to}`); const [overlapFrom, overlapTo] = this.#range.overlap(from, to); - const newData = new Array(to - from); + const newData: VuuRow[] = new Array(to - from); this.rowsWithinRange = 0; for (let i = overlapFrom; i < overlapTo; i++) { - const data = this.getAtIndex(i); - if (data) { + const row = this.getAtIndex(i); + if (row) { const index = i - from; - newData[index] = data; + newData[index] = row; if (this.isWithinClientRange(i)) { this.rowsWithinRange += 1; } @@ -214,7 +214,7 @@ export class ArrayBackedMovingWindow { } }; - getData(): any[] { + getData(): VuuRow[] { const { from, to } = this.#range; const { from: clientFrom, to: clientTo } = this.clientRange; const startOffset = Math.max(0, clientFrom - from); @@ -223,7 +223,7 @@ export class ArrayBackedMovingWindow { to - from, to, clientTo - from, - this.rowCount ?? to + this.rowCount ?? to, ); return this.internalData.slice(startOffset, endOffset); } diff --git a/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts index bd467e831..7248821a8 100644 --- a/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data-remote/src/server-proxy/viewport.ts @@ -552,10 +552,15 @@ export class Viewport { if (this.useBatchMode) { this.batchMode = true; } + const treeKey = + message.index === undefined + ? message.key + : this.getKeyForRowAtIndex(message.index); + console.log(`treeKeu ${treeKey}`); return { type: Message.OPEN_TREE_NODE, vpId: this.serverViewportId, - treeKey: message.key, + treeKey, } as ClientToServerOpenTreeNode; } @@ -563,10 +568,14 @@ export class Viewport { if (this.useBatchMode) { this.batchMode = true; } + const treeKey = + message.index === undefined + ? message.key + : this.getKeyForRowAtIndex(message.index); return { type: Message.CLOSE_TREE_NODE, vpId: this.serverViewportId, - treeKey: message.key, + treeKey, } as ClientToServerCloseTreeNode; } @@ -772,6 +781,11 @@ export class Viewport { } } + private getKeyForRowAtIndex(rowIndex: number) { + const row = this.dataWindow.getAtIndex(rowIndex); + return row?.rowKey; + } + // This is called only after new data has been received from server - data // returned direcly from buffer does not use this. getClientRows(): Readonly< diff --git a/vuu-ui/packages/vuu-data-remote/test/vuu-data-source.test.ts b/vuu-ui/packages/vuu-data-remote/test/vuu-data-source.test.ts index c48efa9a7..4d2b81e6e 100644 --- a/vuu-ui/packages/vuu-data-remote/test/vuu-data-source.test.ts +++ b/vuu-ui/packages/vuu-data-remote/test/vuu-data-source.test.ts @@ -9,7 +9,7 @@ import { WithBaseFilter, } from "@finos/vuu-data-types"; import { LinkDescriptorWithLabel, VuuSortCol } from "@finos/vuu-protocol-types"; -import { VuuDataSource } from "../src/vuu-data-source"; +import { VuuDataSource } from "../src/VuuDataSource"; import ConnectionManager from "../src/ConnectionManager"; vi.mock("../src/ConnectionManager", () => ({ diff --git a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts index 38fd0b570..b190233a1 100644 --- a/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts +++ b/vuu-ui/packages/vuu-data-test/src/TickingArrayDataSource.ts @@ -228,7 +228,7 @@ export class TickingArrayDataSource extends ArrayDataSource { } get visualLink() { - return this.config.visualLink; + return this._config.visualLink; } set visualLink(visualLink: LinkDescriptorWithLabel | undefined) { diff --git a/vuu-ui/packages/vuu-data-types/index.d.ts b/vuu-ui/packages/vuu-data-types/index.d.ts index ee130278a..a222d9d46 100644 --- a/vuu-ui/packages/vuu-data-types/index.d.ts +++ b/vuu-ui/packages/vuu-data-types/index.d.ts @@ -468,7 +468,7 @@ export declare type RowSelectionEventHandler = ( export declare type DataSourceEvents = { config: ( - config: DataSourceConfig | undefined, + config: WithBaseFilter, confirmed?: boolean, configChanges?: DataSourceConfigChanges, ) => void; @@ -576,9 +576,9 @@ export interface DataSource */ preserveExistingConfigAttributes?: boolean, ) => DataSourceConfigChanges | undefined; - closeTreeNode: (key: string, cascade?: boolean) => void; + closeTreeNode: (keyOrIndex: string | number, cascade?: boolean) => void; columns: string[]; - config: WithBaseFilter; + config: WithBaseFilter; status: DataSourceStatus; /** * @@ -627,6 +627,8 @@ export interface DataSource * @returns */ getChildRows?: (rowKey: string) => DataSourceRow[]; + + getRowAtIndex?: (rowIndex: number) => DataSourceRow | undefined; /** * Only implemented on JSON DataSource * @param depth @@ -634,7 +636,7 @@ export interface DataSource * @returns */ getRowsAtDepth?: (depth: number, visibleOnly?: boolean) => DataSourceRow[]; - groupBy: VuuGroupBy; + groupBy?: VuuGroupBy; insertRow?: DataSourceInsertHandler; links?: LinkDescriptorWithLabel[]; menu?: VuuMenu; @@ -646,7 +648,7 @@ export interface DataSource rpcCall?: ( rpcRequest: Omit, ) => Promise; - openTreeNode: (key: string) => void; + openTreeNode: (keyOrIndex: string | number) => void; range: VuuRange; remoteProcedureCall: ( message: VuuRpcRequest, @@ -849,7 +851,8 @@ export interface VuuUIMessageOutViewRange extends ViewportMessageOut { }; } export interface VuuUIMessageOutCloseTreeNode extends ViewportMessageOut { - key: string; + index?: number; + key?: string; type: "closeTreeNode"; } export interface VuuUIMessageOutRemoveLink extends ViewportMessageOut { @@ -867,7 +870,8 @@ export interface VuuUIMessageOutEnable extends ViewportMessageOut { type: "enable"; } export interface VuuUIMessageOutOpenTreeNode extends ViewportMessageOut { - key: string; + index?: number; + key?: string; type: "openTreeNode"; } export interface VuuUIMessageOutResume extends ViewportMessageOut { diff --git a/vuu-ui/packages/vuu-datatable/src/index.ts b/vuu-ui/packages/vuu-datatable/src/index.ts index 6772e4a5c..e1a0baf2a 100644 --- a/vuu-ui/packages/vuu-datatable/src/index.ts +++ b/vuu-ui/packages/vuu-datatable/src/index.ts @@ -1,2 +1,3 @@ export * from "./filter-table"; export * from "./json-table"; +export * from "./tree-table"; diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.css b/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.css new file mode 100644 index 000000000..d5c95343d --- /dev/null +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.css @@ -0,0 +1,16 @@ +.vuuJsonCell { + --group-cell-spacer-width: 20px; + align-items: center; + border-right-style: solid; + border-right-width: 1px; + cursor: pointer; + display: inline-flex; + height: var(--row-height); + padding-left: 20px; + line-height: 16px; + position: relative; + + .vuuToggleIconButton { + position: absolute; + } +} diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.tsx b/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.tsx new file mode 100644 index 000000000..2d084773a --- /dev/null +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonCell.tsx @@ -0,0 +1,49 @@ +import { TableCellProps } from "@finos/vuu-table-types"; +import { metadataKeys } from "@finos/vuu-utils"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import { ToggleIconButton } from "@finos/vuu-ui-controls"; + +// Note the className is actually applied to containing cell +import arrayCellCss from "./JsonCell.css"; + +const { IS_EXPANDED, IS_LEAF } = metadataKeys; + +export const JsonCell = ({ column, columnMap, row }: TableCellProps) => { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "vuu-array-cell", + css: arrayCellCss, + window: targetWindow, + }); + + const { name } = column; + const dataIdx = columnMap[name]; + let { [IS_EXPANDED]: isExpanded, [IS_LEAF]: isLeaf, [dataIdx]: value } = row; + + const getDisplayValue = () => { + if (isLeaf) { + return value; + } else if (typeof value === "string" && value.endsWith("{")) { + value = value.slice(0, -1); + if (!isNaN(parseInt(value))) { + return `${value}: {...}`; + } else { + return `value {...}`; + } + } else if (typeof value === "string" && value.endsWith("[")) { + value = value.slice(0, -1); + return `${value} [...]`; + } + }; + + const displayValue = getDisplayValue(); + const isEmpty = displayValue === "" || displayValue === undefined; + + return ( + <> + {isLeaf || isEmpty ? null : } + {displayValue} + + ); +}; diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx index 49690c556..af29e3fab 100644 --- a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx @@ -1,9 +1,12 @@ import { TableProps } from "@finos/vuu-table"; -import { JsonData } from "@finos/vuu-utils"; +import { JsonData, registerComponent } from "@finos/vuu-utils"; import { Table } from "@finos/vuu-table"; import { JsonDataSource } from "@finos/vuu-data-local"; import { useEffect, useMemo, useRef } from "react"; import { TableConfig } from "@finos/vuu-table-types"; +import { JsonCell } from "./JsonCell"; + +registerComponent("json", JsonCell, "cell-renderer"); export interface JsonTableProps extends Omit { @@ -11,12 +14,12 @@ export interface JsonTableProps TableConfig, "columnSeparators" | "rowSeparators" | "zebraStripes" >; - source: JsonData | undefined; + source: JsonData; } export const JsonTable = ({ config, - source: sourceProp = { "": "" }, + source: sourceProp, ...tableProps }: JsonTableProps) => { const sourceRef = useRef(sourceProp); @@ -31,6 +34,8 @@ export const JsonTable = ({ return { ...config, columns: dataSourceRef.current?.columnDescriptors ?? [], + columnSeparators: true, + rowSeparators: true, }; }, [config]); @@ -49,6 +54,8 @@ export const JsonTable = ({ {...tableProps} config={tableConfig} dataSource={dataSourceRef.current} + showColumnHeaderMenus={false} + selectionModel="none" /> ); }; diff --git a/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.css b/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.css new file mode 100644 index 000000000..e69de29bb diff --git a/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx b/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx new file mode 100644 index 000000000..e06525150 --- /dev/null +++ b/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx @@ -0,0 +1,61 @@ +import { TableProps } from "@finos/vuu-table"; +import { Table } from "@finos/vuu-table"; +import { TreeDataSource } from "@finos/vuu-data-local"; +import { useEffect, useMemo, useRef } from "react"; +import { TableConfig } from "@finos/vuu-table-types"; +import { TreeSourceNode } from "@finos/vuu-utils"; + +export interface TreeTableProps + extends Omit { + config?: Pick< + TableConfig, + "columnSeparators" | "rowSeparators" | "zebraStripes" + >; + source: TreeSourceNode[]; +} + +export const TreeTable = ({ + config, + source: sourceProp, + ...tableProps +}: TreeTableProps) => { + const sourceRef = useRef(sourceProp); + const dataSourceRef = useRef(); + useMemo(() => { + dataSourceRef.current = new TreeDataSource({ + data: sourceRef.current, + }); + }, []); + + const tableConfig = useMemo(() => { + return { + ...config, + columns: dataSourceRef.current?.columnDescriptors ?? [], + columnSeparators: false, + rowSeparators: false, + }; + }, [config]); + + useEffect(() => { + if (dataSourceRef.current) { + dataSourceRef.current.data = sourceProp; + } + }, [sourceProp]); + + if (dataSourceRef.current === undefined) { + return null; + } + + return ( + + ); +}; diff --git a/vuu-ui/packages/vuu-datatable/src/tree-table/index.ts b/vuu-ui/packages/vuu-datatable/src/tree-table/index.ts new file mode 100644 index 000000000..74e7e0c89 --- /dev/null +++ b/vuu-ui/packages/vuu-datatable/src/tree-table/index.ts @@ -0,0 +1 @@ +export * from "./TreeTable"; diff --git a/vuu-ui/packages/vuu-icons/index.css b/vuu-ui/packages/vuu-icons/index.css index a606f502e..9501d5b2f 100644 --- a/vuu-ui/packages/vuu-icons/index.css +++ b/vuu-ui/packages/vuu-icons/index.css @@ -29,7 +29,6 @@ --svg-sort-up: url('data:image/svg+xml;utf8,'); --svg-tree-node-collapse: url('data:image/svg+xml;utf8,'); --svg-tree-node-expand: url('data:image/svg+xml;utf8,'); - --svg-triangle-right: url('data:image/svg+xml;utf8,'); --svg-plus-box: url('data:image/svg+xml;utf8,'); --svg-minus-box: url('data:image/svg+xml;utf8,'); --vuu-icon-size: 12px; @@ -61,7 +60,7 @@ --vuu-svg-search: url('data:image/svg+xml;utf8,'); --vuu-svg-settings: url('data:image/svg+xml;utf8,'); --vuu-svg-tick: url('data:image/svg+xml;utf8,'); - --vuu-svg-triangle-right: url('data:image/svg+xml;utf8,'); + --vuu-svg-triangle-right: url('data:image/svg+xml;utf8,'); --vuu-svg-info-circle: url('data:image/svg+xml;utf8, '); --vuu-svg-warn-triangle: url('data:image/svg+xml;utf8,'); } @@ -162,6 +161,14 @@ span[data-icon] { --vuu-icon-svg: var(--svg-close-circle); } +[data-icon="column-2A"] { + --vuu-icon-svg: var(--svg-column-2A); +} + +[data-icon="column-2B"] { + --vuu-icon-svg: var(--svg-column-2B); +} + [data-icon="cross"] { --vuu-icon-svg: var(--vuu-svg-cross); } @@ -174,6 +181,10 @@ span[data-icon] { --vuu-icon-svg: var(--vuu-svg-date); } +[data-icon="disconnected-status"] { + --svg-icon: var(--svg-disconnected-status); +} + [data-icon="draggable"] { --vuu-icon-svg: var(--vuu-svg-draggable); } @@ -216,14 +227,6 @@ span[data-icon] { --vuu-icon-svg: var(--svg-rings); } -[data-icon="column-2A"] { - --vuu-icon-svg: var(--svg-column-2A); -} - -[data-icon="column-2B"] { - --vuu-icon-svg: var(--svg-column-2B); -} - :is([data-icon="folder"], [data-icon="folder-closed"]) { --vuu-icon-svg: var(--svg-folder-closed); } @@ -244,6 +247,10 @@ span[data-icon] { --vuu-icon-svg: var(--vuu-svg-more-horiz); } +[data-icon="plus"] { + --vuu-icon-svg: var(--vuu-svg-plus); +} + [data-icon="search"] { --vuu-icon-svg: var(--vuu-svg-search); } @@ -264,10 +271,6 @@ span[data-icon] { --vuu-icon-svg: var(--svg-sorted-dsc); } -[data-icon="plus"] { - --vuu-icon-svg: var(--vuu-svg-plus); -} - [data-icon="tick"] { --vuu-icon-svg: var(--vuu-svg-tick); } @@ -284,19 +287,18 @@ span[data-icon] { --svg-icon: var(--svg-connecting-status); } -[data-icon="disconnected-status"] { - --svg-icon: var(--svg-disconnected-status); -} - [data-icon="triangle-right"] { --vuu-icon-svg: var(--vuu-svg-triangle-right); } +[data-icon="triangle-right"]:after { + transform: rotate(-45deg); +} [data-icon="triangle-down"] { --vuu-icon-svg: var(--vuu-svg-triangle-right); } [data-icon="triangle-down"]:after { - transform: rotate(90deg); + transform: rotate(45deg); } [data-icon="warn-triangle"] { diff --git a/vuu-ui/packages/vuu-table-types/index.d.ts b/vuu-ui/packages/vuu-table-types/index.d.ts index 715414a6f..53080c9e9 100644 --- a/vuu-ui/packages/vuu-table-types/index.d.ts +++ b/vuu-ui/packages/vuu-table-types/index.d.ts @@ -30,6 +30,8 @@ import type { } from "react"; import { CellPos } from "@finos/vuu-table/src/table-dom-utils"; +export declare type GroupToggleTarget = "toggle-icon" | "group-column"; + export declare type TableSelectionModel = | "none" | "single" @@ -234,6 +236,11 @@ export interface ColumnDescriptor extends DataValueDescriptor { colHeaderContentRenderer?: string; colHeaderLabelRenderer?: string; flex?: number; + /** + * Only used when the column is included in a grouby clause. + * The icon will be displayed alongside the group label + */ + getIcon?: (row: DataSourceRow) => string | undefined; /** Optional additional level(s) of heading to display above label. May span multiple columns, if multiple adjacent columns declare @@ -385,12 +392,14 @@ export interface BaseRowProps { export interface RowProps extends BaseRowProps { classNameGenerator?: RowClassNameGenerator; columnMap: ColumnMap; + groupToggleTarget?: GroupToggleTarget; highlighted?: boolean; - row: DataSourceRow; offset: number; onClick?: TableRowClickHandlerInternal; onDataEdited?: DataCellEditHandler; onToggleGroup?: (row: DataSourceRow, column: RuntimeColumnDescriptor) => void; + row: DataSourceRow; + showBookends?: boolean; zebraStripes?: boolean; } diff --git a/vuu-ui/packages/vuu-table/src/Row.css b/vuu-ui/packages/vuu-table/src/Row.css index f72793d3a..bd2aebbfd 100644 --- a/vuu-ui/packages/vuu-table/src/Row.css +++ b/vuu-ui/packages/vuu-table/src/Row.css @@ -117,7 +117,7 @@ border-radius: var(--vuu-selection-decorator-right-radius, 0); } -.vuuTableRow-expanded { +.vuuTableRow[aria-expanded="true"] { --toggle-icon-transform: rotate(90deg); } diff --git a/vuu-ui/packages/vuu-table/src/Row.tsx b/vuu-ui/packages/vuu-table/src/Row.tsx index 889cb981b..d0b139926 100644 --- a/vuu-ui/packages/vuu-table/src/Row.tsx +++ b/vuu-ui/packages/vuu-table/src/Row.tsx @@ -5,6 +5,7 @@ import { isJsonGroup, isNotHidden, metadataKeys, + queryClosest, RowSelected, } from "@finos/vuu-utils"; import { useComponentCssInjection } from "@salt-ds/styles"; @@ -16,7 +17,7 @@ import { TableCell, TableGroupCell } from "./table-cell"; import rowCss from "./Row.css"; import { VirtualColSpan } from "./VirtualColSpan"; -const { IDX, IS_EXPANDED, SELECTED } = metadataKeys; +const { COUNT, DEPTH, IDX, IS_EXPANDED, IS_LEAF, SELECTED } = metadataKeys; const classBase = "vuuTableRow"; // A dummy Table Row rendered once and not visible. We measure this to @@ -50,12 +51,14 @@ export const Row = memo( classNameGenerator, columnMap, columns, + groupToggleTarget = "group-column", highlighted, row, offset, onClick, onDataEdited, onToggleGroup, + showBookends = true, virtualColSpan = 0, zebraStripes = false, ...htmlAttributes @@ -68,8 +71,11 @@ export const Row = memo( }); const { + [COUNT]: childRowCount, + [DEPTH]: depth, [IDX]: rowIndex, [IS_EXPANDED]: isExpanded, + [IS_LEAF]: isLeaf, [SELECTED]: selectionStatus, } = row; @@ -90,7 +96,6 @@ export const Row = memo( classNameGenerator?.(row, columnMap), { [`${classBase}-even`]: zebraStripes && rowIndex % 2 === 0, - [`${classBase}-expanded`]: isExpanded, [`${classBase}-highlighted`]: highlighted, [`${classBase}-selected`]: selectionStatus & True, [`${classBase}-selectedStart`]: selectionStatus & First, @@ -98,33 +103,45 @@ export const Row = memo( }, ); + const canExpand = isLeaf === false && childRowCount > 0; + const ariaExpanded = isExpanded ? true : canExpand ? false : undefined; + const ariaLevel = isLeaf ? undefined : depth; + // const style = { transform: `translate3d(0px, ${offset}px, 0px)` }; const style = { top: offset }; const handleGroupCellClick = useCallback( (evt: MouseEvent, column: RuntimeColumnDescriptor) => { if (isGroupColumn(column) || isJsonGroup(column, row, columnMap)) { - evt.stopPropagation(); + if (groupToggleTarget === "toggle-icon") { + if (queryClosest(evt.target, "button") === null) { + return; + } + } onToggleGroup?.(row, column); } }, - [columnMap, onToggleGroup, row], + [columnMap, groupToggleTarget, onToggleGroup, row], ); return (
- + {showBookends ? ( + + ) : null} {columns.filter(isNotHidden).map((column) => { const isGroup = isGroupColumn(column); const isJsonCell = isJsonColumn(column); - const Cell = isGroup ? TableGroupCell : TableCell; + const Cell = isGroup && !isJsonCell ? TableGroupCell : TableCell; return ( ); })} - + {showBookends ? ( + + ) : null}
); }, diff --git a/vuu-ui/packages/vuu-table/src/Table.css b/vuu-ui/packages/vuu-table/src/Table.css index bf4ebb18b..215597144 100644 --- a/vuu-ui/packages/vuu-table/src/Table.css +++ b/vuu-ui/packages/vuu-table/src/Table.css @@ -149,6 +149,7 @@ height: var(--row-height); position: absolute; width: 50px; + z-index: -1; } .vuuPinLeft, diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index 94c5894f8..a525b2425 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -6,6 +6,7 @@ import { import { ContextMenuProvider } from "@finos/vuu-popups"; import { CustomHeader, + GroupToggleTarget, RowProps, TableConfig, TableConfigChangeHandler, @@ -52,7 +53,7 @@ const classBase = "vuuTable"; const { IDX, RENDER_IDX } = metadataKeys; -export type TableNavigationStyle = "none" | "cell" | "row"; +export type TableNavigationStyle = "none" | "cell" | "row" | "tree"; export interface TableProps extends Omit { @@ -89,6 +90,15 @@ export interface TableProps * Function Component is used, it will be passed the props described in BaseRowProps. */ customHeader?: CustomHeader | CustomHeader[]; + /** + * When rows are grouped, user can click group row(s) to expand/collapse display of child rows. + * This allows precise configuration of where user may click to trigger toggle/collapse. When + * row selection is also supported, clicking outside the region specified here will select or + * deselect the row. + * - toggle-icon - the small toggle icon must be clicked directly + * - group-column (default) - user can click anywhere within the group column, i.e on the icon or the column text + */ + groupToggleTarget?: GroupToggleTarget; /** * Defined how focus navigation within data cells will be handled by table. * Default is cell. @@ -104,6 +114,8 @@ export interface TableProps /** * Determines bahaviour of keyboard navigation , either row focused or cell focused. + * `tree` is a specialised navigation behaviour only useful where table is being + * used to present purely grouped data (see TreeTable) */ navigationStyle?: TableNavigationStyle; /** @@ -153,7 +165,7 @@ export interface TableProps /** * Selection Bookends style the left and right edge of a selection block. - * They are optional, value defaults to zero. + * They are optional, value currently defaults to 4. * TODO this should just live in CSS */ selectionBookendWidth?: number; @@ -205,6 +217,7 @@ const TableCore = ({ customHeader, dataSource, disableFocus = false, + groupToggleTarget, highlightedIndex: highlightedIndexProp, id: idProp, navigationStyle = "cell", @@ -220,6 +233,7 @@ const TableCore = ({ renderBufferSize = 0, rowHeight, scrollingApiRef, + selectionBookendWidth = 0, selectionModel = "extended", showColumnHeaders = true, showColumnHeaderMenus = true, @@ -287,6 +301,7 @@ const TableCore = ({ renderBufferSize, rowHeight, scrollingApiRef, + selectionBookendWidth, selectionModel, showColumnHeaders, showPaginationControls, @@ -363,23 +378,28 @@ const TableCore = ({ ) : null} {readyToRenderTableBody ? (
- {data.map((data) => ( - - ))} + {data.map((data) => { + const ariaRowIndex = data[IDX] + headerCount + 1; + return ( + 0} + virtualColSpan={scrollProps.virtualColSpan} + zebraStripes={tableAttributes.zebraStripes} + /> + ); + })} {/* The focusCellPlaceholder allows us to deal with the situation where a cell that has focus is scrolled out of the viewport. That cell, along with the @@ -430,6 +450,7 @@ export const Table = forwardRef(function Table( customHeader, dataSource, disableFocus, + groupToggleTarget, height, highlightedIndex, id, @@ -447,6 +468,7 @@ export const Table = forwardRef(function Table( renderBufferSize, rowHeight: rowHeightProp, scrollingApiRef, + selectionBookendWidth = 4, selectionModel, showColumnHeaders, showColumnHeaderMenus, @@ -564,6 +586,7 @@ export const Table = forwardRef(function Table( customHeader={customHeader} dataSource={dataSource} disableFocus={disableFocus} + groupToggleTarget={groupToggleTarget} highlightedIndex={highlightedIndex} id={id} navigationStyle={navigationStyle} @@ -581,6 +604,7 @@ export const Table = forwardRef(function Table( } rowHeight={rowHeight} scrollingApiRef={scrollingApiRef} + selectionBookendWidth={selectionBookendWidth} selectionModel={selectionModel} showColumnHeaders={showColumnHeaders} showColumnHeaderMenus={showColumnHeaderMenus} diff --git a/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.css b/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.css index cb436e284..98cc93b97 100644 --- a/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.css +++ b/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.css @@ -79,9 +79,6 @@ } } .vuuTable.vuu-cellblock-select-in-progress-mouse { - .vuuTableCell:hover { - anchor-name: --cellblock-end; - } .vuuCellBlock { pointer-events: none; } diff --git a/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.tsx b/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.tsx index dbae7b8c7..fd8645355 100644 --- a/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.tsx +++ b/vuu-ui/packages/vuu-table/src/cell-block/CellBlock.tsx @@ -1,6 +1,7 @@ import { HTMLAttributes, KeyboardEventHandler, + MouseEventHandler, forwardRef, useCallback, } from "react"; @@ -36,11 +37,16 @@ export const CellBlock = forwardRef( [onCopy], ); + const onContextMenu = useCallback(() => { + console.log("on cvontext menu"); + }, []); + return (
diff --git a/vuu-ui/packages/vuu-table/src/cell-block/cellblock-utils.ts b/vuu-ui/packages/vuu-table/src/cell-block/cellblock-utils.ts index 0af2ee462..2ac07091a 100644 --- a/vuu-ui/packages/vuu-table/src/cell-block/cellblock-utils.ts +++ b/vuu-ui/packages/vuu-table/src/cell-block/cellblock-utils.ts @@ -1,6 +1,10 @@ import { VuuRange } from "@finos/vuu-protocol-types"; import { queryClosest } from "@finos/vuu-utils"; -import { getTableCellPos } from "../table-dom-utils"; +import { + getAriaCellPos, + getAriaColIndex, + getAriaRowIndex, +} from "../table-dom-utils"; export type TableCellBlock = { columnRange: VuuRange; @@ -67,30 +71,15 @@ export const outsideBox = ( y: number, ) => x < left || x > right || y < top || y > bottom; -const getColIndex = ({ ariaColIndex }: HTMLDivElement) => { - if (ariaColIndex !== null) { - return parseInt(ariaColIndex); - } - throw Error("invalid aria-colindex in table cell"); -}; - -const getRowIndex = (cell: HTMLDivElement) => { - const row = queryClosest(cell, ".vuuTableRow"); - if (row) { - const { ariaRowIndex } = row; - if (ariaRowIndex !== null) { - return parseInt(ariaRowIndex); - } - } - throw Error("invalid aria-rowindex in table row"); -}; +const getRowIndex = (cell: HTMLDivElement) => + getAriaRowIndex(queryClosest(cell, ".vuuTableRow")); export const getTableCellBlock = ( startCell: HTMLDivElement, endCell: HTMLDivElement, ): TableCellBlock => { - const colStart = getColIndex(startCell); - const colEnd = getColIndex(endCell); + const colStart = getAriaColIndex(startCell); + const colEnd = getAriaColIndex(endCell); const rowStart = getRowIndex(startCell); const rowEnd = getRowIndex(endCell); @@ -166,8 +155,8 @@ export const getTextFromCells = ( ".vuuTable-body", true, ); - const [startRow, startCol] = getTableCellPos(startCell); - const [endRow, endCol] = getTableCellPos(endCell); + const [startRow, startCol] = getAriaCellPos(startCell); + const [endRow, endCol] = getAriaCellPos(endCell); const rowRange = { from: Math.min(startRow, endRow), @@ -182,12 +171,12 @@ export const getTextFromCells = ( const results: string[][] = []; for (let rowIdx = rowRange.from; rowIdx <= rowRange.to; rowIdx++) { const row = tableBody.querySelector( - `.vuuTableRow[aria-rowindex='${rowIdx + 1}']`, + `.vuuTableRow[aria-rowindex='${rowIdx}']`, ); const rowData = []; for (let colIdx = colRange.from; colIdx <= colRange.to; colIdx++) { const cell = row?.querySelector( - `.vuuTableCell[aria-colindex='${colIdx + 1}']`, + `.vuuTableCell[aria-colindex='${colIdx}']`, ); if (cell) { rowData.push(cell.textContent ?? ""); diff --git a/vuu-ui/packages/vuu-table/src/cell-block/useCellBlockSelection.tsx b/vuu-ui/packages/vuu-table/src/cell-block/useCellBlockSelection.tsx index d20199930..df41e9222 100644 --- a/vuu-ui/packages/vuu-table/src/cell-block/useCellBlockSelection.tsx +++ b/vuu-ui/packages/vuu-table/src/cell-block/useCellBlockSelection.tsx @@ -10,11 +10,10 @@ import { useState, } from "react"; import { + getAriaCellPos, getNextCellPos, getTableCell, - getTableCellPos, } from "../table-dom-utils"; -import { FocusCell } from "../useCellFocus"; import { CellBlock } from "./CellBlock"; import { PosTuple, @@ -59,7 +58,6 @@ export interface CellblockSelectionHookProps { allowCellBlockSelection?: boolean; columnCount?: number; containerRef: RefObject; - focusCell: FocusCell; onSelectCellBlock?: (cellBlock: TableCellBlock) => void; rowCount?: number; } @@ -68,7 +66,6 @@ export const useCellBlockSelection = ({ allowCellBlockSelection, columnCount = 0, containerRef, - focusCell, onSelectCellBlock, rowCount = 0, }: CellblockSelectionHookProps) => { @@ -216,22 +213,49 @@ export const useCellBlockSelection = ({ removeMouseListener("mouseUpPreDrag"); }, [removeMouseListener]); + const handleNativeMouseOver = useCallback((evt: MouseEvent) => { + const cell = queryClosest(evt.target, ".vuuTableCell"); + if (cell) { + stateRef.current.endPos = getAriaCellPos(cell); + stateRef.current.endCell?.classList.remove("vuu-cellblock-end"); + stateRef.current.endCell = cell; + setElementBox(cell, stateRef.current.endBox); + updateCellBlockClassName(stateRef.current); + + cell?.classList.add("vuu-cellblock-end"); + } + }, []); + + const handleNativeMouseUp = useCallback(() => { + window.removeEventListener("mouseover", handleNativeMouseOver); + }, [handleNativeMouseOver]); + const handleMouseDown = useCallback( (evt) => { - initializeStateRef(); - const { current: state } = stateRef; - const cell = queryClosest(evt.target, ".vuuTableCell"); - if (cell) { - state.startCell = cell; - state.mouseStartX = evt.clientX; - state.mouseStartY = evt.clientY; - - const { mouseMovePreDrag, mouseUpPreDrag } = handlersRef.current; - addMouseListener("mouseMovePreDrag", mouseMovePreDrag); - addMouseListener("mouseUpPreDrag", mouseUpPreDrag); + if (evt.button === 0) { + initializeStateRef(); + const { current: state } = stateRef; + const cell = queryClosest(evt.target, ".vuuTableCell"); + if (cell) { + state.startCell = cell; + state.mouseStartX = evt.clientX; + state.mouseStartY = evt.clientY; + + const { mouseMovePreDrag, mouseUpPreDrag } = handlersRef.current; + addMouseListener("mouseMovePreDrag", mouseMovePreDrag); + addMouseListener("mouseUpPreDrag", mouseUpPreDrag); + console.log("register mouse enter"); + window.addEventListener("mouseover", handleNativeMouseOver); + window.addEventListener("mouseup", handleNativeMouseUp); + } } }, - [addMouseListener, initializeStateRef], + [ + addMouseListener, + handleNativeMouseOver, + handleNativeMouseUp, + initializeStateRef, + ], ); const nativeKeyDownHandlerRef = useRef(NullHandler); @@ -253,6 +277,7 @@ export const useCellBlockSelection = ({ }); } }, []); + const handleNativeKeyDown = (nativeKeyDownHandlerRef.current = useCallback( ({ key }: KeyboardEvent) => { if (isArrowKey(key)) { @@ -262,14 +287,13 @@ export const useCellBlockSelection = ({ } const nextCell = getNextCellPos(key, endPos, columnCount, rowCount); stateRef.current.endPos = nextCell; - focusCell(nextCell); const cell = getTableCell(containerRef, nextCell); stateRef.current.endCell = cell as HTMLDivElement; setElementBox(cell, endBox); updateCellBlockClassName(stateRef.current); } }, - [columnCount, containerRef, createCellBlock, focusCell, rowCount], + [columnCount, containerRef, createCellBlock, rowCount], )); const handleKeyDown = useCallback( (evt) => { @@ -277,7 +301,7 @@ export const useCellBlockSelection = ({ initializeStateRef(); const cell = queryClosest(evt.target, ".vuuTableCell"); if (cell) { - const startPos = getTableCellPos(cell); + const startPos = getAriaCellPos(cell); stateRef.current.startPos = startPos; stateRef.current.endPos = clone(startPos); const { current: state } = stateRef; diff --git a/vuu-ui/packages/vuu-table/src/column-header-pill/ColumnHeaderPill.tsx b/vuu-ui/packages/vuu-table/src/column-header-pill/ColumnHeaderPill.tsx index fba9b5e0e..62d172d9f 100644 --- a/vuu-ui/packages/vuu-table/src/column-header-pill/ColumnHeaderPill.tsx +++ b/vuu-ui/packages/vuu-table/src/column-header-pill/ColumnHeaderPill.tsx @@ -29,9 +29,11 @@ export const ColumnHeaderPill = ({ window: targetWindow, }); + console.log({ htmlAttributes }); + if (removable && typeof onRemove !== "function") { throw Error( - "ColumnHeaderPill onRemove prop must be provided if Pill is removable" + "ColumnHeaderPill onRemove prop must be provided if Pill is removable", ); } @@ -41,11 +43,11 @@ export const ColumnHeaderPill = ({ evt.stopPropagation(); onRemove?.(column); }, - [column, onRemove] + [column, onRemove], ); return ( -
+
{children} {removable ? ( '); + --vuu-svg-cog: url('data:image/svg+xml;utf8,'); } .vuuColumnMenu { - --menu-button-size: calc(var(--salt-size-base) - var(--salt-spacing-100)); - --saltButton-padding: var(--salt-spacing-50); - --saltButton-minWidth: var(--menu-button-size); - --saltButton-height: var(--menu-button-size); - --saltButton-width: var(--menu-button-size); + --menu-button-size: calc(var(--salt-size-base) - var(--salt-spacing-100)); + --saltButton-padding: var(--salt-spacing-50); + --saltButton-minWidth: var(--menu-button-size); + --saltButton-height: var(--menu-button-size); + --saltButton-width: var(--menu-button-size); - --vuu-icon-height: var(--menu-button-size); - --vuu-icon-left: 0px; - --vuu-icon-top: 0px; - --vuu-icon-width: var(--menu-button-size); + --vuu-icon-height: var(--menu-button-size); + --vuu-icon-left: 0px; + --vuu-icon-top: 0px; + --vuu-icon-width: var(--menu-button-size); - border-radius: 4px; - flex: 0 0 var(--menu-button-size); - margin: var(--vuuTable-columnMenu-margin, 0); + border: none; + border-radius: 4px; + flex: 0 0 var(--menu-button-size); + margin: var(--vuuTable-columnMenu-margin, 0); + padding: 0; } - - \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/context-menu/buildContextMenuDescriptors.ts b/vuu-ui/packages/vuu-table/src/context-menu/buildContextMenuDescriptors.ts index a7b4f767c..669083de7 100644 --- a/vuu-ui/packages/vuu-table/src/context-menu/buildContextMenuDescriptors.ts +++ b/vuu-ui/packages/vuu-table/src/context-menu/buildContextMenuDescriptors.ts @@ -20,13 +20,13 @@ export const buildContextMenuDescriptors = //TODO which should it be ? if (location === "header" || location === "column-menu") { descriptors.push( - ...buildSortMenuItems(options as MaybeColumn, dataSource) + ...buildSortMenuItems(options as MaybeColumn, dataSource), ); descriptors.push( - ...buildGroupMenuItems(options as MaybeColumn, dataSource) + ...buildGroupMenuItems(options as MaybeColumn, dataSource), ); descriptors.push( - ...buildAggregationMenuItems(options as MaybeColumn, dataSource) + ...buildAggregationMenuItems(options as MaybeColumn, dataSource), ); descriptors.push(...buildColumnDisplayMenuItems(options as MaybeColumn)); descriptors.push({ @@ -56,7 +56,7 @@ export const buildContextMenuDescriptors = function buildSortMenuItems( options: MaybeColumn, - { sort: { sortDefs } }: DataSource + { sort: { sortDefs } }: DataSource, ): ContextMenuItemDescriptor[] { const { column } = options; const menuItems: ContextMenuItemDescriptor[] = []; @@ -138,10 +138,10 @@ function buildSortMenuItems( function buildAggregationMenuItems( options: MaybeColumn, - dataSource: DataSource + dataSource: DataSource, ): ContextMenuItemDescriptor[] { const { column } = options; - if (column === undefined || dataSource.groupBy.length === 0) { + if (column === undefined || dataSource.groupBy?.length === 0) { return []; } const { name, label = name } = column; @@ -160,7 +160,7 @@ function buildAggregationMenuItems( { label: "High", action: "agg-high", options }, { label: "Low", action: "agg-low", options }, ] - : [] + : [], ), }, ]; @@ -171,14 +171,14 @@ const pinColumn = (options: unknown, pinLocation: PinLocation) => label: `Pin ${pinLocation}`, action: `column-pin-${pinLocation}`, options, - } as ContextMenuItemDescriptor); + }) 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 + options: MaybeColumn, ): ContextMenuItemDescriptor[] { const { column } = options; if (column === undefined) { @@ -210,7 +210,7 @@ function buildColumnDisplayMenuItems( { label: `Pin column`, children: [pinFloating(options), pinRight(options)], - } + }, ); } else if (pin === "right") { menuItems.push( @@ -218,7 +218,7 @@ function buildColumnDisplayMenuItems( { label: `Pin column`, children: [pinLeft(options), pinFloating(options)], - } + }, ); } else if (pin === "floating") { menuItems.push( @@ -226,7 +226,7 @@ function buildColumnDisplayMenuItems( { label: `Pin column`, children: [pinLeft(options), pinRight(options)], - } + }, ); } @@ -235,7 +235,7 @@ function buildColumnDisplayMenuItems( function buildGroupMenuItems( options: MaybeColumn, - { groupBy }: DataSource + { groupBy }: DataSource, ): ContextMenuItemDescriptor[] { const { column } = options; const menuItems: ContextMenuItemDescriptor[] = []; @@ -245,7 +245,7 @@ function buildGroupMenuItems( const { name, label = name } = column; - if (groupBy.length === 0) { + if (groupBy?.length === 0) { menuItems.push({ label: `Group by ${label}`, action: "group", diff --git a/vuu-ui/packages/vuu-table/src/context-menu/useHandleTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/context-menu/useHandleTableContextMenu.ts index a250b68cc..0261cb90b 100644 --- a/vuu-ui/packages/vuu-table/src/context-menu/useHandleTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/context-menu/useHandleTableContextMenu.ts @@ -34,12 +34,12 @@ export interface ContextMenuHookProps { const removeFilterColumn = ( dataSourceFilter: DataSourceFilter, - column: RuntimeColumnDescriptor + column: RuntimeColumnDescriptor, ) => { if (dataSourceFilter.filterStruct && column) { const [filterStruct, filter] = removeColumnFromFilter( column, - dataSourceFilter.filterStruct + dataSourceFilter.filterStruct, ); return { filter, @@ -67,8 +67,8 @@ export const useHandleTableContextMenu = ({ 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 "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; diff --git a/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.css b/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.css index b6a61930c..0590bd30b 100644 --- a/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.css +++ b/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.css @@ -1,65 +1,100 @@ - .vuu-theme { - --svg-spinner: url('data:image/svg+xml;utf8,'); + --svg-spinner: url('data:image/svg+xml;utf8,'); } .vuuTableGroupHeaderCell { --vuuColumnHeaderPill-margin: 0; - --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 */ - } + --cell-align: "flex-start"; + text-align: left; + cursor: default; + /* ensure header row sits atop everything else when scrolling down */ + --vuuColumnHeaderPill-margin: 0; + --vuuColumnHeaderPill-flex: 0 0 24px; - .vuuTableGroupHeaderCell-inner { - align-items: center; - display: flex; - gap: 4px; - height: 100%; - padding-left: 1px; - } + align-items: center; + background-color: var( + --vuuTableHeaderCell-background, + var(--table-background) + ); + border-bottom: none; + border-right-color: var(--cell-borderColor); + border-right-style: solid; + border-right-width: 1px; + box-sizing: border-box; + display: inline-flex; + gap: 4px; + height: 100%; + padding: 0 12px 0 4px; + position: relative; + vertical-align: top; - .vuuTableGroupHeaderCell-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; + &.vuuPinLeft, + &.vuuPinRight { + background-color: var( + --vuuTableHeaderCell-background, + var(--table-background) + ); } +} - .vuuTableGroupHeaderCell-label { - align-items: center; - display: flex; - flex: 0 0 auto; - } +.vuuTableGroupHeaderCell-inner { + align-items: center; + display: flex; + gap: 4px; + height: 100%; + padding-left: 1px; +} - .vuuTableGroupHeaderCell-close { - --vuu-icon-height: 18px; - --vuu-icon-width: 18px; - cursor: pointer; - left: 3px; - } - - .vuuTableGroupHeaderCell-resizing { - --columnResizer-color: var(--salt-color-blue-500); - --columnResizer-height: var(--table-height); - --columnResizer-width: 2px; - } - .vuuTableGroupHeaderCell-pending { - --pending-content: ''; - } +.vuuTableGroupHeaderCell-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; +} - .vuuTableGroupHeaderCell-col:has(+ .vuuColumnResizer):after { - content: var(--pending-content); - width: 24px; - height:24px; - background-image: var(--svg-spinner); - background-repeat: no-repeat; - background-size: cover; - } +.vuuTableGroupHeaderCell-label { + align-items: center; + display: flex; + flex: 0 0 auto; +} + +.vuuTableGroupHeaderCell-close { + --vuu-icon-height: 18px; + --vuu-icon-width: 18px; + cursor: pointer; + left: 3px; +} + +.vuuTableGroupHeaderCell-resizing { + --columnResizer-color: var(--salt-color-blue-500); + --columnResizer-height: var(--table-height); + --columnResizer-width: 2px; +} +.vuuTableGroupHeaderCell-pending { + --pending-content: ""; +} + +.vuuTableGroupHeaderCell-col:has(+ .vuuColumnResizer):after { + content: var(--pending-content); + width: 24px; + height: 24px; + background-image: var(--svg-spinner); + background-repeat: no-repeat; + background-size: cover; +} + +.vuuTableGroupHeaderCell:focus { + outline: var( + --vuuTableCell-outline, + solid var(--salt-focused-outlineColor) 2px + ); + outline-offset: -2px; + /** This is to achieve a white background to outline dashes */ + box-shadow: inset 0 0 0 var(--cell-outline-width) white; + border-bottom: none; +} diff --git a/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.tsx b/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.tsx index d01246d4e..c838782c6 100644 --- a/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.tsx +++ b/vuu-ui/packages/vuu-table/src/header-cell/GroupHeaderCell.tsx @@ -9,7 +9,13 @@ import { useLayoutEffectSkipFirst } from "@finos/vuu-utils"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import cx from "clsx"; -import { useCallback, useRef, useState } from "react"; +import { + KeyboardEventHandler, + MouseEventHandler, + useCallback, + useRef, + useState, +} from "react"; import { ColumnHeaderPill, GroupColumnPill } from "../column-header-pill"; import { ColumnResizer, useTableColumnResize } from "../column-resizing"; import { useCell } from "../useCell"; @@ -20,7 +26,7 @@ const classBase = "vuuTableGroupHeaderCell"; const switchIfChanged = ( columns: RuntimeColumnDescriptor[], - newColumns: RuntimeColumnDescriptor[] + newColumns: RuntimeColumnDescriptor[], ) => { if (columns === newColumns) { return columns; @@ -84,13 +90,22 @@ export const GroupHeaderCell = ({ } }); }, - [onMoveColumn] + [onMoveColumn], ); useLayoutEffectSkipFirst(() => { setColumns((cols) => switchIfChanged(cols, groupColumn.columns)); }, [groupColumn.columns]); + const handleClick = useCallback>(() => { + console.log("click"); + }, []); + const handleKeyDown = useCallback< + KeyboardEventHandler + >(() => { + console.log("keydown"); + }, []); + return (
); })} { const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "vuu-table-cell", + css: tableCellCss, + window: targetWindow, + }); useComponentCssInjection({ testId: "vuu-table-group-cell", css: tableGroupCellCss, @@ -26,32 +33,32 @@ export const TableGroupCell = ({ }); const { columns } = column as GroupColumnDescriptor; - const [value, offset] = getGroupValueAndOffset(columns, row, columnMap); + const value = getGroupValue(columns, row, columnMap); + const icon = getGroupIcon(columns, row); const { className, style } = useCell(column, classBase); const handleClick = useCallback( (evt: MouseEvent) => { onClick?.(evt, column); }, - [column, onClick] + [column, onClick], ); - const isLeaf = row[IS_LEAF]; - const spacers = Array(offset) - .fill(0) - .map((n, i) => ); + const { [COUNT]: count, [IS_EXPANDED]: isExpanded, [IS_LEAF]: isLeaf } = row; return (
- {spacers} - {isLeaf ? null : ( - + + {isLeaf || count == 0 ? null : ( + )} + {icon ? : null} {value}
); diff --git a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts index b19b7ba21..8b5d17f1b 100644 --- a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts @@ -3,24 +3,41 @@ import { ScrollDirection } from "./useTableScroll"; import type { ArrowKey, PageKey } from "@finos/vuu-utils"; import type { CellPos } from "@finos/vuu-table-types"; -const NULL_CELL_POS: CellPos = [-1, -1]; - export type NavigationKey = PageKey | ArrowKey; export const headerCellQuery = (colIdx: number) => `.vuuTable-col-headers .vuuTableHeaderCell[aria-colindex='${colIdx}']`; -export const dataCellQuery = (rowIdx: number, colIdx: number) => - `.vuuTable-table [aria-rowindex='${rowIdx}'] > [aria-colindex='${colIdx}']`; +export const dataCellQuery = (ariaRowIdx: number, ariaColIdx: number) => + `.vuuTable-table [aria-rowindex='${ariaRowIdx}'] > [aria-colindex='${ariaColIdx}']`; +export const getLevelUp = ( + containerRef: RefObject, + cellPos: CellPos, +): CellPos => { + const cell = getTableCell(containerRef, cellPos); + let row = cell?.parentElement; + const level = parseInt(row?.ariaLevel ?? "1"); + if (level > 1) { + const targetLevel = `${level - 1}`; + while (row !== null && row.ariaLevel !== targetLevel) { + row = row.previousElementSibling as HTMLElement; + } + if (row) { + const nextRowIndex = parseInt(row.ariaRowIndex ?? "- 1"); + if (nextRowIndex !== -1) { + return [nextRowIndex - 1, 0]; + } + } + } + return cellPos; +}; export const getTableCell = ( containerRef: RefObject, [rowIdx, colIdx]: CellPos, ) => { const cssQuery = dataCellQuery(rowIdx, colIdx); - const cell = containerRef.current?.querySelector( - cssQuery, - ) as HTMLTableCellElement; + const cell = containerRef.current?.querySelector(cssQuery) as HTMLDivElement; if (cellIsEditable(cell)) { // Dropdown gets focus, Input does not @@ -31,6 +48,16 @@ export const getTableCell = ( } }; +export const getFocusedCell = (el: HTMLElement | Element | null) => { + if (el?.role == "cell" || el?.role === "columnheader") { + return el as HTMLDivElement; + } else { + return el?.closest( + "[role='columnHeader'],[role='cell']", + ) as HTMLDivElement | null; + } +}; + export const cellIsEditable = (cell: HTMLDivElement | null) => cell?.classList.contains("vuuTableCell-editable"); @@ -41,6 +68,20 @@ export const cellDropdownShowing = (cell: HTMLDivElement | null) => { return false; }; +const cellIsGroupCell = (cell: HTMLElement | null) => + cell?.classList.contains("vuuTableGroupCell"); + +const rowIsExpanded = (cell: HTMLElement) => { + switch (cell.parentElement?.ariaExpanded) { + case "true": + return true; + case "false": + return false; + default: + return undefined; + } +}; + export const cellIsTextInput = (cell: HTMLElement) => cell.querySelector(".vuuTableInputCell") !== null; @@ -55,8 +96,8 @@ export const getAriaRowIndex = (rowElement: HTMLElement | null) => { return -1; }; -export const getAriaColIndex = (rowElement: HTMLElement | null) => { - const colIndex = rowElement?.ariaColIndex; +export const getAriaColIndex = (cellElement: HTMLElement | null) => { + const colIndex = cellElement?.ariaColIndex; if (colIndex != null) { const index = parseInt(colIndex); if (!isNaN(index)) { @@ -87,27 +128,9 @@ export const getRowElementByAriaIndex = ( } }; -export const getIndexFromRowElement = (rowElement: HTMLElement | null) => { - const ariaRowIndex = getAriaRowIndex(rowElement); - return ariaRowIndex === -1 ? -1 : ariaRowIndex - 1; -}; - export const getIndexFromCellElement = (cellElement: HTMLElement | null) => getAriaColIndex(cellElement); -export const getTableCellPos = (tableCell: HTMLDivElement): CellPos => { - const colIdx = getIndexFromCellElement(tableCell); - if (tableCell.role === "columnHeader") { - return [-1, colIdx]; - } else { - const focusedRow = tableCell.closest("[role='row']") as HTMLElement; - if (focusedRow) { - return [getIndexFromRowElement(focusedRow), colIdx]; - } - } - return NULL_CELL_POS; -}; - export const getAriaCellPos = (tableCell: HTMLDivElement): CellPos => { const focusedRow = tableCell.closest("[role='row']") as HTMLElement; return [getAriaRowIndex(focusedRow), getAriaColIndex(tableCell)]; @@ -117,7 +140,7 @@ const closestRow = (el: HTMLElement) => el.closest('[role="row"]') as HTMLElement; export const closestRowIndex = (el: HTMLElement) => - getIndexFromRowElement(closestRow(el)); + getAriaRowIndex(closestRow(el)); export function getNextCellPos( key: ArrowKey, @@ -155,6 +178,40 @@ export function getNextCellPos( return [rowIdx, colIdx]; } +export type TreeNodeOperation = "expand" | "collapse" | "level-up"; + +export const getTreeNodeOperation = ( + containerRef: RefObject, + navigationStyle: "cell" | "tree", + cellPos: CellPos, + key: NavigationKey, + shiftKey: boolean, +): TreeNodeOperation | undefined => { + const cell = getTableCell(containerRef, cellPos); + if (navigationStyle === "cell" && !cellIsGroupCell(cell)) { + return undefined; + } + if (navigationStyle == "cell" && !shiftKey) { + return undefined; + } + if (cellIsGroupCell(cell)) { + const isExpanded = rowIsExpanded(cell); + if (isExpanded === true) { + if (key === "ArrowLeft") { + return "collapse"; + } + } else if (isExpanded === false) { + if (key === "ArrowRight") { + return "expand"; + } else if (key === "ArrowLeft") { + return "level-up"; + } + } else if (key === "ArrowLeft") { + return "level-up"; + } + } +}; + const NO_SCROLL_NECESSARY = [undefined, undefined] as const; export const howFarIsRowOutsideViewport = ( diff --git a/vuu-ui/packages/vuu-table/src/useCell.ts b/vuu-ui/packages/vuu-table/src/useCell.ts index b5c23be13..40b3818e9 100644 --- a/vuu-ui/packages/vuu-table/src/useCell.ts +++ b/vuu-ui/packages/vuu-table/src/useCell.ts @@ -6,16 +6,15 @@ import { useMemo } from "react"; export const useCell = ( column: RuntimeColumnDescriptor, classBase: string, - isHeader?: boolean + isHeader?: boolean, ) => // TODO measure perf without the memo, might not be worth the cost useMemo(() => { - const className = cx(classBase, { + const className = cx(classBase, column.className, { vuuPinFloating: column.pin === "floating", vuuPinLeft: column.pin === "left", vuuPinRight: column.pin === "right", vuuEndPin: isHeader && column.endPin, - // [`${classBase}-resizing`]: column.resizing, [`${classBase}-editable`]: column.editable, [`${classBase}-right`]: column.align === "right", }); diff --git a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts index 79896d981..81300b1d1 100644 --- a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts @@ -15,8 +15,11 @@ import { NavigationKey, cellDropdownShowing, closestRowIndex, - getNextCellPos, getAriaCellPos, + getFocusedCell, + getNextCellPos, + getTreeNodeOperation, + getLevelUp as getLevelUp, } from "./table-dom-utils"; import { ScrollRequestHandler } from "./useTableScroll"; import { FocusCell } from "./useCellFocus"; @@ -31,6 +34,11 @@ const rowNavigationKeys = new Set([ "ArrowUp", ]); +const CellLocator = + ".vuuTableCell,.vuuTableHeaderCell,.vuuTableGroupHeaderCell"; + +const CellControlLocator = ".vuuColumnMenu,.vuuColumnHeaderPill"; + const cellNavigationKeys = new Set(rowNavigationKeys); cellNavigationKeys.add("ArrowLeft"); cellNavigationKeys.add("ArrowRight"); @@ -41,6 +49,7 @@ export const isNavigationKey = ( ): key is NavigationKey => { switch (navigationStyle) { case "cell": + case "tree": return cellNavigationKeys.has(key as NavigationKey); case "row": return rowNavigationKeys.has(key as NavigationKey); @@ -49,10 +58,7 @@ export const isNavigationKey = ( } }; -const focusColumnMenuIfAppropriate = ( - e: KeyboardEvent, - el: HTMLElement | null, -) => { +const focusControlWithinCell = (e: KeyboardEvent, el: HTMLElement | null) => { if (e.shiftKey && e.key.match(/Arrow(Left|Right)/)) { if (el?.classList.contains("vuuTableHeaderCell")) { const menuButton = el?.querySelector(".vuuColumnMenu"); @@ -60,6 +66,31 @@ const focusColumnMenuIfAppropriate = ( menuButton.focus(); return true; } + } else if (el?.classList.contains("vuuTableGroupHeaderCell")) { + const headerPill = el?.querySelector( + ".vuuColumnHeaderPill", + ); + if (headerPill) { + headerPill.focus(); + return true; + } + } else if (el?.classList.contains("vuuColumnHeaderPill")) { + const nextPill = el.parentElement?.nextElementSibling + ?.firstChild as HTMLElement; + if (nextPill?.classList.contains("vuuColumnHeaderPill")) { + nextPill.focus(); + return true; + } else { + const removeButton = queryClosest( + el, + ".vuuTableGroupHeaderCell", + true, + ).querySelector(".vuuTableGroupHeaderCell-removeAll") as HTMLElement; + if (removeButton) { + removeButton.focus(); + return true; + } + } } } return false; @@ -69,6 +100,11 @@ const PageKeys = ["Home", "End", "PageUp", "PageDown"]; export const isPagingKey = (key: string): key is PageKey => PageKeys.includes(key); +export type GroupToggleHandler = ( + treeNodeOperation: "expand" | "collapse", + rowIndex: number, +) => void; + export interface NavigationHookProps { cellFocusStateRef: MutableRefObject; containerRef: RefObject; @@ -83,6 +119,7 @@ export interface NavigationHookProps { navigationStyle: TableNavigationStyle; viewportRange: VuuRange; onHighlight?: (idx: number) => void; + onToggleGroup: GroupToggleHandler; requestScroll?: ScrollRequestHandler; restoreLastFocus?: boolean; rowCount?: number; @@ -102,6 +139,7 @@ export const useKeyboardNavigation = ({ navigationStyle, requestScroll, onHighlight, + onToggleGroup, rowCount = 0, viewportRowCount, }: NavigationHookProps) => { @@ -125,20 +163,11 @@ export const useKeyboardNavigation = ({ (idx: number) => { onHighlight?.(idx); setHighlightedIdx(idx); + highlightedIndexRef.current = idx; }, [onHighlight, setHighlightedIdx], ); - const getFocusedCell = (el: HTMLElement | Element | null) => { - if (el?.role == "cell" || el?.role === "columnheader") { - return el as HTMLDivElement; - } else { - return el?.closest( - "[role='columnHeader'],[role='cell']", - ) as HTMLDivElement | null; - } - }; - const setActiveCell = useCallback( (rowIdx: number, colIdx: number, fromKeyboard = false) => { const pos: CellPos = [rowIdx, colIdx]; @@ -220,7 +249,6 @@ export const useKeyboardNavigation = ({ const focusedCell = getFocusedCell(document.activeElement); if (focusedCell) { cellFocusStateRef.current.cellPos = getAriaCellPos(focusedCell); - console.log({ pos: cellFocusStateRef.current.cellPos }); if (navigationStyle === "row") { setHighlightedIdx(cellFocusStateRef.current.cellPos[0]); } @@ -236,25 +264,58 @@ export const useKeyboardNavigation = ({ ]); const navigateChildItems = useCallback( - async (key: NavigationKey) => { - const { - current: { cellPos }, - } = cellFocusStateRef; - const [nextRowIdx, nextColIdx] = isPagingKey(key) - ? await nextPageItemIdx(key, cellPos) - : getNextCellPos(key, cellPos, columnCount, maxRowIndex); - + async ( + navigationStyle: "cell" | "tree" = "cell", + key: NavigationKey, + shiftKey = false, + ): Promise => { + const { cellPos } = cellFocusStateRef.current; const [rowIdx, colIdx] = cellPos; + let nextRowIdx = -1, + nextColIdx = -1; + + if (isPagingKey(key)) { + [nextRowIdx, nextColIdx] = await nextPageItemIdx(key, cellPos); + } else { + const treeNodeOperation = getTreeNodeOperation( + containerRef, + navigationStyle, + cellPos, + key, + shiftKey, + ); + if ( + treeNodeOperation === "expand" || + treeNodeOperation === "collapse" + ) { + onToggleGroup(treeNodeOperation, rowIdx - headerCount - 1); + } else if (treeNodeOperation === "level-up") { + [nextRowIdx, nextColIdx] = getLevelUp(containerRef, cellPos); + } else { + [nextRowIdx, nextColIdx] = getNextCellPos( + key, + cellPos, + columnCount, + maxRowIndex, + ); + } + } + if (nextRowIdx !== rowIdx || nextColIdx !== colIdx) { setActiveCell(nextRowIdx, nextColIdx, true); + setHighlightedIndex(nextRowIdx); } }, [ cellFocusStateRef, - columnCount, nextPageItemIdx, + containerRef, + onToggleGroup, + headerCount, + columnCount, maxRowIndex, setActiveCell, + setHighlightedIndex, ], ); @@ -270,7 +331,7 @@ export const useKeyboardNavigation = ({ const { current: highlighted } = highlightedIndexRef; const [nextRowIdx] = isPagingKey(key) ? await nextPageItemIdx(key, [highlighted ?? -1, 0]) - : getNextCellPos(key, [highlighted ?? -1, 0], columnCount, rowCount); + : getNextCellPos(key, [highlighted ?? -1, 0], columnCount, maxRowIndex); if (nextRowIdx !== highlighted) { setHighlightedIndex(nextRowIdx); // TO(DO make this a scroll request) @@ -279,8 +340,8 @@ export const useKeyboardNavigation = ({ }, [ columnCount, + maxRowIndex, nextPageItemIdx, - rowCount, scrollRowIntoViewIfNecessary, setHighlightedIndex, ], @@ -299,7 +360,7 @@ export const useKeyboardNavigation = ({ (e: KeyboardEvent) => { const cell = queryClosest( e.target, - ".vuuTableCell,.vuuColumnMenu,.vuuTableHeaderCell", + `${CellLocator},${CellControlLocator}`, ); if (cellDropdownShowing(cell)) { return; @@ -309,9 +370,9 @@ export const useKeyboardNavigation = ({ e.stopPropagation(); if (navigationStyle === "row") { moveHighlightedRow(e.key); - } else { - if (!focusColumnMenuIfAppropriate(e, cell)) { - navigateChildItems(e.key); + } else if (navigationStyle !== "none") { + if (!focusControlWithinCell(e, cell)) { + navigateChildItems(navigationStyle, e.key, e.shiftKey); } } } @@ -322,7 +383,7 @@ export const useKeyboardNavigation = ({ const handleClick = useCallback( // Might not be a cell e.g the Settings button (evt: MouseEvent) => { - const target = evt.target as HTMLElement; + const target = queryClosest(evt.target, CellLocator); const focusedCell = getFocusedCell(target); if (focusedCell) { const [rowIdx, colIdx] = getAriaCellPos(focusedCell); @@ -338,21 +399,24 @@ export const useKeyboardNavigation = ({ const handleMouseMove = useCallback( (evt: MouseEvent) => { - const idx = closestRowIndex(evt.target as HTMLElement); - if (idx !== -1 && idx !== highlightedIndexRef.current) { - setHighlightedIndex(idx); + const rowIdx = closestRowIndex(evt.target as HTMLElement); + if (rowIdx !== -1 && rowIdx !== highlightedIndexRef.current) { + setHighlightedIndex(rowIdx); } }, [setHighlightedIndex], ); - const navigate = useCallback(() => { - navigateChildItems("ArrowDown"); + /** + * used when editing cells + */ + const navigateCell = useCallback(() => { + navigateChildItems("cell", "ArrowDown"); }, [navigateChildItems]); return { highlightedIndexRef, - navigate, + navigateCell, onClick: handleClick, onFocus: handleFocus, onKeyDown: handleKeyDown, diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index 106e17b8b..f1a8bb35e 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -51,11 +51,14 @@ import { useHandleTableContextMenu, } from "./context-menu"; import { updateTableConfig } from "./table-config"; -import { getIndexFromRowElement } from "./table-dom-utils"; +import { getAriaRowIndex } from "./table-dom-utils"; import { useCellEditing } from "./useCellEditing"; import { FocusCell, useCellFocus } from "./useCellFocus"; import { useDataSource } from "./useDataSource"; -import { useKeyboardNavigation } from "./useKeyboardNavigation"; +import { + GroupToggleHandler, + useKeyboardNavigation, +} from "./useKeyboardNavigation"; import { useRowClassNameGenerators } from "./useRowClassNameGenerators"; import { useSelection } from "./useSelection"; import { useTableAndColumnSettings } from "./useTableAndColumnSettings"; @@ -122,6 +125,7 @@ export interface TableHookProps | "onRowClick" | "renderBufferSize" | "scrollingApiRef" + | "selectionBookendWidth" | "showColumnHeaders" | "showPaginationControls" > { @@ -170,6 +174,7 @@ export const useTable = ({ renderBufferSize = 0, rowHeight = 20, scrollingApiRef, + selectionBookendWidth, selectionModel, showColumnHeaders, showPaginationControls, @@ -223,21 +228,18 @@ export const useTable = ({ tableConfig, } = useTableModel(config, dataSource, selectionModel, availableWidth); + // this is realy here to capture changes to available Width - typically when we get + // rowcount so add allowance for vertical scrollbar, reducing available width + // including dataSOurce is causing us to do unnecessary work in useTableModel + // split this iniot multip effects useLayoutEffectSkipFirst(() => { dispatchTableModelAction({ availableWidth, type: "init", - // tableConfig: config, tableConfig: tableConfigRef.current, dataSource, }); - }, [ - availableWidth, - config, - dataSource, - dispatchTableModelAction, - verticalScrollbarWidth, - ]); + }, [availableWidth, config, dataSource, dispatchTableModelAction]); const applyTableConfigChange = useCallback( (config: TableConfig) => { @@ -283,6 +285,7 @@ export const useTable = ({ headerHeight: headerState.height, rowCount, rowHeight, + selectionEndSize: selectionBookendWidth, size: size, showPaginationControls, }); @@ -344,6 +347,7 @@ export const useTable = ({ direction: "home", }); } + console.log(`useTable dispatch tableConfig`); dispatchTableModelAction({ type: "tableConfig", ...config, @@ -515,6 +519,7 @@ export const useTable = ({ if (row[IS_EXPANDED]) { dataSource.closeTreeNode(key, true); if (isJson) { + // TODO could this be instigated by an event emitted by the JsonDataSOurce ? "hide-columns" ? const idx = columns.indexOf(column); const rows = dataSource.getRowsAtDepth?.(idx + 1); if (rows && !rows.some((row) => row[IS_EXPANDED] || row[IS_LEAF])) { @@ -545,6 +550,18 @@ export const useTable = ({ [columnMap, columns, dataSource, dispatchTableModelAction], ); + // TODO combine with aboue + const handleToggleGroup = useCallback( + (treeNodeOperation, rowIdx) => { + if (treeNodeOperation === "expand") { + dataSource.openTreeNode(rowIdx); + } else { + dataSource.closeTreeNode(rowIdx); + } + }, + [dataSource], + ); + const { focusCell, focusCellPlaceholderKeyDown, @@ -563,7 +580,7 @@ export const useTable = ({ const { highlightedIndexRef, - navigate, + navigateCell: navigate, onFocus: navigationFocus, onKeyDown: navigationKeyDown, ...containerProps @@ -579,6 +596,7 @@ export const useTable = ({ requestScroll, rowCount, onHighlight, + onToggleGroup: handleToggleGroup, viewportRange: range, viewportRowCount: viewportMeasurements.rowCount, }); @@ -607,6 +625,7 @@ export const useTable = ({ data, dataSource, getSelectedRows, + headerCount: headerState.count, }); const onMoveGroupColumn = useCallback( @@ -621,7 +640,7 @@ export const useTable = ({ if (isGroupColumn(column)) { dataSource.groupBy = []; } else { - if (dataSource && dataSource.groupBy.includes(column.name)) { + if (dataSource && dataSource.groupBy?.includes(column.name)) { dataSource.groupBy = dataSource.groupBy.filter( (columnName) => columnName !== column.name, ); @@ -674,7 +693,6 @@ export const useTable = ({ allowCellBlockSelection, columnCount, containerRef, - focusCell, onSelectCellBlock: handleSelectCellBlock, rowCount, }); @@ -751,7 +769,8 @@ export const useTable = ({ const handleDragStartRow = useCallback( (dragDropState) => { const { initialDragElement } = dragDropState; - const rowIndex = getIndexFromRowElement(initialDragElement); + const rowIndex = + getAriaRowIndex(initialDragElement) - headerState.count - 1; const row = dataRef.current.find((row) => row[0] === rowIndex); if (row) { dragDropState.setPayload(row); @@ -760,7 +779,7 @@ export const useTable = ({ } onDragStart?.(dragDropState); }, - [dataRef, onDragStart], + [dataRef, headerState.count, onDragStart], ); const onHeaderHeightMeasured = useCallback( diff --git a/vuu-ui/packages/vuu-table/src/useTableContextMenu.ts b/vuu-ui/packages/vuu-table/src/useTableContextMenu.ts index 26e19121c..cf28dee14 100644 --- a/vuu-ui/packages/vuu-table/src/useTableContextMenu.ts +++ b/vuu-ui/packages/vuu-table/src/useTableContextMenu.ts @@ -2,7 +2,7 @@ import { DataSource, DataSourceRow } from "@finos/vuu-data-types"; import { RuntimeColumnDescriptor } from "@finos/vuu-table-types"; import { useContextMenu as usePopupContextMenu } from "@finos/vuu-popups"; import { buildColumnMap } from "@finos/vuu-utils"; -import { getIndexFromRowElement } from "./table-dom-utils"; +import { getAriaColIndex, getAriaRowIndex } from "./table-dom-utils"; import { MouseEvent, useCallback } from "react"; export interface TableContextMenuHookProps { @@ -10,6 +10,8 @@ export interface TableContextMenuHookProps { data: DataSourceRow[]; dataSource: DataSource; getSelectedRows: () => DataSourceRow[]; + // TODO can we eliminate this it is only needed to convert aria row index to actual row index + headerCount: number; } const NO_ROWS = [] as const; @@ -19,19 +21,20 @@ export const useTableContextMenu = ({ data, dataSource, getSelectedRows, + headerCount, }: TableContextMenuHookProps) => { const [showContextMenu] = usePopupContextMenu(); const onContextMenu = useCallback( (evt: MouseEvent) => { const target = evt.target as HTMLElement; - const cellEl = target?.closest("div[role='cell']"); - const rowEl = target?.closest("div[role='row']") as HTMLElement; + const cellEl = target?.closest("div[role='cell']"); + const rowEl = target?.closest("div[role='row']"); if (cellEl && rowEl) { const { selectedRowsCount } = dataSource; const columnMap = buildColumnMap(columns); - const rowIndex = getIndexFromRowElement(rowEl); - const cellIndex = Array.from(rowEl.childNodes).indexOf(cellEl); + const rowIndex = getAriaRowIndex(rowEl) - headerCount - 1; + const cellIndex = getAriaColIndex(cellEl) - 1; const row = data.find(([idx]) => idx === rowIndex); const columnName = columns[cellIndex]; // TODO does it really make sense to collect selected rows ? @@ -46,7 +49,7 @@ export const useTableContextMenu = ({ }); } }, - [columns, data, dataSource, getSelectedRows, showContextMenu], + [columns, data, dataSource, getSelectedRows, headerCount, showContextMenu], ); return onContextMenu; diff --git a/vuu-ui/packages/vuu-table/src/useTableModel.ts b/vuu-ui/packages/vuu-table/src/useTableModel.ts index 120f5470c..4fda17c4e 100644 --- a/vuu-ui/packages/vuu-table/src/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/useTableModel.ts @@ -38,6 +38,8 @@ import { DataSource, DataSourceConfig, TableSchema, + WithBaseFilter, + WithFullConfig, } from "@finos/vuu-data-types"; import { VuuColumnDataType, VuuTable } from "@finos/vuu-protocol-types"; import { Reducer, useReducer } from "react"; @@ -161,7 +163,8 @@ export interface ColumnActionUpdateProp { width?: ColumnDescriptor["width"]; } -export interface ColumnActionTableConfig extends DataSourceConfig { +export interface ColumnActionTableConfig + extends WithBaseFilter { confirmed?: boolean; type: "tableConfig"; } @@ -306,6 +309,7 @@ function init({ tableAttributes, tableSchema, ); + const runtimeColumns = columns .filter(subscribedOnly(dataSourceConfig?.columns)) .map(toRuntimeColumnDescriptor); @@ -568,22 +572,37 @@ function updateColumnProp( function updateTableConfig( state: InternalTableModel, - { confirmed, filterSpec: filter, groupBy, sort }: ColumnActionTableConfig, + { + confirmed, + filterSpec, + groupBy, + sort, + }: Omit, ) { - const hasGroupBy = groupBy !== undefined; - const hasFilter = typeof filter?.filter === "string"; - const hasSort = sort && sort.sortDefs.length > 0; - let result = state; - if (hasGroupBy) { + if (groupBy.length > 0) { + const groupedColumns = applyGroupByToColumns( + result.columns, + groupBy, + confirmed, + ); + const { availableWidth, columnLayout = "static" } = state; + const columns = applyWidthToColumns(groupedColumns, { + availableWidth, + columnLayout, + }); + + console.log(`useTableModel.updateTableConfig applyGroupBy`, { + groupBy, + }); result = { ...state, - columns: applyGroupByToColumns(result.columns, groupBy, confirmed), + columns, }; } - if (hasSort) { + if (sort.sortDefs.length > 0) { result = { ...state, columns: applySortToColumns(result.columns, sort), @@ -595,10 +614,10 @@ function updateTableConfig( }; } - if (hasFilter) { + if (filterSpec.filter.length > 0) { result = { ...state, - columns: applyFilterToColumns(result.columns, filter), + columns: applyFilterToColumns(result.columns, filterSpec), }; } else if (result.columns.some(isFilteredColumn)) { result = { diff --git a/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.css b/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.css new file mode 100644 index 000000000..bd3e07db0 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.css @@ -0,0 +1,37 @@ +.vuuToggleIconButton { + &.saltButton { + &:active { + --saltButton-background-active: transparent; + } + &:hover { + --saltButton-background-hover: transparent; + } + .vuuIcon { + --vuu-icon-height: 18px; + --vuu-icon-left: -3px; + --vuu-icon-width: 18px; + &:after { + transition: transform 0.1s linear; + } + &[data-icon="triangle-down"]:after { + --vuu-icon-left: -1px; + --vuu-icon-top: -3px; + } + } + border: none; + border-radius: 0; + height: 18px; + left: 0; + min-width: 16px; + padding: 0; + width: 18px; + } +} + +.vuuTableGroupCell:hover { + .vuuToggleIconButton { + .vuuIcon:after { + --vuu-icon-color: var(--salt-palette-interact-cta-background-hover); + } + } +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.tsx b/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.tsx new file mode 100644 index 000000000..09274dc50 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.tsx @@ -0,0 +1,38 @@ +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import cx from "clsx"; +import { IconButton, IconButtonProps } from "./IconButton"; + +import toggleIconCss from "./ToggleIconButton.css"; + +const classBase = "vuuToggleIconButton"; + +export interface ToggleIconButtonProps extends Omit { + isExpanded: boolean; +} + +export const ToggleIconButton = ({ + className, + isExpanded, + size = 7, + variant = "secondary", + ...props +}: ToggleIconButtonProps) => { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "vuu-toggle-icon-button", + css: toggleIconCss, + window: targetWindow, + }); + + const icon = isExpanded ? "triangle-down" : "triangle-right"; + return ( + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/icon-button/index.ts b/vuu-ui/packages/vuu-ui-controls/src/icon-button/index.ts index 30ad13512..107af1813 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/icon-button/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/icon-button/index.ts @@ -1,2 +1,3 @@ export * from "./Icon"; export * from "./IconButton"; +export * from "./ToggleIconButton"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx b/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx index d0833c0d6..79d0d49d5 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx @@ -11,7 +11,6 @@ import { } from "react"; import { closestListItemIndex } from "./list-dom-utils"; import { isExpanded } from "./treeTypeUtils"; -import type { NormalisedTreeSourceNode, TreeSourceNode } from "./treeTypes"; import { useItemsWithIds } from "./use-items-with-ids"; import { GroupSelection, @@ -23,6 +22,7 @@ import { useViewportTracking } from "./use-viewport-tracking"; import { useTree } from "./useTree"; import treeCss from "./Tree.css"; +import { NormalisedTreeSourceNode, TreeSourceNode } from "@finos/vuu-utils"; const classBase = "vuuTree"; @@ -66,7 +66,7 @@ export const Tree = forwardRef(function Tree( source, ...htmlAttributes }: TreeProps, - forwardedRef: ForwardedRef + forwardedRef: ForwardedRef, ) { const targetWindow = useWindow(); useComponentCssInjection({ @@ -80,7 +80,7 @@ export const Tree = forwardRef(function Tree( // returns the full source data const [, sourceWithIds, sourceItemById] = useItemsWithIds(source, id, { revealSelected: revealSelected - ? selectedProp ?? defaultSelected ?? false + ? (selectedProp ?? defaultSelected ?? false) : undefined, }); @@ -138,7 +138,7 @@ export const Tree = forwardRef(function Tree( function addLeafNode( list: JSX.Element[], item: NormalisedTreeSourceNode, - idx: Indexer + idx: Indexer, ) { list.push( ) : null} {item.label} - + , ); idx.value += 1; } @@ -159,7 +159,7 @@ export const Tree = forwardRef(function Tree( child: NormalisedTreeSourceNode, idx: Indexer, id: string, - title: string + title: string, ) { const { value: i } = idx; idx.value += 1; @@ -198,13 +198,13 @@ export const Tree = forwardRef(function Tree(
    {isExpanded(child) ? renderSourceContent(child.childNodes, idx) : ""}
- + , ); } function renderSourceContent( items: NormalisedTreeSourceNode[], - idx = { value: 0 } + idx = { value: 0 }, ) { if (items?.length > 0) { const listItems: JSX.Element[] = []; @@ -240,7 +240,7 @@ const getListItemProps = ( highlightedIdx: number, selected: string[], focusVisible: number, - className?: string + className?: string, ) => ({ id: item.id, key: item.id, diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/hierarchical-data-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/hierarchical-data-utils.ts index 734c54d02..073bb3ad3 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/hierarchical-data-utils.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/hierarchical-data-utils.ts @@ -1,4 +1,4 @@ -import { NonLeafNode, NormalisedTreeSourceNode } from "./treeTypes"; +import { NonLeafNode, NormalisedTreeSourceNode } from "@finos/vuu-utils"; export const getNodeParentPath = ({ id }: NormalisedTreeSourceNode) => { let pos = id.lastIndexOf("-"); @@ -30,7 +30,7 @@ const PATH_SEPARATORS = new Set([".", "/"]); const isDescendantOf = ( node: NormalisedTreeSourceNode, - targetPath: string + targetPath: string, ): node is NonLeafNode => { if (!targetPath.startsWith(node.id)) { return false; @@ -41,7 +41,7 @@ const isDescendantOf = ( export const getNodeById = ( nodes: NormalisedTreeSourceNode[], - id: string + id: string, ): NormalisedTreeSourceNode | undefined => { for (const node of nodes) { if (node.id === id) { @@ -54,7 +54,7 @@ export const getNodeById = ( export const getIndexOfNode = ( treeNodes: NormalisedTreeSourceNode[], - node: NormalisedTreeSourceNode + node: NormalisedTreeSourceNode, ) => { const id = typeof node === "string" ? node : node.id; for (let i = 0; i < treeNodes.length; i++) { @@ -67,7 +67,7 @@ export const getIndexOfNode = ( export const replaceNode = ( nodes: NormalisedTreeSourceNode[], id: string, - props: Partial + props: Partial, ): NormalisedTreeSourceNode[] => { let childNodes; const newNodes = nodes.map((node) => { diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts index d1e7871d6..057c4493b 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts @@ -1,4 +1,3 @@ export * from "./Tree"; export * from "./Tree"; -export * from "./treeTypes"; export * from "./use-items-with-ids"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts index 00eac1b9d..5d9ff277c 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts @@ -1,5 +1,5 @@ -import type { NonLeafNode, NormalisedTreeSourceNode } from "./treeTypes"; +import type { NonLeafNode, NormalisedTreeSourceNode } from "@finos/vuu-utils"; export const isExpanded = ( - node: NormalisedTreeSourceNode + node: NormalisedTreeSourceNode, ): node is NonLeafNode => node.expanded === true; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-collapsible-groups.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-collapsible-groups.ts index eed7ec973..d986b0735 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-collapsible-groups.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-collapsible-groups.ts @@ -2,7 +2,7 @@ import { KeyboardEvent, MouseEvent, useCallback, useRef } from "react"; import { closestListItem } from "./list-dom-utils"; import { ArrowLeft, ArrowRight, Enter } from "./key-code"; import { getNodeById, replaceNode } from "./hierarchical-data-utils"; -import { NormalisedTreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; const NO_HANDLERS: CollapsibleHookResult["listHandlers"] = {}; const isToggleElement = (element: HTMLElement) => @@ -39,18 +39,18 @@ export const useCollapsibleGroups = ({ (value) => { setVisibleData((stateSource.current = value)); }, - [setVisibleData] + [setVisibleData], ); const expandNode = useCallback( (nodeList: NormalisedTreeSourceNode[], { id }: NormalisedTreeSourceNode) => replaceNode(nodeList, id, { expanded: true }), - [] + [], ); const collapseNode = useCallback( (nodeList, { id }) => replaceNode(nodeList, id, { expanded: false }), - [] + [], ); const handleKeyDown = useCallback( @@ -75,7 +75,7 @@ export const useCollapsibleGroups = ({ } } }, - [collapseNode, expandNode, highlightedIdx, treeNodes, setSource] + [collapseNode, expandNode, highlightedIdx, treeNodes, setSource], ); /** @@ -102,7 +102,7 @@ export const useCollapsibleGroups = ({ } } }, - [collapseNode, expandNode, setSource, source] + [collapseNode, expandNode, setSource, source], ); const listItemHandlers = { diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-hierarchical-data.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-hierarchical-data.ts index 494c4b420..f89de1523 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-hierarchical-data.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-hierarchical-data.ts @@ -1,12 +1,12 @@ import { useRef, useState } from "react"; import { isGroupNode, isHeader } from "./hierarchical-data-utils"; import { isExpanded } from "./treeTypeUtils"; -import { NormalisedTreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; const populateIndices = ( nodes: NormalisedTreeSourceNode[], results: NormalisedTreeSourceNode[] = [], - idx = { value: 0 } + idx = { value: 0 }, ) => { let skipToNextHeader = false; for (const node of nodes) { diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-items-with-ids.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-items-with-ids.ts index e34025862..8edff0848 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-items-with-ids.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-items-with-ids.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { NormalisedTreeSourceNode, TreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode, TreeSourceNode } from "@finos/vuu-utils"; const PathSeparators = new Set(["/", "-", "."]); // TODO where do we define or identify separators @@ -15,7 +15,7 @@ type Indexer = { type SourceItemById = ( id: string, - target?: NormalisedTreeSourceNode[] + target?: NormalisedTreeSourceNode[], ) => TreeSourceNode | undefined; export const useItemsWithIds = ( @@ -25,12 +25,12 @@ export const useItemsWithIds = ( collapsibleHeaders = undefined, defaultExpanded = false, revealSelected = false, - } = {} + } = {}, ): [number, NormalisedTreeSourceNode[], SourceItemById] => { const countChildItems = ( item: TreeSourceNode, items: TreeSourceNode[], - idx: number + idx: number, ) => { if (item.childNodes) { return item.childNodes.length; @@ -54,7 +54,7 @@ export const useItemsWithIds = ( } return defaultExpanded; }, - [defaultExpanded, revealSelected] + [defaultExpanded, revealSelected], ); const normalizeItems = useCallback( @@ -64,7 +64,7 @@ export const useItemsWithIds = ( level = 1, path = "", results: NormalisedTreeSourceNode[] = [], - flattenedSource: TreeSourceNode[] = [] + flattenedSource: TreeSourceNode[] = [], ): [number, NormalisedTreeSourceNode[], TreeSourceNode[]] => { let count = 0; // TODO get rid of the Proxy @@ -107,7 +107,7 @@ export const useItemsWithIds = ( level + 1, childPath, [], - flattenedSource + flattenedSource, ); normalisedItem.childNodes = children; if (expanded === true || isNonCollapsibleGroupNode) { @@ -117,7 +117,7 @@ export const useItemsWithIds = ( }); return [count, results, flattenedSource]; }, - [collapsibleHeaders, idRoot, isExpanded] + [collapsibleHeaders, idRoot, isExpanded], ); const [count, sourceWithIds, flattenedSource] = useMemo< @@ -129,7 +129,7 @@ export const useItemsWithIds = ( const sourceItemById = useCallback( (id, target = sourceWithIds): TreeSourceNode | undefined => { const sourceWithId = target.find( - (i) => i.id === id || (i?.childNodes?.length && isParentPath(i.id, id)) + (i) => i.id === id || (i?.childNodes?.length && isParentPath(i.id, id)), ); if (sourceWithId?.id === id) { return flattenedSource[sourceWithId.index]; @@ -137,7 +137,7 @@ export const useItemsWithIds = ( return sourceItemById(id, sourceWithId.childNodes); } }, - [flattenedSource, sourceWithIds] + [flattenedSource, sourceWithIds], ); return [count, sourceWithIds, sourceItemById]; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-keyboard-navigation.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-keyboard-navigation.ts index 0dbbc958e..ada0e1e97 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-keyboard-navigation.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-keyboard-navigation.ts @@ -2,7 +2,7 @@ import { KeyboardEvent, useCallback, useMemo, useRef } from "react"; import { getIndexOfNode, getNodeById } from "./hierarchical-data-utils"; import { useControlled } from "@salt-ds/core"; import { ArrowDown, ArrowLeft, ArrowUp, isNavigationKey } from "./key-code"; -import { NormalisedTreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; function nextItemIdx(count: number, key: string, idx: number) { if (key === ArrowUp || key === ArrowLeft) { @@ -50,7 +50,7 @@ export const useKeyboardNavigation = ({ bwd: ArrowUp, fwd: ArrowDown, }), - [] + [], ); const [highlightedIdx, setHighlightedIdx, isControlledHighlighting] = @@ -65,7 +65,7 @@ export const useKeyboardNavigation = ({ onHighlight?.(idx); setHighlightedIdx(idx); }, - [onHighlight, setHighlightedIdx] + [onHighlight, setHighlightedIdx], ); const nextFocusableItemIdx = useCallback( @@ -81,7 +81,7 @@ export const useKeyboardNavigation = ({ } return nextIdx; }, - [ArrowBwd, ArrowFwd, treeNodes] + [ArrowBwd, ArrowFwd, treeNodes], ); // does this belong here or should it be a method passed in? @@ -117,7 +117,7 @@ export const useKeyboardNavigation = ({ nextFocusableItemIdx, onKeyboardNavigation, setHighlightedIndex, - ] + ], ); const handleKeyDown = useCallback( @@ -129,7 +129,7 @@ export const useKeyboardNavigation = ({ navigateChildItems(e); } }, - [treeNodes, navigateChildItems] + [treeNodes, navigateChildItems], ); const listProps = useMemo( @@ -157,7 +157,7 @@ export const useKeyboardNavigation = ({ setHighlightedIndex(-1); }, }), - [handleFocus, handleKeyDown, setHighlightedIndex] + [handleFocus, handleKeyDown, setHighlightedIndex], ); return { diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-selection.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-selection.ts index fac9faf91..a615fa3a7 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-selection.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-selection.ts @@ -6,7 +6,7 @@ import { useRef, } from "react"; import { useControlled } from "@salt-ds/core"; -import { NormalisedTreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; export type TreeSelection = | "none" @@ -31,7 +31,7 @@ const isCollapsibleItem = (item: NormalisedTreeSourceNode) => export type TreeNodeSelectionHandler = ( evt: SyntheticEvent, - selected: string[] + selected: string[], ) => void; export const groupSelectionEnabled = (groupSelection: GroupSelection) => @@ -75,7 +75,7 @@ export const useSelection = ({ const isSelectionEvent = useCallback( (evt) => selectionKeys.includes(evt.key), - [selectionKeys] + [selectionKeys], ); const [selected, setSelected] = useControlled({ @@ -93,7 +93,7 @@ export const useSelection = ({ idx: number, id: string, rangeSelect: boolean, - preserveExistingSelection = false + preserveExistingSelection = false, ) => { const { current: active } = lastActive; const isSelected = selected?.includes(id); @@ -139,7 +139,7 @@ export const useSelection = ({ selected, setSelected, singleSelect, - ] + ], ); const handleKeyDown = useCallback( @@ -152,7 +152,7 @@ export const useSelection = ({ highlightedIdx, item.id, false, - evt.ctrlKey || evt.metaKey + evt.ctrlKey || evt.metaKey, ); if (extendedSelect) { lastActive.current = highlightedIdx; @@ -165,7 +165,7 @@ export const useSelection = ({ treeNodes, isSelectionEvent, selectItemAtIndex, - ] + ], ); const handleKeyboardNavigation = useCallback( @@ -175,7 +175,7 @@ export const useSelection = ({ selectItemAtIndex(evt, currentIndex, item.id, true); } }, - [extendedSelect, treeNodes, selectItemAtIndex] + [extendedSelect, treeNodes, selectItemAtIndex], ); const listHandlers = @@ -198,7 +198,7 @@ export const useSelection = ({ highlightedIdx, item.id, evt.shiftKey, - evt.ctrlKey || evt.metaKey + evt.ctrlKey || evt.metaKey, ); if (extendedSelect) { lastActive.current = highlightedIdx; @@ -206,7 +206,7 @@ export const useSelection = ({ } } }, - [extendedSelect, highlightedIdx, treeNodes, selectItemAtIndex] + [extendedSelect, highlightedIdx, treeNodes, selectItemAtIndex], ); const listItemHandlers = diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-tree-keyboard-navigation.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-tree-keyboard-navigation.ts index efdfe6304..28bf36222 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-tree-keyboard-navigation.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/use-tree-keyboard-navigation.ts @@ -5,7 +5,7 @@ import { getNodeParentPath, getIndexOfNode, } from "./hierarchical-data-utils"; -import { NormalisedTreeSourceNode } from "./treeTypes"; +import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; export interface TreeKeyboardNavigationHookProps { highlightedIdx: number; @@ -38,7 +38,7 @@ export const useTreeKeyboardNavigation = ({ } } }, - [highlightedIdx, hiliteItemAtIndex, indexPositions, source] + [highlightedIdx, hiliteItemAtIndex, indexPositions, source], ); const listHandlers = { diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts index 82ac6e442..a79238d58 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts @@ -1,4 +1,7 @@ +import type { NormalisedTreeSourceNode } from "@finos/vuu-utils"; import { KeyboardEvent, useCallback, useRef } from "react"; +import { useCollapsibleGroups } from "./use-collapsible-groups"; +import { useHierarchicalData } from "./use-hierarchical-data"; import { useKeyboardNavigation } from "./use-keyboard-navigation"; import { GroupSelection, @@ -6,10 +9,7 @@ import { TreeSelection, useSelection, } from "./use-selection"; -import { useHierarchicalData } from "./use-hierarchical-data"; -import { useCollapsibleGroups } from "./use-collapsible-groups"; import { useTreeKeyboardNavigation } from "./use-tree-keyboard-navigation"; -import type { NormalisedTreeSourceNode } from "./treeTypes"; const EMPTY_ARRAY: string[] = []; @@ -76,7 +76,7 @@ export const useTree = ({ selectionHook.listItemHandlers?.onClick?.(evt); } }, - [collapsibleHook, selectionHook] + [collapsibleHook, selectionHook], ); const handleKeyDown = useCallback( @@ -97,7 +97,7 @@ export const useTree = ({ keyboardHook.listProps, selectionHook.listHandlers, treeNavigationHook.listHandlers, - ] + ], ); const getActiveDescendant = () => diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index ee1139662..314ed6634 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -331,7 +331,9 @@ export function extractGroupColumn( if (groupBy && groupBy.length > 0) { const flattenedColumns = flattenColumnGroup(columns); // Note: groupedColumns will be in column order, not groupBy order - const [groupedColumns, rest] = flattenedColumns.reduce( + const [groupedColumns, rest] = flattenedColumns.reduce< + [RuntimeColumnDescriptor[], RuntimeColumnDescriptor[]] + >( (result, column, i) => { const [g, r] = result; if (groupBy.includes(column.name)) { @@ -345,7 +347,7 @@ export function extractGroupColumn( return result; }, - [[], []] as [RuntimeColumnDescriptor[], RuntimeColumnDescriptor[]], + [[], []], ); if (groupedColumns.length !== groupBy.length) { throw Error( @@ -367,15 +369,16 @@ export function extractGroupColumn( }); const groupCol = { - name: "group-col", + columns: groupCols, heading: ["group-col"], + index: 1, isGroup: true, - columns: groupCols, groupConfirmed: confirmed, + name: "group-col", width: groupCols.map((c) => c.width).reduce((a, b) => a + b) + 100, } as GroupColumnDescriptor; - return [groupCol, rest]; + return [groupCol, rest.map((col, i) => ({ ...col, index: i + 2 }))]; } return [null, flattenColumnGroup(columns)]; } @@ -385,7 +388,7 @@ export const isGroupColumn = ( ): column is GroupColumnDescriptor => column.isGroup === true; export const isJsonAttribute = (value: unknown) => - typeof value === "string" && value.endsWith("+"); + typeof value === "string" && (value.endsWith("{") || value.endsWith("[")); export const isJsonGroup = ( column: RuntimeColumnDescriptor, @@ -739,24 +742,41 @@ export const visibleColumnAtIndex = ( }; const { DEPTH, IS_LEAF } = metadataKeys; -// Get the value for a specific columns within a grouped column -export const getGroupValueAndOffset = ( + +export const getGroupIcon = ( + columns: RuntimeColumnDescriptor[], + row: DataSourceRow, +): string | undefined => { + const { [DEPTH]: depth, [IS_LEAF]: isLeaf } = row; + // Depth can be greater tha group columns when we have just removed a column from groupby + // but new data has not yet been received. + if (isLeaf || depth > columns.length) { + return undefined; + } else if (depth === 0) { + return undefined; + } else { + const { getIcon } = columns[depth - 1]; + return getIcon?.(row); + } +}; + +export const getGroupValue = ( columns: RuntimeColumnDescriptor[], row: DataSourceRow, columnMap: ColumnMap, -): [unknown, number] => { +): unknown => { const { [DEPTH]: depth, [IS_LEAF]: isLeaf } = row; // Depth can be greater tha group columns when we have just removed a column from groupby // but new data has not yet been received. if (isLeaf || depth > columns.length) { - return [null, depth === null ? 0 : Math.max(0, depth - 1)]; + return null; } else if (depth === 0) { - return ["$root", 0]; + return "$root"; } else { // offset 1 for now to allow for $root const { name, valueFormatter } = columns[depth - 1]; const value = valueFormatter(row[columnMap[name]]); - return [value, depth - 1]; + return value; } }; diff --git a/vuu-ui/packages/vuu-utils/src/datasource/BaseDataSource.ts b/vuu-ui/packages/vuu-utils/src/datasource/BaseDataSource.ts index 765e39358..6410f5527 100644 --- a/vuu-ui/packages/vuu-utils/src/datasource/BaseDataSource.ts +++ b/vuu-ui/packages/vuu-utils/src/datasource/BaseDataSource.ts @@ -49,7 +49,7 @@ export abstract class BaseDataSource sort, title, viewport, - }: DataSourceConstructorProps) { + }: Omit) { super(); this._config = { ...this._config, @@ -158,7 +158,7 @@ export abstract class BaseDataSource return this._config; } - set config(config: WithBaseFilter) { + set config(config: WithBaseFilter) { const configChanges = this.applyConfig(config); if (configChanges) { this.emit("config", this._config, undefined, configChanges); @@ -192,6 +192,15 @@ export abstract class BaseDataSource this.emit("config", this._config); } + get title() { + return this._title ?? ""; + } + + set title(title: string) { + this._title = title; + this.emit("title-changed", this.viewport ?? "", title); + } + // Public while we use this from useSessionDataSource public applyConfig( config: WithBaseFilter, diff --git a/vuu-ui/packages/vuu-utils/src/index.ts b/vuu-ui/packages/vuu-utils/src/index.ts index 6eb827803..582945c35 100644 --- a/vuu-ui/packages/vuu-utils/src/index.ts +++ b/vuu-ui/packages/vuu-utils/src/index.ts @@ -49,6 +49,8 @@ export * from "./sort-utils"; export * from "./table-schema-utils"; export * from "./text-utils"; export * from "./typeahead-utils"; +export * from "./tree-types"; +export * from "./tree-utils"; export * from "./ThemeProvider"; export * from "./ts-utils"; export * from "./url-utils"; diff --git a/vuu-ui/packages/vuu-utils/src/json-utils.ts b/vuu-ui/packages/vuu-utils/src/json-utils.ts index 643d38775..98310f9b7 100644 --- a/vuu-ui/packages/vuu-utils/src/json-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/json-utils.ts @@ -19,7 +19,7 @@ type Index = { value: number }; type CellValue = { attribute: string; attributeValue: JsonData | VuuRowDataItemType | null; - type: "json" | "number" | "string" | "boolean"; + isLeaf: boolean; }; const isJsonData = (value: unknown): value is JsonData => @@ -28,30 +28,34 @@ const isJsonData = (value: unknown): value is JsonData => const vuuRowDataItemTypes = ["boolean", "number", "string"]; const isVuuRowDataItem = (value: unknown): value is VuuRowDataItemType => vuuRowDataItemTypes.includes(typeof value); -const typeofVuuDataItem = (value: VuuRowDataItemType) => - typeof value === "boolean" - ? "boolean" - : typeof value === "number" - ? "number" - : "string"; const getCellValue = ( attribute: string, attributeValue: unknown, ): CellValue => { - if (isJsonData(attributeValue)) { - return { attribute: `${attribute}+`, attributeValue: "", type: "json" }; + if (Array.isArray(attributeValue)) { + return { + attribute: `${attribute}[`, + attributeValue: "", + isLeaf: false, + }; + } else if (isJsonData(attributeValue)) { + return { + attribute: `${attribute}{`, + attributeValue: "", + isLeaf: false, + }; } else if (attributeValue === undefined) { return { attribute, attributeValue: "undefined", - type: "string", + isLeaf: true, }; } else if (isVuuRowDataItem(attributeValue)) { return { attribute, attributeValue, - type: typeofVuuDataItem(attributeValue), + isLeaf: true, }; } else { throw Error(`unsupported type ${typeof attributeValue} in JSON`); @@ -72,11 +76,13 @@ export const jsonToDataSourceRows = ( cols.push( { - name: "col 1", + className: "vuuJsonCell", + name: "Level 1", type: jsonColumnType, }, { - name: "col 2", + className: "vuuJsonCell", + name: "Level 2", type: jsonColumnType, }, ); @@ -99,7 +105,8 @@ const addChildValues = ( let rowCount = 0; if (depth === cols.length - 1) { cols.push({ - name: `col ${cols.length + 1}`, + className: "vuuJsonCell", + name: `Level ${cols.length + 1}`, hidden: true, type: jsonColumnType, }); @@ -107,8 +114,7 @@ const addChildValues = ( const columnEntries = Object.entries(json); for (let i = 0; i < columnEntries.length; i++, index.value += 1) { const [key, value] = columnEntries[i]; - const { attribute, attributeValue, type } = getCellValue(key, value); - const isLeaf = type !== "json"; + const { attribute, attributeValue, isLeaf } = getCellValue(key, value); const blanks = Array(depth).fill(""); const fullKey = `${keyBase}|${key}`; // prettier-ignore diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypes.ts b/vuu-ui/packages/vuu-utils/src/tree-types.ts similarity index 100% rename from vuu-ui/packages/vuu-ui-controls/src/tree/treeTypes.ts rename to vuu-ui/packages/vuu-utils/src/tree-types.ts diff --git a/vuu-ui/packages/vuu-utils/src/tree-utils.ts b/vuu-ui/packages/vuu-utils/src/tree-utils.ts new file mode 100644 index 000000000..79b20245c --- /dev/null +++ b/vuu-ui/packages/vuu-utils/src/tree-utils.ts @@ -0,0 +1,86 @@ +import { TreeSourceNode } from "./tree-types"; +import { ColumnDescriptor } from "@finos/vuu-table-types"; +import { DataSourceRow } from "@finos/vuu-data-types"; +import { metadataKeys } from "./column-utils"; +import { IconProvider } from "@finos/vuu-data-local/src/tree-data-source/IconProvider"; + +const { COUNT } = metadataKeys; + +type Index = { value: number }; + +export const treeToDataSourceRows = ( + treeSourceNodes: TreeSourceNode[], + iconProvider?: IconProvider, +): [ColumnDescriptor[], DataSourceRow[]] => { + const columns: ColumnDescriptor[] = []; + + columns.push( + { + getIcon: iconProvider?.getIcon, + name: "Level 1", + type: "string", + }, + { + getIcon: iconProvider?.getIcon, + name: "Level 2", + type: "string", + }, + ); + + const rows: DataSourceRow[] = []; + + addChildValues(rows, treeSourceNodes, columns, iconProvider); + return [columns, rows]; +}; + +const addChildValues = ( + rows: DataSourceRow[], + treeSourceNodes: TreeSourceNode[], + cols: ColumnDescriptor[], + iconProvider: IconProvider | undefined, + index: Index = { value: 0 }, + keyBase = "$root", + depth = 1, +): [number, number] => { + let leafCount = 0; + let rowCount = 0; + if (depth === cols.length - 1) { + cols.push({ + getIcon: iconProvider?.getIcon, + name: `Level ${cols.length + 1}`, + type: "string", + }); + } + for (let i = 0; i < treeSourceNodes.length; i++, index.value += 1) { + const { childNodes, icon, label } = treeSourceNodes[i]; + const blanks = Array(depth - 1).fill(""); + const fullKey = `${keyBase}|${label}`; + // prettier-ignore + const row = [index.value, index.value, false,false,depth,0,fullKey,0, ...blanks, label ] as DataSourceRow; + if (icon) { + iconProvider?.setIcon(fullKey, icon); + } + rows.push(row); + rowCount += 1; + + if (childNodes && childNodes.length > 0) { + const [nestedLeafCount, nestedRowCount] = addChildValues( + rows, + childNodes, + cols, + iconProvider, + { value: index.value + 1 }, + fullKey, + depth + 1, + ); + row[COUNT] = nestedLeafCount; + leafCount += nestedLeafCount; + rowCount += nestedRowCount; + index.value += nestedRowCount; + } else { + leafCount += 1; + } + } + + return [leafCount, rowCount]; +}; diff --git a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts index 69344abd8..4c3393ea2 100644 --- a/vuu-ui/packages/vuu-utils/test/json-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/json-utils.test.ts @@ -12,8 +12,8 @@ describe("jsonToDataSourceRows", () => { test4: true, }) ).toEqual([[ - {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 2", type: {name: "json", "renderer": {name: "json"}}} + {className: "vuuJsonCell", name: 'Level 1', type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 2", type: {name: "json", "renderer": {name: "json"}}} ], [ [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], @@ -37,16 +37,16 @@ describe("jsonToDataSourceRows", () => { } }) ).toEqual([[ - {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}} + {className: "vuuJsonCell", name: 'Level 1', type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 2", type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}} ], [ [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], - [4, 4, false, false, 0, 2, "$root|test5", 0, "test5+", ""], + [4, 4, false, false, 0, 2, "$root|test5", 0, "test5{", ""], [5, 5, true, false, 1, 0, "$root|test5|test5.1", 0, "", "test5.1", "test 5.1 value"], [6, 6, true, false, 1, 0, "$root|test5|test5.2", 0, "", "test5.2", "test 5.2 value"], ]]); @@ -74,23 +74,23 @@ describe("jsonToDataSourceRows", () => { } }) ).toEqual([[ - {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} + {className: "vuuJsonCell", name: 'Level 1', type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 2", type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} ], [ [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], - [4, 4, false, false, 0, 4, "$root|test5", 0, "test5+", ""], + [4, 4, false, false, 0, 4, "$root|test5", 0, "test5{", ""], [5, 5, true, false, 1, 0, "$root|test5|test5.1", 0, "", "test5.1", "test 5.1 value"], - [6, 6, false, false, 1, 2, "$root|test5|test5.2", 0, "", "test5.2+", ""], + [6, 6, false, false, 1, 2, "$root|test5|test5.2", 0, "", "test5.2{", ""], [7, 7, true, false, 2, 0, "$root|test5|test5.2|test5.2.1", 0, "", "", "test5.2.1", "test 5.2.1 value"], [8, 8, true, false, 2, 0, "$root|test5|test5.2|test5.2.2", 0, "", "", "test5.2.2", "test 5.2.2 value"], [9, 9, true, false, 1, 0, "$root|test5|test5.3", 0, "", "test5.3", "test 5.3 value"], - [10, 10, false, false, 0, 2, "$root|test6", 0, "test6+", ""], + [10, 10, false, false, 0, 2, "$root|test6", 0, "test6{", ""], [11, 11, true, false, 1, 0, "$root|test6|test6.1", 0, "", "test6.1", "test 6.1 value"], [12, 12, true, false, 1, 0, "$root|test6|test6.2", 0, "", "test6.2", "test 6.2 value"], ]]); @@ -110,15 +110,15 @@ describe("jsonToDataSourceRows", () => { } }) ).toEqual([[ - {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: 'Level 1', type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 2", type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, ], [ - [0, 0, false, false, 0, 2, "$root|test5", 0, "test5+", ""], + [0, 0, false, false, 0, 2, "$root|test5", 0, "test5{", ""], [1, 1, true, false, 1, 0, "$root|test5|test5.1", 0, "", "test5.1", "test 5.1 value"], [2, 2, true, false, 1, 0, "$root|test5|test5.2", 0, "", "test5.2", "test 5.2 value"], - [3, 3, false, false, 0, 2, "$root|test6", 0, "test6+", ""], + [3, 3, false, false, 0, 2, "$root|test6", 0, "test6{", ""], [4, 4, true, false, 1, 0, "$root|test6|test6.1", 0, "", "test6.1", "test 6.1 value"], [5, 5, true, false, 1, 0, "$root|test6|test6.2", 0, "", "test6.2", "test 6.2 value"], ]]); @@ -137,10 +137,10 @@ describe("jsonToDataSourceRows", () => { }) ).toEqual([ [ - { name: "col 1", type: { name: "json", renderer: { name: "json" } } }, - { name: "col 2", type: { name: "json", renderer: { name: "json" } } }, + { className: "vuuJsonCell", name: "Level 1", type: { name: "json", renderer: { name: "json" } } }, + { className: "vuuJsonCell", name: "Level 2", type: { name: "json", renderer: { name: "json" } } }, { - name: "col 3", + className: "vuuJsonCell", name: "Level 3", hidden: true, type: { name: "json", renderer: { name: "json" } }, }, @@ -150,7 +150,7 @@ describe("jsonToDataSourceRows", () => { [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], - [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5[", ""], [5, 5, true, false, 1, 0, "$root|test5|0", 0, "", "0", "test5.1"], [6, 6, true, false, 1, 0, "$root|test5|1", 0, "", "1", "test5.2"], [7, 7, true, false, 1, 0, "$root|test5|2", 0, "", "2", "test5.3"], @@ -176,22 +176,22 @@ describe("jsonToDataSourceRows", () => { }) // prettier-ignore ).toEqual([[ - {name: 'col 1', type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 2", type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, - {name: "col 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} + {className: "vuuJsonCell", name: 'Level 1', type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 2", type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 3", hidden: true, type: {name: "json", "renderer": {name: "json"}}}, + {className: "vuuJsonCell", name: "Level 4", hidden: true, type: {name: "json", "renderer": {name: "json"}}} ], [ [0, 0, true, false, 0, 0, "$root|test1", 0, "test1", "value 1"], [1, 1, true, false, 0, 0, "$root|test2", 0, "test2", 12345], [2, 2, true, false, 0, 0, "$root|test3", 0, "test3", 100.01], [3, 3, true, false, 0, 0, "$root|test4", 0, "test4", true], - [4, 4, false, false, 0, 3, "$root|test5", 0, "test5+", ""], - [5, 5, false, false, 1, 1, '$root|test5|0', 0, '', '0+', '' ], + [4, 4, false, false, 0, 3, "$root|test5", 0, "test5[", ""], + [5, 5, false, false, 1, 1, '$root|test5|0', 0, '', '0{', '' ], [6, 6, true, false, 2, 0, '$root|test5|0|test5.1', 0, '', '', 'test5.1', 'test 5.1 value' ], - [7, 7, false, false, 1, 1, '$root|test5|1', 0, '', '1+', '' ], + [7, 7, false, false, 1, 1, '$root|test5|1', 0, '', '1{', '' ], [8,8, true, false, 2, 0, '$root|test5|1|test5.2', 0, '', '', 'test5.2','test 5.2 value'], - [9, 9, false, false, 1, 1, '$root|test5|2', 0, '', '2+', '' ], + [9, 9, false, false, 1, 1, '$root|test5|2', 0, '', '2{', '' ], [10,10, true,false, 2, 0, '$root|test5|2|test5.3', 0, '', '', 'test5.3', 'test 5.2 value'] ] ]); diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts index 034fb06ca..6fb31a953 100644 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts +++ b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts @@ -1,4 +1,4 @@ -import type { TreeSourceNode } from "@finos/vuu-ui-controls"; +import type { TreeSourceNode } from "@finos/vuu-utils"; export const folderData: TreeSourceNode[] = [ // prettier-ignore diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx index 73a817922..e066376fa 100644 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx +++ b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx @@ -1,6 +1,7 @@ -import { Tree, TreeSourceNode, useItemsWithIds } from "@finos/vuu-ui-controls"; +import { Tree, useItemsWithIds } from "@finos/vuu-ui-controls"; import { groupByInitialLetter, usa_states_cities } from "./List/List.data"; import { folderData } from "./Tree.data"; +import { TreeSourceNode } from "@finos/vuu-utils"; let displaySequence = 1; @@ -35,7 +36,7 @@ export const SimpleTree = () => { source={ groupByInitialLetter( usa_states_cities, - "groups-only" + "groups-only", ) as TreeSourceNode[] } /> diff --git a/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx index daa98a266..23ec087a4 100644 --- a/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx @@ -34,7 +34,8 @@ const DataTableTemplate = ({ return { ...configProp, columns: schema.columns, - rowSeparators: false, + columnSeparators: true, + rowSeparators: true, zebraStripes: true, }; }, [configProp, schema]); diff --git a/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx index 033aa5d93..0696c393a 100644 --- a/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx @@ -1,9 +1,27 @@ import { Tree } from "@finos/vuu-ui-controls"; +import { TreeTable } from "@finos/vuu-datatable"; + import showcaseData from "./Tree.data"; +console.log({ showcaseData }); + let displaySequence = 1; export const ShowcaseTree = () => { - return ; + return ( +
+ + {/*
+ +
*/} +
+ +
+
+ ); }; ShowcaseTree.displaySequence = displaySequence++; diff --git a/vuu-ui/tools/vuu-showcase/src/App.tsx b/vuu-ui/tools/vuu-showcase/src/App.tsx index aad1e9e17..e046982fc 100644 --- a/vuu-ui/tools/vuu-showcase/src/App.tsx +++ b/vuu-ui/tools/vuu-showcase/src/App.tsx @@ -1,12 +1,12 @@ import { Flexbox } from "@finos/vuu-layout"; -import { Tree, TreeSourceNode } from "@finos/vuu-ui-controls"; -import { Density, ThemeMode } from "@finos/vuu-utils"; +import { Tree } from "@finos/vuu-ui-controls"; +import type { Density, ThemeMode, TreeSourceNode } from "@finos/vuu-utils"; import { Button, SaltProvider, Text, ToggleButton, - ToggleButtonGroup + ToggleButtonGroup, } from "@salt-ds/core"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; @@ -20,7 +20,7 @@ import "./App.css"; const sourceFromImports = ( stories: ExamplesModule, prefix = "", - icon = "folder" + icon = "folder", ): TreeSourceNode[] => Object.entries(stories) .filter(([path]) => path !== "default") @@ -33,14 +33,14 @@ const sourceFromImports = ( return { id, icon: "rings", - label + label, }; } return { id, icon, label, - childNodes: sourceFromImports(stories, `${id}/`, "box") + childNodes: sourceFromImports(stories, `${id}/`, "box"), }; }); export interface AppProps { @@ -55,19 +55,19 @@ const availableThemes: ThemeDescriptor[] = [ { id: "no-theme", label: "No Theme" }, { id: "salt-theme", label: "Salt" }, { id: "vuu-theme", label: "Vuu" }, - { id: "tar-theme", label: "Tar" } + { id: "tar-theme", label: "Tar" }, ]; const availableThemeModes: ThemeModeDescriptor[] = [ { id: "light", label: "Light" }, - { id: "dark", label: "Dark" } + { id: "dark", label: "Dark" }, ]; const availableDensity: DensityDescriptor[] = [ { id: "high", label: "High" }, { id: "medium", label: "Medium" }, { id: "low", label: "Low" }, - { id: "touch", label: "Touch" } + { id: "touch", label: "Touch" }, ]; export const App = ({ stories }: AppProps) => { @@ -91,14 +91,14 @@ export const App = ({ stories }: AppProps) => { const theme = useMemo(() => availableThemes[themeIndex], [themeIndex]); const themeMode = useMemo( () => availableThemeModes[themeModeIndex], - [themeModeIndex] + [themeModeIndex], ); const density = useMemo(() => availableDensity[densityIndex], [densityIndex]); const launchStandaloneWindow = useCallback(() => { window.open( `${location.href}?standalone&theme=${theme.id}#themeMode=${themeMode.id},density=${density.id}`, - "_blank" + "_blank", ); }, [density.id, theme.id, themeMode.id]); @@ -146,7 +146,7 @@ export const App = ({ stories }: AppProps) => {
@@ -191,7 +191,7 @@ export const App = ({ stories }: AppProps) => { className={`ShowcaseContent`} style={{ flex: "1 1 auto", - position: "relative" + position: "relative", }} >