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 d4ff4e60a..8ee204d59 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 @@ -191,6 +191,8 @@ export class ArrayDataSource columns, aggregations, range, + selectedIndexValues, + selectedKeyValues, sort, groupBy, filterSpec, @@ -200,6 +202,11 @@ export class ArrayDataSource this.clientCallback = callback; this.viewport = viewport; this.#status = "subscribed"; + this.selectedRows = + selectedIndexValues ?? + this.convertKeysToIndexValues(selectedKeyValues) ?? + []; + this.#selectedRowsCount = selectionCount(this.selectedRows); this.lastRangeServed = { from: 0, to: 0 }; let config = this._config; @@ -566,7 +573,7 @@ export class ArrayDataSource // TODO take sorting, filtering. grouping into account const colIndex = this.#columnMap[columnName]; const dataColIndex = this.dataMap?.[columnName]; - const dataIndex = this.#data.findIndex((row) => row[KEY] === keyValue); + const dataIndex = this.indexOfRowWithKey(keyValue); if (dataIndex !== -1 && dataColIndex !== undefined) { const dataSourceRow = this.#data[dataIndex]; dataSourceRow[colIndex] = value; @@ -578,6 +585,9 @@ export class ArrayDataSource } }; + private indexOfRowWithKey = (key: string) => + this.#data.findIndex((row) => row[KEY] === key); + protected update = (row: VuuRowDataItemType[], columnName: string) => { // TODO take sorting, filtering. grouping into account const keyValue = row[this.key] as string; @@ -779,15 +789,6 @@ export class ArrayDataSource console.log("remove link"); } - private findRow(rowKey: number) { - const row = this.#data[rowKey]; - if (row) { - return row; - } else { - throw `no row found for key ${rowKey}`; - } - } - applyEdit( rowKey: string, columnName: string, @@ -829,4 +830,17 @@ export class ArrayDataSource } }); } + + private convertKeysToIndexValues(keys?: string[]) { + if (Array.isArray(keys)) { + const indexValues: number[] = []; + keys.forEach((key) => { + const rowIdx = this.indexOfRowWithKey(key); + if (rowIdx !== -1) { + indexValues.push(rowIdx); + } + }); + return indexValues; + } + } } 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 index a9183708f..7fef467ff 100644 --- 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 @@ -93,10 +93,12 @@ export class TreeDataSource extends BaseDataSource { async subscribe( { - viewport = this.viewport ?? uuid(), - columns, aggregations, + columns, range, + revealSelected, + selectedKeyValues, + viewport = this.viewport ?? uuid(), }: SubscribeProps, callback: SubscribeCallback, ) { @@ -120,6 +122,16 @@ export class TreeDataSource extends BaseDataSource { this.viewport = viewport; this.#status = "subscribed"; + this.#selectedRowsCount = selectedKeyValues?.length ?? 0; + + if (selectedKeyValues) { + this.applySelectedKeyValues(selectedKeyValues, revealSelected); + } + + [this.visibleRows, this.visibleRowIndex] = getVisibleRows( + this.#data, + this.expandedRows, + ); this.clientCallback?.({ aggregations: this.#aggregations, @@ -172,22 +184,40 @@ export class TreeDataSource extends BaseDataSource { } 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(); }); } + /** + * used to apply an initial selection. These may not necessarily be + * visible. If revealOnSelect is in force, expand nodes as necessary + * to ensure selected nodes are visible + */ + private applySelectedKeyValues(keys: string[], revealSelected = false) { + keys.forEach((key) => { + const rowIdx = this.indexOfRowWithKey(key); + const row = this.#data[rowIdx]; + row[SELECTED] = 1; + + if (revealSelected && row[DEPTH] !== 1) { + console.log(`we've got a deep one here`); + const keys = key.slice(6).split("|").slice(0, -1); + console.log(JSON.stringify(keys)); + + let path = "$root"; + do { + path = `${path}|${keys.shift()}`; + this.expandedRows.add(path); + } while (keys.length); + } + }); + } + + private indexOfRowWithKey = (key: string) => + this.#data.findIndex((row) => row[KEY] === key); + // Incoming Selection references visibleRow indices select(selected: Selection) { // todo get a diff diff --git a/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts b/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts index d9bfe9b63..7084d6afb 100644 --- a/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts +++ b/vuu-ui/packages/vuu-data-remote/src/VuuDataSource.ts @@ -52,9 +52,9 @@ type RangeRequest = (range: VuuRange) => void; const { info } = logger("VuuDataSource"); -/*----------------------------------------------------------------- - A RemoteDataSource manages a single subscription via the ServerProxy - ----------------------------------------------------------------*/ +/*--------------------------------------------------------------------- + A VuuDataSource manages a single subscription via the ServerProxy + ---------------------------------------------------------------------*/ export class VuuDataSource extends BaseDataSource implements DataSource { private bufferSize: number; private server: ServerAPI | null = null; @@ -90,8 +90,10 @@ export class VuuDataSource extends BaseDataSource implements DataSource { async subscribe(subscribeProps: SubscribeProps, callback: SubscribeCallback) { super.subscribe(subscribeProps, callback); - const { viewport = this.viewport || (this.viewport = uuid()) } = - subscribeProps; + const { + selectedIndexValues, + viewport = this.viewport || (this.viewport = uuid()), + } = subscribeProps; if (this.#status === "disabled" || this.#status === "disabling") { this.enable(callback); @@ -109,22 +111,25 @@ export class VuuDataSource extends BaseDataSource implements DataSource { } this.#status = "subscribing"; + this.#selectedRowsCount = selectionCount(selectedIndexValues); this.server = await ConnectionManager.serverAPI; const { bufferSize } = this; - // TODO make this async and await response here + // TODO and await response here const dataSourceConfig = combineFilters(this.config); + this.server?.subscribe( { ...dataSourceConfig, bufferSize, - viewport, - table: this.table, range: this._range, + selectedIndexValues: selectedIndexValues, + table: this.table, title: this._title, + viewport, }, this.handleMessageFromServer, ); @@ -267,8 +272,6 @@ export class VuuDataSource extends BaseDataSource implements DataSource { } select(selected: Selection) { - //TODO this isn't always going to be correct - need to count - // selection block items this.#selectedRowsCount = selectionCount(selected); if (this.viewport) { this.server?.send({ diff --git a/vuu-ui/packages/vuu-data-remote/src/server-proxy/server-proxy.ts b/vuu-ui/packages/vuu-data-remote/src/server-proxy/server-proxy.ts index 2414d3a6c..1e36492e9 100644 --- a/vuu-ui/packages/vuu-data-remote/src/server-proxy/server-proxy.ts +++ b/vuu-ui/packages/vuu-data-remote/src/server-proxy/server-proxy.ts @@ -240,10 +240,12 @@ export class ServerProxy { viewport.subscribe(), message.viewport, ); - const awaitPendingReponses = Promise.all([ - pendingSubscription, - pendingTableSchema, - ]) as Promise<[VuuViewportCreateResponse, TableSchema]>; + + const pendingResponses = [pendingSubscription, pendingTableSchema]; + const awaitPendingReponses = Promise.all(pendingResponses) as Promise< + [VuuViewportCreateResponse, TableSchema] + >; + awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { const { viewPortId: serverViewportId } = subscribeResponse; const { status: previousViewportStatus } = viewport; @@ -269,6 +271,13 @@ export class ServerProxy { } } + if (message.selectedIndexValues) { + console.log( + `selected = ${JSON.stringify(message.selectedIndexValues)}`, + ); + this.select(viewport, { selected: message.selectedIndexValues }); + } + // In the case of a reconnect, we may have resubscribed a disabled viewport, // reset the disabled state on server if (viewport.disabled) { @@ -466,7 +475,10 @@ export class ServerProxy { } } - private select(viewport: Viewport, message: VuuUIMessageOutSelect) { + private select( + viewport: Viewport, + message: Pick, + ) { const requestId = nextRequestId(); const { selected } = message; const request = viewport.selectRequest(requestId, selected); diff --git a/vuu-ui/packages/vuu-data-types/index.d.ts b/vuu-ui/packages/vuu-data-types/index.d.ts index 686b9650a..5e89ed71b 100644 --- a/vuu-ui/packages/vuu-data-types/index.d.ts +++ b/vuu-ui/packages/vuu-data-types/index.d.ts @@ -455,6 +455,9 @@ export interface SubscribeProps extends Partial> { viewport?: string; range?: VuuRange; + revealSelected?: boolean; + selectedIndexValues?: Selection; + selectedKeyValues?: string[]; title?: string; } @@ -709,6 +712,7 @@ export interface ConnectionQualityMetrics { export interface ServerProxySubscribeMessage extends WithFullConfig { bufferSize?: number; range: VuuRange; + selectedIndexValues?: Selection; table: VuuTable; title?: string; viewport: string; diff --git a/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx b/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx index e06525150..45d92f462 100644 --- a/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/tree-table/TreeTable.tsx @@ -49,6 +49,7 @@ export const TreeTable = ({ return ( { label: typeOf(component), path, childNodes: React.Children.map(component.props.children, (child, i) => - toTreeJson(child, path ? `${path}.${i}` : `${i}`) + toTreeJson(child, path ? `${path}.${i}` : `${i}`), ), }; }; -export const LayoutTreeViewer = ({ layout, onSelect, style }) => { +export const LayoutTreeViewer = ({ layout, onSelect: _, style }) => { const targetWindow = useWindow(); useComponentCssInjection({ testId: "vuu-layout-tree-viewer", @@ -29,16 +29,17 @@ export const LayoutTreeViewer = ({ layout, onSelect, style }) => { const treeJson = [toTreeJson(layout)]; - const handleSelection = (evt, [{ path }]) => { - onSelect(path); + const handleSelection = (row) => { + console.log({ row }); + // onSelect(path); }; return (
-
); diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index 0f4908319..7e7684e6a 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -1,6 +1,7 @@ import { DataSource, SchemaColumn, + Selection, SelectionChangeHandler, } from "@finos/vuu-data-types"; import { ContextMenuProvider } from "@finos/vuu-popups"; @@ -83,6 +84,17 @@ export interface TableProps */ config: TableConfig; dataSource: DataSource; + + /** + * define rows ro be initially selected based on row index positions + */ + defaultSelectedIndexValues?: Selection; + /** + * define rows ro be initially selected based on row key value. Not all DataSource + * implementations support this feature. + */ + defaultSelectedKeyValues?: string[]; + disableFocus?: boolean; /** * Allows additional custom element(s) to be embedded immediately below column headers. @@ -161,6 +173,17 @@ export interface TableProps onSelectionChange?: SelectionChangeHandler; renderBufferSize?: number; + + /** + * Only applicable to grouped data. If there are selected rows which are not top-level + * items and group items above are not already expanded, expand all group items in + * the hierarchy above selected item. Selected items will thus always be visible, initially. + * This affects items set at load time via defaultSelectedKeyValues as well as items + * selected programatically (ie not directly by user). + * Nodes can of course be collapsed by user at runtime which may hide selected rows. + * Note: this is not supported by all DataSource implementations + */ + revealSelected?: boolean; /** * Pixel height of rows. If specified here, this will take precedence over CSS * values and Table will not respond to density changes. @@ -224,6 +247,8 @@ const TableCore = ({ containerRef, customHeader, dataSource, + defaultSelectedIndexValues, + defaultSelectedKeyValues, disableFocus = false, groupToggleTarget, highlightedIndex: highlightedIndexProp, @@ -240,6 +265,7 @@ const TableCore = ({ onSelectCellBlock, onSelectionChange, renderBufferSize = 0, + revealSelected, rowHeight, scrollingApiRef, selectionBookendWidth = 0, @@ -294,6 +320,8 @@ const TableCore = ({ config, containerRef, dataSource, + defaultSelectedIndexValues, + defaultSelectedKeyValues, disableFocus, highlightedIndex: highlightedIndexProp, id, @@ -309,6 +337,7 @@ const TableCore = ({ onSelectCellBlock, onSelectionChange, renderBufferSize, + revealSelected, rowHeight, scrollingApiRef, selectionBookendWidth, @@ -460,6 +489,8 @@ export const Table = forwardRef(function Table( config, customHeader, dataSource, + defaultSelectedIndexValues, + defaultSelectedKeyValues, disableFocus, groupToggleTarget, height, @@ -478,6 +509,7 @@ export const Table = forwardRef(function Table( onSelectCellBlock, onSelectionChange, renderBufferSize, + revealSelected, rowHeight: rowHeightProp, scrollingApiRef, selectionBookendWidth = 4, @@ -519,6 +551,11 @@ export const Table = forwardRef(function Table( if (dataSource === undefined) { throw Error("vuu Table requires dataSource prop"); } + if (defaultSelectedIndexValues && defaultSelectedKeyValues) { + throw Error( + `defaultSelectedIndexValues and defaultSelectedKeyValues can not be used in combination. Use at most one.`, + ); + } if (showPaginationControls && renderBufferSize !== undefined) { console.warn( @@ -597,6 +634,8 @@ export const Table = forwardRef(function Table( containerRef={containerRef} customHeader={customHeader} dataSource={dataSource} + defaultSelectedIndexValues={defaultSelectedIndexValues} + defaultSelectedKeyValues={defaultSelectedKeyValues} disableFocus={disableFocus} groupToggleTarget={groupToggleTarget} highlightedIndex={highlightedIndex} @@ -615,6 +654,7 @@ export const Table = forwardRef(function Table( renderBufferSize={ showPaginationControls ? 0 : Math.max(5, renderBufferSize ?? 0) } + revealSelected={revealSelected} rowHeight={rowHeight} scrollingApiRef={scrollingApiRef} selectionBookendWidth={selectionBookendWidth} diff --git a/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.css b/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.css index c098bd1cc..e5eb7297b 100644 --- a/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.css +++ b/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.css @@ -16,6 +16,7 @@ --vuu-icon-height: 20px; --vuu-icon-width: 20px; margin-right: var(--salt-spacing-100); + min-width: 20px; } .vuuTableGroupCell-toggle { @@ -26,6 +27,11 @@ transition: transform 0.25s; transform: var(--toggle-icon-transform); } + + .vuuTableGroupCell-label { + overflow: hidden; + text-overflow: ellipsis; + } } [aria-level="2"] { @@ -58,5 +64,11 @@ .vuuTableGroupCell-spacer { display: inline-block; - width: calc(var(--indent, 0) * var(--group-cell-spacer-width)); + min-width: calc(var(--indent, 0) * var(--group-cell-spacer-width)); } + +.vuuTreeTable { + .vuuTableGroupCell.vuuTableCell { + border-right: none; + } +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.tsx b/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.tsx index e02c3451f..ef12dbf37 100644 --- a/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.tsx +++ b/vuu-ui/packages/vuu-table/src/table-cell/TableGroupCell.tsx @@ -59,7 +59,7 @@ export const TableGroupCell = ({ )} {icon ? : null} - {value} + {value} ); }; diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index ccb7c0703..aebb60aac 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -1,5 +1,4 @@ import { - DataSource, DataSourceRow, DataSourceSubscribedMessage, SubscribeCallback, @@ -8,19 +7,29 @@ import { VuuRange } from "@finos/vuu-protocol-types"; import { getFullRange, NULL_RANGE, rangesAreSame } from "@finos/vuu-utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { MovingWindow } from "./moving-window"; +import { TableProps } from "./Table"; -export interface DataSourceHookProps { - dataSource: DataSource; +export interface DataSourceHookProps + extends Pick< + TableProps, + | "dataSource" + | "defaultSelectedIndexValues" + | "defaultSelectedKeyValues" + | "renderBufferSize" + | "revealSelected" + > { onSizeChange: (size: number) => void; onSubscribed: (subscription: DataSourceSubscribedMessage) => void; - renderBufferSize?: number; } export const useDataSource = ({ dataSource, + defaultSelectedIndexValues, + defaultSelectedKeyValues, onSizeChange, onSubscribed, renderBufferSize = 0, + revealSelected, }: DataSourceHookProps) => { const [, forceUpdate] = useState(null); const data = useRef([]); @@ -128,7 +137,15 @@ export const useDataSource = ({ dataWindow.setRange(fullRange); if (dataSource.status !== "subscribed") { - dataSource?.subscribe({ range: fullRange }, datasourceMessageHandler); + dataSource?.subscribe( + { + range: fullRange, + revealSelected, + selectedIndexValues: defaultSelectedIndexValues, + selectedKeyValues: defaultSelectedKeyValues, + }, + datasourceMessageHandler, + ); } else { dataSource.range = rangeRef.current = fullRange; } @@ -140,7 +157,15 @@ export const useDataSource = ({ dataSource.emit("range", range); } }, - [dataSource, dataWindow, datasourceMessageHandler, renderBufferSize], + [ + dataSource, + dataWindow, + datasourceMessageHandler, + defaultSelectedIndexValues, + defaultSelectedKeyValues, + renderBufferSize, + revealSelected, + ], ); return { diff --git a/vuu-ui/packages/vuu-table/src/useSelection.ts b/vuu-ui/packages/vuu-table/src/useSelection.ts index fd40c893b..514b7321d 100644 --- a/vuu-ui/packages/vuu-table/src/useSelection.ts +++ b/vuu-ui/packages/vuu-table/src/useSelection.ts @@ -11,7 +11,7 @@ import { queryClosest, selectItem, } from "@finos/vuu-utils"; -import { Selection, SelectionChangeHandler } from "@finos/vuu-data-types"; +import { Selection } from "@finos/vuu-data-types"; import { KeyboardEvent, KeyboardEventHandler, @@ -21,6 +21,7 @@ import { useRef, } from "react"; import { getRowElementByAriaIndex } from "./table-dom-utils"; +import { TableProps } from "./Table"; const { IDX } = metadataKeys; @@ -28,17 +29,18 @@ const NO_SELECTION: Selection = []; const defaultSelectionKeys = ["Enter", " "]; -export interface SelectionHookProps { +export interface SelectionHookProps + extends Pick { containerRef: RefObject; highlightedIndexRef: MutableRefObject; selectionKeys?: string[]; selectionModel: TableSelectionModel; onSelect?: TableRowSelectHandlerInternal; - onSelectionChange: SelectionChangeHandler; } export const useSelection = ({ containerRef, + defaultSelectedIndexValues = NO_SELECTION, highlightedIndexRef, selectionKeys = defaultSelectionKeys, selectionModel, @@ -47,7 +49,7 @@ export const useSelection = ({ }: SelectionHookProps) => { selectionModel === "extended" || selectionModel === "checkbox"; const lastActiveRef = useRef(-1); - const selectedRef = useRef(NO_SELECTION); + const selectedRef = useRef(defaultSelectedIndexValues); const isSelectionEvent = useCallback( (evt: KeyboardEvent) => selectionKeys.includes(evt.key), diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index 7406b1a5f..3ac36cd63 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -102,6 +102,8 @@ export interface TableHookProps | "availableColumns" | "config" | "dataSource" + | "defaultSelectedIndexValues" + | "defaultSelectedKeyValues" | "disableFocus" | "highlightedIndex" | "id" @@ -117,6 +119,7 @@ export interface TableHookProps | "onSelectionChange" | "onRowClick" | "renderBufferSize" + | "revealSelected" | "scrollingApiRef" | "selectionBookendWidth" | "showColumnHeaders" @@ -151,6 +154,8 @@ export const useTable = ({ config, containerRef, dataSource, + defaultSelectedIndexValues, + defaultSelectedKeyValues, disableFocus, highlightedIndex: highlightedIndexProp, id, @@ -166,6 +171,7 @@ export const useTable = ({ onSelectCellBlock, onSelectionChange, renderBufferSize = 0, + revealSelected, rowHeight = 20, scrollingApiRef, selectionBookendWidth, @@ -288,8 +294,10 @@ export const useTable = ({ const { data, dataRef, getSelectedRows, range, setRange } = useDataSource({ dataSource, - // We need to factor this out of Table + defaultSelectedIndexValues, + defaultSelectedKeyValues, renderBufferSize, + revealSelected, onSizeChange: onDataRowcountChange, onSubscribed, }); @@ -669,6 +677,7 @@ export const useTable = ({ onRowClick: selectionHookOnRowClick, } = useSelection({ containerRef, + defaultSelectedIndexValues: defaultSelectedIndexValues, highlightedIndexRef, onSelect: handleSelect, onSelectionChange: handleSelectionChange, diff --git a/vuu-ui/packages/vuu-table/src/useTableModel.ts b/vuu-ui/packages/vuu-table/src/useTableModel.ts index 7f93f7876..e6ab8c055 100644 --- a/vuu-ui/packages/vuu-table/src/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/useTableModel.ts @@ -590,13 +590,15 @@ function updateTableConfig( ) { let result = state; + const { availableWidth, columnLayout = "static" } = state; + if (groupBy.length > 0) { - const groupedColumns = applyGroupByToColumns( - result.columns, + const groupedColumns = applyGroupByToColumns({ + columns: result.columns, groupBy, confirmed, - ); - const { availableWidth, columnLayout = "static" } = state; + availableWidth, + }); const columns = applyWidthToColumns(groupedColumns, { availableWidth, columnLayout, 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 index bd3e07db0..bc7aef494 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.css +++ b/vuu-ui/packages/vuu-ui-controls/src/icon-button/ToggleIconButton.css @@ -22,7 +22,7 @@ border-radius: 0; height: 18px; left: 0; - min-width: 16px; + min-width: 18px; padding: 0; width: 18px; } diff --git a/vuu-ui/packages/vuu-ui-controls/src/index.ts b/vuu-ui/packages/vuu-ui-controls/src/index.ts index 504bbddf0..fc00d6946 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/index.ts @@ -21,7 +21,6 @@ export * from "./split-button"; export * from "./tabs-next"; export * from "./tabstrip"; export * from "./toolbar"; -export * from "./tree"; export * from "./utils"; export * from "./vuu-date-picker"; export * from "./vuu-input"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.css b/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.css deleted file mode 100644 index 8548edd51..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.css +++ /dev/null @@ -1,221 +0,0 @@ -.vuuTree { - --tree-node-collapse: var(--vuuTree-toggle-collapse, var(--svg-tree-node-collapse)); - --tree-node-expand: var(--vuuTree-toggle-expand, var(--svg-tree-node-expand)); - --tree-toggle-width: 12px; - --tree-icon-color: var(--vuuTree-icon-color, #4c505b); - --tree-node-expanded-transform: var(--vuuTree-node-expanded-transform, none); - --tree-node-indent: 0px; - - --list-hilited-bg: var(--hw-list-hilited-bg, rgba(0, 0, 0, 0.1)); - --list-item-height: var(--hw-list-item-height, 30px); - --list-item-padding: var(--hw-list-item-padding, 0 6px); - --list-item-header-bg: var(--hw-list-item-header-bg, black); - --list-item-header-color: var(--hw-list-item-header-color, white); - --list-item-header-font-weight: bold; - --list-item-header-twisty-color: black; - --list-item-header-twisty-content: ''; - --list-item-header-twisty-top: 50%; - --list-item-header-twisty-left: -18px; - --list-item-header-twisty-right: auto; - --list-item-selected-bg: var(--hw-list-selected-bg, #1ea7fd); - --list-item-selected-color: white; - --list-item-text-color: var(--hw-gray-800); - --focus-visible-border-color: var(--hw-focus-visible-border-color, rgb(141, 154, 179)); - - list-style: none; - margin: 0; - padding: 0 1px; - font-size: var(--vuuTree-font-size, 14px); - max-height: inherit; - outline: none; - overflow-y: auto; - position: relative; - user-select: none; -} - -.vuuTree-viewport { - --list-item-height: 30px; - box-sizing: border-box; - max-height: inherit; - overflow: auto; -} - -.vuuTree-scrollingContentContainer { - box-sizing: inherit; - position: relative; -} - -.vuuTree-scrollingContentContainer .vuuTreeNode { - line-height: 30px; - position: absolute; - top: 0; - left: 0; - right: 0; - will-change: transform; -} - -.vuuTreeNode { - list-style: none; -} - -/* Leaf node or the div child of a collapsible node */ -.vuuTreeNode:not([aria-expanded]), -.vuuTreeNode[aria-expanded] > .vuuTreeNode-label { - --checkbox-border-color: black; - --checkbox-border-width: 1px; - --checkbox-tick: black; - --list-item-padding-left: 6px; - --svg-toggle: var(--tree-node-collapse); - - align-items: center; - color: var(--list-item-text-color); - display: flex; - flex-wrap: nowrap; - height: var(--list-item-height); - line-height: var(--list-item-height); - padding: var(--list-item-padding); - padding-left: var(--padding-left); - position: relative; - cursor: default; - margin: 0; - white-space: nowrap; -} - -.vuuTreeNode:not([aria-expanded]) { - --padding-left: calc( - var(--list-item-padding-left) + var(--tree-toggle-width) + var(--tree-node-indent) - ); -} - -.vuuTreeNode[aria-expanded] > .vuuTreeNode-label { - --padding-left: calc( - var(--list-item-padding-left) + var(--tree-toggle-width) + var(--tree-node-indent) - ); -} - -.vuuTreeNode-icon { - background-color: var(--tree-icon-color); - display: inline-block; - height: 18px; - margin-right: 6px; - -webkit-mask: var(--vuu-icon-svg) center center/12px 12px no-repeat; - mask: var(--vuu-icon-svg) center center/12px 12px no-repeat; - flex: 0 0 18px; -} - -.vuuTreeNode[aria-expanded] { - flex-direction: column; -} - -.vuuTreeNode[aria-expanded] { - flex-direction: column; - height: auto; -} - -.vuuTreeNode > *[role='group'] { - padding-left: 0px; -} - -.vuuTreeNode { - padding-left: calc(var(--padding-left) + var(--tree-node-indent)); -} - -.vuuTreeNode[aria-level='2'] { - --tree-node-indent: 12px; -} -.vuuTreeNode[aria-level='3'] { - --tree-node-indent: 24px; -} -.vuuTreeNode[aria-level='4'] { - --tree-node-indent: 36px; -} - -.vuuTreeNode:not(.focusVisible):not(.hwListItemHeader):not([aria-expanded])[data-highlighted], -.vuuTreeNode:not(.focusVisible):not(.hwListItemHeader)[aria-expanded][data-highlighted] - > div:first-child { - background-color: var(--list-hilited-bg); -} - -.vuuTreeNode-toggle { - cursor: pointer; -} - -.vuuTreeNode > .vuuTreeNode-toggle { - display: inline-block; - height: 100%; - left: 0; - position: absolute; - width: calc(var(--list-item-padding-left) + var(--tree-toggle-width)); -} - -.vuuTreeNode[aria-expanded] > .vuuTreeNode-label:after { - content: var(--list-item-header-twisty-content); - -webkit-mask: var(--svg-toggle) center center/8px 8px no-repeat; - mask: var(--svg-toggle) center center/8px 8px no-repeat; - background-color: var(--list-item-header-twisty-color); - height: 18px; - margin-top: -9px; - left: var(--tree-node-indent); - position: absolute; - top: var(--list-item-header-twisty-top); - transition: transform 0.3s; - width: 18px; -} - -.vuuTreeNode[aria-selected='true'] { - --list-item-header-twisty-color: var(--list-item-selected-color); -} - - -.vuuTreeNode:not(.focusVisible):focus { - background-color: rgba(0, 0, 0, 0.1); -} - -.vuuTreeNode:not([aria-expanded]).focusVisible:before, -.vuuTreeNode[aria-expanded].focusVisible > div:first-child:before { - content: ''; - position: absolute; - top: 0px; - left: var(--tree-focus-offset, 0px); - right: 0; - bottom: 0px; - border: dotted var(--focus-visible-border-color) 2px; - background-color: var(--list-hilited-bg); -} - - -.vuuTreeNode[aria-expanded='false'] > *:first-child:after { - --svg-toggle: var(--tree-node-expand); -} - -.vuuTreeNode[aria-expanded='true'] > *:first-child:after { - transform: var(--tree-node-expanded-transform); -} - -/* Selection */ - -.vuuTree:not(.checkbox-only) .vuuTreeNode:not([aria-expanded])[aria-selected='true'], -.vuuTree:not(.checkbox-only) .vuuTreeNode[aria-expanded][aria-selected='true'] > div:first-child { - --checkbox-border-color: var(--list-item-selected-color); - --checkbox-tick: var(--list-item-selected-color); - --focus-visible-border-color: var(--list-item-selected-color); - background-color: var(--list-item-selected-bg); - color: var(--list-item-selected-color); -} - -.with-checkbox .vuuTreeNode { - padding-left: 28px; -} - -.with-checkbox .vuuTreeNode:before { - border-style: solid; - border-width: var(--checkbox-border-width); - border-color: var(--checkbox-border-color); - content: ''; - height: 12px; - left: 3px; - margin-top: -7px; - position: absolute; - top: 50%; - width: 12px; -} diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx b/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx deleted file mode 100644 index 79d0d49d5..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/Tree.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { useForkRef, useIdMemo as useId } from "@salt-ds/core"; -import cx from "clsx"; -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; -import { - ForwardedRef, - HTMLAttributes, - MouseEvent, - forwardRef, - useRef, -} from "react"; -import { closestListItemIndex } from "./list-dom-utils"; -import { isExpanded } from "./treeTypeUtils"; -import { useItemsWithIds } from "./use-items-with-ids"; -import { - GroupSelection, - TreeNodeSelectionHandler, - TreeSelection, - groupSelectionEnabled, -} from "./use-selection"; -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"; - -type Indexer = { - value: number; -}; - -export interface TreeNodeProps extends HTMLAttributes { - idx?: number; -} - -// eslint-disable-next-line no-unused-vars -export const TreeNode = ({ children, idx, ...props }: TreeNodeProps) => { - return
  • {children}
  • ; -}; - -export interface TreeProps extends HTMLAttributes { - allowDragDrop?: boolean; - defaultSelected?: any; - groupSelection?: GroupSelection; - onHighlight?: (index: number) => void; - onSelectionChange?: (selected: TreeSourceNode[]) => void; - revealSelected?: boolean; - selected?: string[]; - selection?: TreeSelection; - source: TreeSourceNode[]; -} - -export const Tree = forwardRef(function Tree( - { - allowDragDrop, - className, - defaultSelected, - groupSelection = "none", - id: idProp, - onHighlight, - onSelectionChange, - revealSelected, - selected: selectedProp, - selection = "single", - source, - ...htmlAttributes - }: TreeProps, - forwardedRef: ForwardedRef, -) { - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "vuu-tree", - css: treeCss, - window: targetWindow, - }); - - const id = useId(idProp); - const rootRef = useRef(null); - // returns the full source data - const [, sourceWithIds, sourceItemById] = useItemsWithIds(source, id, { - revealSelected: revealSelected - ? (selectedProp ?? defaultSelected ?? false) - : undefined, - }); - - const handleSelectionChange: TreeNodeSelectionHandler = (evt, selected) => { - if (onSelectionChange) { - const sourceItems = selected - .map((id) => sourceItemById(id)) - .filter((sourceItem) => sourceItem !== undefined) as TreeSourceNode[]; - onSelectionChange(sourceItems); - } - }; - - const { - focusVisible, - highlightedIdx, - hiliteItemAtIndex, - listProps, - listItemHandlers, - selected, - visibleData, - } = useTree({ - defaultSelected, - groupSelection, - onChange: handleSelectionChange, - onHighlight, - selected: selectedProp, - selection, - sourceWithIds, - }); - - // const isScrolling = useViewportTracking(root, highlightedIdx); - useViewportTracking(rootRef, highlightedIdx); - - const defaultItemHandlers = { - onMouseEnter: (evt: MouseEvent) => { - // if (!isScrolling.current) { - const targetEl = evt.target as HTMLElement; - const idx = closestListItemIndex(targetEl); - hiliteItemAtIndex(idx); - // onMouseEnterListItem && onMouseEnterListItem(evt, idx); - // } - }, - }; - - const propsCommonToAllListItems = { - ...defaultItemHandlers, - ...listItemHandlers, - role: "treeitem", - }; - const allowGroupSelect = groupSelectionEnabled(groupSelection); - - /** - * Add a ListItem from source item - */ - function addLeafNode( - list: JSX.Element[], - item: NormalisedTreeSourceNode, - idx: Indexer, - ) { - list.push( - - {item.icon ? ( - - ) : null} - {item.label} - , - ); - idx.value += 1; - } - - function addGroupNode( - list: JSX.Element[], - child: NormalisedTreeSourceNode, - idx: Indexer, - id: string, - title: string, - ) { - const { value: i } = idx; - idx.value += 1; - list.push( - - {allowGroupSelect ? ( -
    - - {title} -
    - ) : ( -
    - {child.icon ? ( - - ) : null} - {title} -
    - )} -
      - {isExpanded(child) ? renderSourceContent(child.childNodes, idx) : ""} -
    -
    , - ); - } - - function renderSourceContent( - items: NormalisedTreeSourceNode[], - idx = { value: 0 }, - ) { - if (items?.length > 0) { - const listItems: JSX.Element[] = []; - for (const item of items) { - if (item.childNodes) { - addGroupNode(listItems, item, idx, item.id, item.label); - } else { - addLeafNode(listItems, item, idx); - } - } - return listItems; - } - } - - return ( -
      (rootRef, forwardedRef)} - role="tree" - tabIndex={0} - > - {renderSourceContent(visibleData)} -
    - ); -}); - -const getListItemProps = ( - item: NormalisedTreeSourceNode, - idx: Indexer, - highlightedIdx: number, - selected: string[], - focusVisible: number, - className?: string, -) => ({ - id: item.id, - key: item.id, - "aria-level": item.level, - "aria-selected": selected.includes(item.id) || undefined, - "data-idx": idx.value, - "data-highlighted": idx.value === highlightedIdx || undefined, - className: cx("vuuTreeNode", className, { - focusVisible: focusVisible === idx.value, - }), -}); - -Tree.displayName = "Tree"; 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 deleted file mode 100644 index 073bb3ad3..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/hierarchical-data-utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { NonLeafNode, NormalisedTreeSourceNode } from "@finos/vuu-utils"; - -export const getNodeParentPath = ({ id }: NormalisedTreeSourceNode) => { - let pos = id.lastIndexOf("-"); - if (pos !== -1) { - // using the built-in hierarchical id scheme - // rootId-n-n.n - const path = id.slice(pos + 1); - const steps = path.split("."); - if (steps.length === 1) { - return null; - } else { - steps.pop(); - return `${id.slice(0, pos)}-${steps.join(".")}`; - } - } else if ((pos = id.lastIndexOf("/")) !== -1) { - // using a path scheme step/step/step - return id.slice(0, pos); - } -}; - -export const isGroupNode = (node: NormalisedTreeSourceNode) => - node.childNodes !== undefined; -export const isCollapsibleGroupNode = (node: NormalisedTreeSourceNode) => - isGroupNode(node) && node.expanded !== undefined; -export const isHeader = (node: NormalisedTreeSourceNode) => - node.header === true; - -const PATH_SEPARATORS = new Set([".", "/"]); - -const isDescendantOf = ( - node: NormalisedTreeSourceNode, - targetPath: string, -): node is NonLeafNode => { - if (!targetPath.startsWith(node.id)) { - return false; - } else { - return PATH_SEPARATORS.has(targetPath.charAt(node.id.length)); - } -}; - -export const getNodeById = ( - nodes: NormalisedTreeSourceNode[], - id: string, -): NormalisedTreeSourceNode | undefined => { - for (const node of nodes) { - if (node.id === id) { - return node; - } else if (isDescendantOf(node, id)) { - return getNodeById(node.childNodes, id); - } - } -}; - -export const getIndexOfNode = ( - treeNodes: NormalisedTreeSourceNode[], - node: NormalisedTreeSourceNode, -) => { - const id = typeof node === "string" ? node : node.id; - for (let i = 0; i < treeNodes.length; i++) { - if (treeNodes[i].id === id) { - return i; - } - } -}; - -export const replaceNode = ( - nodes: NormalisedTreeSourceNode[], - id: string, - props: Partial, -): NormalisedTreeSourceNode[] => { - let childNodes; - const newNodes = nodes.map((node) => { - if (node.id === id) { - return { - ...node, - ...props, - }; - } else if (isDescendantOf(node, id)) { - childNodes = replaceNode(node.childNodes, id, props); - return { - ...node, - childNodes, - }; - } else { - return node; - } - }); - - return newNodes; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts deleted file mode 100644 index 057c4493b..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./Tree"; -export * from "./Tree"; -export * from "./use-items-with-ids"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/key-code.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/key-code.ts deleted file mode 100644 index 2034d0d4a..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/key-code.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { KeyboardEvent } from "react"; - -function union(set1: Set, ...sets: Set[]) { - const result = new Set(set1); - for (const set of sets) { - for (const element of set) { - result.add(element); - } - } - return result; -} - -export const ArrowUp = "ArrowUp"; -export const ArrowDown = "ArrowDown"; -export const ArrowLeft = "ArrowLeft"; -export const Backspace = "Backspace"; -export const ArrowRight = "ArrowRight"; -export const Enter = "Enter"; -export const Escape = "Escape"; -export const Delete = "Delete"; - -const actionKeys = new Set([Enter, Delete]); -const focusKeys = new Set(["Tab"]); -// const navigationKeys = new Set(["Home", "End", "ArrowRight", "ArrowLeft","ArrowDown", "ArrowUp"]); -const arrowLeftRightKeys = new Set(["ArrowRight", "ArrowLeft"]); -const verticalNavigationKeys = new Set(["Home", "End", "ArrowDown", "ArrowUp"]); -const horizontalNavigationKeys = new Set([ - "Home", - "End", - "ArrowRight", - "ArrowLeft", -]); -const functionKeys = new Set([ - "F1", - "F2", - "F3", - "F4", - "F5", - "F6", - "F7", - "F8", - "F9", - "F10", - "F11", - "F12", -]); -const specialKeys = union( - actionKeys, - horizontalNavigationKeys, - verticalNavigationKeys, - arrowLeftRightKeys, - functionKeys, - focusKeys -); -export const isCharacterKey = (evt: KeyboardEvent) => { - if (specialKeys.has(evt.key)) { - return false; - } - if (typeof evt.which === "number" && evt.which > 0) { - return !evt.ctrlKey && !evt.metaKey && !evt.altKey && evt.which !== 8; - } -}; - -export const isNavigationKey = ( - { key }: KeyboardEvent, - orientation = "vertical" -) => { - const navigationKeys = - orientation === "vertical" - ? verticalNavigationKeys - : horizontalNavigationKeys; - return navigationKeys.has(key); -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/list-dom-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/list-dom-utils.ts deleted file mode 100644 index b2703e4ea..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/list-dom-utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const listItemElement = (listEl: HTMLElement, listItemIdx: number) => - listEl.querySelector(`:scope > [data-idx="${listItemIdx}"]`); - -export function listItemIndex(listItemEl: HTMLElement) { - if (listItemEl) { - let idx = listItemEl.dataset.idx; - if (idx) { - return parseInt(idx, 10); - // eslint-disable-next-line no-cond-assign - } else if ((idx = listItemEl.ariaPosInSet ?? "-1")) { - return parseInt(idx, 10) - 1; - } - } -} - -export const listItemId = (el: HTMLElement | null) => el?.id; - -export const closestListItem = (el: HTMLElement) => - el.closest("[data-idx],[aria-posinset]") as HTMLElement; - -export const closestListItemId = (el: HTMLElement) => - listItemId(closestListItem(el)); - -export const closestListItemIndex = (el: HTMLElement) => - listItemIndex(closestListItem(el)); diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts deleted file mode 100644 index 5d9ff277c..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/treeTypeUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { NonLeafNode, NormalisedTreeSourceNode } from "@finos/vuu-utils"; - -export const isExpanded = ( - 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 deleted file mode 100644 index d986b0735..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-collapsible-groups.ts +++ /dev/null @@ -1,116 +0,0 @@ -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 "@finos/vuu-utils"; - -const NO_HANDLERS: CollapsibleHookResult["listHandlers"] = {}; -const isToggleElement = (element: HTMLElement) => - element && element.hasAttribute("aria-expanded"); - -export interface CollapsibleGroupsHookProps { - collapsibleHeaders?: boolean; - highlightedIdx: number; - treeNodes: NormalisedTreeSourceNode[]; - setVisibleData: (nodes: NormalisedTreeSourceNode[]) => void; - source: NormalisedTreeSourceNode[]; -} - -export interface CollapsibleHookResult { - listHandlers: { - onKeyDown?: (e: KeyboardEvent) => void; - }; - listItemHandlers: { - onClick: (e: MouseEvent) => void; - }; -} - -export const useCollapsibleGroups = ({ - collapsibleHeaders, - highlightedIdx, - treeNodes, - setVisibleData, - source, -}: CollapsibleGroupsHookProps): CollapsibleHookResult => { - const fullSource = useRef(source); - const stateSource = useRef(fullSource.current); - - const setSource = useCallback( - (value) => { - setVisibleData((stateSource.current = value)); - }, - [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( - (e: KeyboardEvent) => { - if (e.key === ArrowRight || e.key === Enter) { - const node = treeNodes[highlightedIdx]; - if (node) { - if (node.expanded === false) { - e.preventDefault(); - setSource(expandNode(stateSource.current, node)); - } - } - } - - if (e.key === ArrowLeft || e.key === Enter) { - const node = treeNodes[highlightedIdx]; - if (node) { - if (node.expanded) { - e.preventDefault(); - setSource(collapseNode(stateSource.current, node)); - } - } - } - }, - [collapseNode, expandNode, highlightedIdx, treeNodes, setSource], - ); - - /** - * These are List handlers, so we will not have reference to the actual node - * element. We must rely on highlightedIdx to tell us which node is interactive. - */ - const listHandlers = collapsibleHeaders - ? { - onKeyDown: handleKeyDown, - } - : NO_HANDLERS; - - const handleClick = useCallback( - (evt) => { - const el = closestListItem(evt.target); - if (isToggleElement(el)) { - evt.stopPropagation(); - evt.preventDefault(); - const node = getNodeById(source, el.id); - if (node?.expanded === false) { - setSource(expandNode(source, node)); - } else if (node?.expanded === true) { - setSource(collapseNode(source, node)); - } - } - }, - [collapseNode, expandNode, setSource, source], - ); - - const listItemHandlers = { - onClick: handleClick, - }; - - return { - listHandlers, - 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 deleted file mode 100644 index f89de1523..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-hierarchical-data.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useRef, useState } from "react"; -import { isGroupNode, isHeader } from "./hierarchical-data-utils"; -import { isExpanded } from "./treeTypeUtils"; -import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; - -const populateIndices = ( - nodes: NormalisedTreeSourceNode[], - results: NormalisedTreeSourceNode[] = [], - idx = { value: 0 }, -) => { - let skipToNextHeader = false; - for (const node of nodes) { - if (skipToNextHeader && !isHeader(node)) { - continue; - } else { - results[idx.value] = node; - idx.value += 1; - skipToNextHeader = false; - if (isHeader(node) && node.expanded === false) { - skipToNextHeader = true; - } else if (isGroupNode(node)) { - if (isExpanded(node)) { - populateIndices(node.childNodes, results, idx); - } - } - } - } - return results; -}; - -//TODO return a read-only data structure -// Question: is source changes at runtime, do we lose any current state ? -export const useHierarchicalData = (source: NormalisedTreeSourceNode[]) => { - // console.log(`%c[useHierarchicalData<${label}>] entry`, 'color: green; font-weight: bold;'); - - const externalSource = useRef(source); - const statefulSource = useRef(source); - const indexPositions = useRef(populateIndices(source)); - const [, forceUpdate] = useState({}); - - // Maintain a mapping between nodes and their current index position within the rendered list. - // This index position is liable to change with every expand/collapse operation. We require this - // when handling keyboard events - these are List level, not listItem level, so we depend on the - - // Client needs to be careful source is not recreated inadvertently on each render - if (source !== externalSource.current) { - // console.log( - // `%cuseHierarchicalData source has changed`, - // 'color:red;font-weight: bold;', - // externalSource.current, - // source - // ); - externalSource.current = source; - // we might want to try and merge existing state here ? - statefulSource.current = source; - indexPositions.current = populateIndices(source); - } - - const setData = (value: NormalisedTreeSourceNode[]) => { - statefulSource.current = value; - indexPositions.current = populateIndices(value); - // console.log( - // `data set in ${label} (${indexPositions.current.length} visible items)`, - // indexPositions.current.map((i) => ({ index: i.index, label: i.label })) - // ); - forceUpdate({}); - }; - - return { - // data, // do we actually use the data anywhere - data: statefulSource.current, - indexPositions: indexPositions.current, - setData, - }; -}; 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 deleted file mode 100644 index 8edff0848..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-items-with-ids.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { NormalisedTreeSourceNode, TreeSourceNode } from "@finos/vuu-utils"; - -const PathSeparators = new Set(["/", "-", "."]); -// TODO where do we define or identify separators -const isPathSeparator = (char: string) => PathSeparators.has(char); - -const isParentPath = (parentPath: string, childPath: string) => - childPath.startsWith(parentPath) && - isPathSeparator(childPath[parentPath.length]); - -type Indexer = { - index: number; -}; - -type SourceItemById = ( - id: string, - target?: NormalisedTreeSourceNode[], -) => TreeSourceNode | undefined; - -export const useItemsWithIds = ( - sourceProp: TreeSourceNode[], - idRoot = "root", - { - collapsibleHeaders = undefined, - defaultExpanded = false, - revealSelected = false, - } = {}, -): [number, NormalisedTreeSourceNode[], SourceItemById] => { - const countChildItems = ( - item: TreeSourceNode, - items: TreeSourceNode[], - idx: number, - ) => { - if (item.childNodes) { - return item.childNodes.length; - } else if (item.header) { - let i = idx + 1; - let count = 0; - while (i < items.length && !items[i].header) { - count++; - i++; - } - return count; - } else { - return 0; - } - }; - - const isExpanded = useCallback( - (path) => { - if (Array.isArray(revealSelected)) { - return revealSelected.some((id) => isParentPath(path, id)); - } - return defaultExpanded; - }, - [defaultExpanded, revealSelected], - ); - - const normalizeItems = useCallback( - ( - items: TreeSourceNode[], - indexer: Indexer, - level = 1, - path = "", - results: NormalisedTreeSourceNode[] = [], - flattenedSource: TreeSourceNode[] = [], - ): [number, NormalisedTreeSourceNode[], TreeSourceNode[]] => { - let count = 0; - // TODO get rid of the Proxy - items.forEach((item, i, all) => { - const isCollapsibleHeader = item.header && collapsibleHeaders; - const isNonCollapsibleGroupNode = - item.childNodes && collapsibleHeaders === false; - const isLeaf = !item.childNodes || item.childNodes.length === 0; - const nonCollapsible = - isNonCollapsibleGroupNode || (isLeaf && !isCollapsibleHeader); - const childPath = path ? `${path}.${i}` : `${i}`; - const id = item.id ?? `${idRoot}-${childPath}`; - - const expanded = nonCollapsible ? undefined : isExpanded(id); - //TODO dev time check - if id is provided by user, make sure - // hierarchical pattern is consistent - const normalisedItem: NormalisedTreeSourceNode = { - ...item, - childNodes: undefined, - id, - count: - !isNonCollapsibleGroupNode && expanded === undefined - ? 0 - : countChildItems(item, all, i), - expanded, - index: indexer.index, - level, - }; - results.push(normalisedItem); - flattenedSource.push(items[i]); - - count += 1; - indexer.index += 1; - - // if ((isNonCollapsibleGroupNode || expanded !== undefined) && !isCollapsibleHeader) { - if (item.childNodes) { - const [childCount, children] = normalizeItems( - item.childNodes, - indexer, - level + 1, - childPath, - [], - flattenedSource, - ); - normalisedItem.childNodes = children; - if (expanded === true || isNonCollapsibleGroupNode) { - count += childCount; - } - } - }); - return [count, results, flattenedSource]; - }, - [collapsibleHeaders, idRoot, isExpanded], - ); - - const [count, sourceWithIds, flattenedSource] = useMemo< - [number, NormalisedTreeSourceNode[], TreeSourceNode[]] - >(() => { - return normalizeItems(sourceProp, { index: 0 }); - }, [normalizeItems, sourceProp]); - - const sourceItemById = useCallback( - (id, target = sourceWithIds): TreeSourceNode | undefined => { - const sourceWithId = target.find( - (i) => i.id === id || (i?.childNodes?.length && isParentPath(i.id, id)), - ); - if (sourceWithId?.id === id) { - return flattenedSource[sourceWithId.index]; - } else if (sourceWithId) { - return sourceItemById(id, sourceWithId.childNodes); - } - }, - [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 deleted file mode 100644 index ada0e1e97..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-keyboard-navigation.ts +++ /dev/null @@ -1,172 +0,0 @@ -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 "@finos/vuu-utils"; - -function nextItemIdx(count: number, key: string, idx: number) { - if (key === ArrowUp || key === ArrowLeft) { - if (idx > 0) { - return idx - 1; - } else { - return idx; - } - } else { - if (idx === null) { - return 0; - } else if (idx === count - 1) { - return idx; - } else { - return idx + 1; - } - } -} - -const isLeaf = (item: NormalisedTreeSourceNode) => - !item.header && !item.childNodes; -const isFocusable = (item: NormalisedTreeSourceNode) => - isLeaf(item) || item.expanded !== undefined; - -export interface KeyboardNavigationHookProps { - defaultHighlightedIdx?: number; - highlightedIdx?: number; - onHighlight?: (highlightedIdx: number) => void; - onKeyboardNavigation?: (evt: KeyboardEvent, nextIdx: number) => void; - selected: string[]; - treeNodes: NormalisedTreeSourceNode[]; -} - -// we need a way to set highlightedIdx when selection changes -export const useKeyboardNavigation = ({ - defaultHighlightedIdx = -1, - highlightedIdx: highlightedIdxProp, - treeNodes, - onHighlight, - onKeyboardNavigation, - selected = [], -}: KeyboardNavigationHookProps) => { - const { bwd: ArrowBwd, fwd: ArrowFwd } = useMemo( - () => ({ - bwd: ArrowUp, - fwd: ArrowDown, - }), - [], - ); - - const [highlightedIdx, setHighlightedIdx, isControlledHighlighting] = - useControlled({ - controlled: highlightedIdxProp, - default: defaultHighlightedIdx, - name: "highlightedIdx", - }); - - const setHighlightedIndex = useCallback( - (idx) => { - onHighlight?.(idx); - setHighlightedIdx(idx); - }, - [onHighlight, setHighlightedIdx], - ); - - const nextFocusableItemIdx = useCallback( - (key = ArrowFwd, idx = key === ArrowFwd ? -1 : treeNodes.length) => { - let nextIdx = nextItemIdx(treeNodes.length, key, idx); - while ( - nextIdx !== -1 && - ((key === ArrowFwd && nextIdx < treeNodes.length) || - (key === ArrowBwd && nextIdx > 0)) && - !isFocusable(treeNodes[nextIdx]) - ) { - nextIdx = nextItemIdx(treeNodes.length, key, nextIdx); - } - return nextIdx; - }, - [ArrowBwd, ArrowFwd, treeNodes], - ); - - // does this belong here or should it be a method passed in? - const keyBoardNavigation = useRef(true); - const ignoreFocus = useRef(false); - const setIgnoreFocus = (value: boolean) => (ignoreFocus.current = value); - - const handleFocus = useCallback(() => { - if (ignoreFocus.current) { - ignoreFocus.current = false; - } else if (selected.length > 0) { - const node = getNodeById(treeNodes, selected[0]); - if (node) { - const idx = getIndexOfNode(treeNodes, node); - setHighlightedIndex(idx); - } - } else { - setHighlightedIndex(nextFocusableItemIdx()); - } - }, [treeNodes, nextFocusableItemIdx, selected, setHighlightedIndex]); - - const navigateChildItems = useCallback( - (e) => { - const nextIdx = nextFocusableItemIdx(e.key, highlightedIdx); - if (nextIdx !== highlightedIdx) { - setHighlightedIndex(nextIdx); - // What exactly is the point of this ? - onKeyboardNavigation?.(e, nextIdx); - } - }, - [ - highlightedIdx, - nextFocusableItemIdx, - onKeyboardNavigation, - setHighlightedIndex, - ], - ); - - const handleKeyDown = useCallback( - (e) => { - if (treeNodes.length > 0 && isNavigationKey(e, "vertical")) { - e.preventDefault(); - e.stopPropagation(); - keyBoardNavigation.current = true; - navigateChildItems(e); - } - }, - [treeNodes, navigateChildItems], - ); - - const listProps = useMemo( - () => ({ - onBlur: () => { - setHighlightedIndex(-1); - }, - onFocus: handleFocus, - onKeyDown: handleKeyDown, - onMouseDownCapture: () => { - keyBoardNavigation.current = false; - setIgnoreFocus(true); - }, - - // onMouseEnter would seem less expensive but it misses some cases - // SHould this be here - this is not strictly keyboard nav - onMouseMove: () => { - if (keyBoardNavigation.current) { - keyBoardNavigation.current = false; - } - }, - onMouseLeave: () => { - keyBoardNavigation.current = true; - setIgnoreFocus(false); - setHighlightedIndex(-1); - }, - }), - [handleFocus, handleKeyDown, setHighlightedIndex], - ); - - return { - focusVisible: keyBoardNavigation.current ? highlightedIdx : -1, - controlledHighlighting: isControlledHighlighting, - highlightedIdx, - hiliteItemAtIndex: setHighlightedIndex, - keyBoardNavigation, - listProps, - setIgnoreFocus, - }; -}; 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 deleted file mode 100644 index a615fa3a7..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-selection.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - KeyboardEvent, - MouseEvent, - SyntheticEvent, - useCallback, - useRef, -} from "react"; -import { useControlled } from "@salt-ds/core"; -import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; - -export type TreeSelection = - | "none" - | "single" - | "checkbox" - | "multi" - | "extended"; - -export const SINGLE = "single"; -export const CHECKBOX = "checkbox"; -export const MULTI = "multi"; -export const EXTENDED = "extended"; - -export type GroupSelection = "none" | "single" | "cascade"; - -const defaultSelectionKeys = ["Enter", " "]; - -const NO_HANDLERS = {}; - -const isCollapsibleItem = (item: NormalisedTreeSourceNode) => - item.expanded !== undefined; - -export type TreeNodeSelectionHandler = ( - evt: SyntheticEvent, - selected: string[], -) => void; - -export const groupSelectionEnabled = (groupSelection: GroupSelection) => - groupSelection && groupSelection !== "none"; - -export interface SelectionHookProps { - defaultSelected?: string[]; - highlightedIdx: number; - onChange: TreeNodeSelectionHandler; - selected?: string[]; - selection: TreeSelection; - selectionKeys?: string[]; - treeNodes: NormalisedTreeSourceNode[]; -} - -export interface SelectionHookResult { - listHandlers: { - onKeyDown?: (evt: KeyboardEvent) => void; - onKeyboardNavigation?: (evt: KeyboardEvent, currentIndex: number) => void; - }; - listItemHandlers: { - onClick?: (evt: MouseEvent) => void; - }; - selected: string[]; - setSelected: (selected: string[]) => void; -} - -export const useSelection = ({ - defaultSelected, - highlightedIdx, - treeNodes, - onChange, - selected: selectedProp, - selection = SINGLE, - selectionKeys = defaultSelectionKeys, -}: SelectionHookProps): SelectionHookResult => { - const singleSelect = selection === SINGLE; - const multiSelect = selection === MULTI || selection.startsWith(CHECKBOX); - const extendedSelect = selection === EXTENDED; - const lastActive = useRef(-1); - - const isSelectionEvent = useCallback( - (evt) => selectionKeys.includes(evt.key), - [selectionKeys], - ); - - const [selected, setSelected] = useControlled({ - controlled: selectedProp, - default: defaultSelected ?? [], - name: "selected", - }); - - // const highlightedIdxRef = useRef(); - // highlightedIdxRef.current = highlightedIdx; - - const selectItemAtIndex = useCallback( - ( - evt: SyntheticEvent, - idx: number, - id: string, - rangeSelect: boolean, - preserveExistingSelection = false, - ) => { - const { current: active } = lastActive; - const isSelected = selected?.includes(id); - const inactiveRange = active === -1; - const actsLikeSingleSelect = - singleSelect || - (extendedSelect && - !preserveExistingSelection && - (!rangeSelect || inactiveRange)); - const actsLikeMultiSelect = - multiSelect || - (extendedSelect && preserveExistingSelection && !rangeSelect); - - let newSelected: string[] = []; - if (actsLikeSingleSelect && isSelected) { - newSelected = []; - } else if (actsLikeSingleSelect) { - newSelected = [id]; - } else if (actsLikeMultiSelect && isSelected) { - newSelected = selected.filter((i) => i !== id); - } else if (actsLikeMultiSelect) { - newSelected = selected.concat(id); - } else if (extendedSelect) { - const [from, to] = idx > active ? [active, idx] : [idx, active]; - newSelected = selected.slice(); - for (let i = from; i <= to; i++) { - const { id } = treeNodes[i]; - if (!selected.includes(id)) { - newSelected.push(id); - } - } - } - setSelected(newSelected); - if (onChange) { - onChange(evt, newSelected); - } - }, - [ - extendedSelect, - treeNodes, - multiSelect, - onChange, - selected, - setSelected, - singleSelect, - ], - ); - - const handleKeyDown = useCallback( - (evt: KeyboardEvent) => { - if (~highlightedIdx && isSelectionEvent(evt)) { - evt.preventDefault(); - const item = treeNodes[highlightedIdx]; - selectItemAtIndex( - evt, - highlightedIdx, - item.id, - false, - evt.ctrlKey || evt.metaKey, - ); - if (extendedSelect) { - lastActive.current = highlightedIdx; - } - } - }, - [ - extendedSelect, - highlightedIdx, - treeNodes, - isSelectionEvent, - selectItemAtIndex, - ], - ); - - const handleKeyboardNavigation = useCallback( - (evt: KeyboardEvent, currentIndex: number) => { - if (extendedSelect && evt.shiftKey) { - const item = treeNodes[currentIndex]; - selectItemAtIndex(evt, currentIndex, item.id, true); - } - }, - [extendedSelect, treeNodes, selectItemAtIndex], - ); - - const listHandlers = - selection === "none" - ? NO_HANDLERS - : { - onKeyDown: handleKeyDown, - onKeyboardNavigation: handleKeyboardNavigation, - }; - - const handleClick = useCallback( - (evt: MouseEvent) => { - if (highlightedIdx !== -1) { - const item = treeNodes[highlightedIdx]; - if (!isCollapsibleItem(item)) { - evt.preventDefault(); - evt.stopPropagation(); - selectItemAtIndex( - evt, - highlightedIdx, - item.id, - evt.shiftKey, - evt.ctrlKey || evt.metaKey, - ); - if (extendedSelect) { - lastActive.current = highlightedIdx; - } - } - } - }, - [extendedSelect, highlightedIdx, treeNodes, selectItemAtIndex], - ); - - const listItemHandlers = - selection === "none" - ? NO_HANDLERS - : { - onClick: handleClick, - }; - - return { - listHandlers, - listItemHandlers, - selected, - setSelected, - }; -}; 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 deleted file mode 100644 index 28bf36222..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-tree-keyboard-navigation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useCallback } from "react"; -import { ArrowLeft } from "./key-code"; -import { - getNodeById, - getNodeParentPath, - getIndexOfNode, -} from "./hierarchical-data-utils"; -import { NormalisedTreeSourceNode } from "@finos/vuu-utils"; - -export interface TreeKeyboardNavigationHookProps { - highlightedIdx: number; - hiliteItemAtIndex: (idx: number) => void; - indexPositions: NormalisedTreeSourceNode[]; - source: NormalisedTreeSourceNode[]; -} - -// we need a way to set highlightedIdx when selection changes -export const useTreeKeyboardNavigation = ({ - highlightedIdx, - hiliteItemAtIndex, - indexPositions, - source, -}: TreeKeyboardNavigationHookProps) => { - const handleKeyDown = useCallback( - (e) => { - if (e.key === ArrowLeft) { - const node = indexPositions[highlightedIdx]; - const parentId = getNodeParentPath(node); - if (parentId) { - e.preventDefault(); - const parentNode = getNodeById(source, parentId); - if (parentNode) { - const idx = getIndexOfNode(indexPositions, parentNode); - if (idx !== undefined) { - hiliteItemAtIndex(idx); - } - } - } - } - }, - [highlightedIdx, hiliteItemAtIndex, indexPositions, source], - ); - - const listHandlers = { - onKeyDown: handleKeyDown, - }; - - return { - listHandlers, - }; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/use-viewport-tracking.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/use-viewport-tracking.ts deleted file mode 100644 index 41873b214..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/use-viewport-tracking.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - RefObject, - useCallback, - useEffect, - useLayoutEffect, - useRef, -} from "react"; -import { useResizeObserver } from "../common-hooks"; - -const HeightOnly = ["height", "scrollHeight"]; - -export const useViewportTracking = ( - root: RefObject, - highlightedIdx: number, - stickyHeaders = false -) => { - const scrollTop = useRef(0); - const scrolling = useRef(false); - const rootHeight = useRef(0); - const rootScrollHeight = useRef(0); - - const scrollIntoView = useCallback( - (el) => { - const targetEl = el.ariaExpanded ? el.firstChild : el; - const headerHeight = stickyHeaders ? 30 : 0; - const t = targetEl.offsetTop; - const h = targetEl.offsetHeight; - const viewportStart = scrollTop.current + headerHeight; - const viewportEnd = viewportStart + rootHeight.current - headerHeight; - - if (t + h > viewportEnd || t < viewportStart) { - scrollTop.current = - t + h > viewportEnd - ? scrollTop.current + (t + h) - viewportEnd - : t - headerHeight; - - scrolling.current = true; - if (root.current) { - root.current.scrollTop = scrollTop.current; - } - setTimeout(() => { - scrolling.current = false; - }); - } - }, - [root, stickyHeaders] - ); - - const scrollHandler = useCallback((e) => { - scrollTop.current = e.target.scrollTop; - }, []); - - useEffect(() => { - const { current: rootEl } = root; - if (rootEl) { - rootEl.addEventListener("scroll", scrollHandler); - } - - return () => { - if (rootEl) { - rootEl.removeEventListener("scroll", scrollHandler); - } - }; - }, [root, scrollHandler]); - - useLayoutEffect(() => { - if ( - highlightedIdx !== -1 && - rootScrollHeight.current > rootHeight.current - ) { - if (root.current) { - const item = root.current.querySelector(` - [data-idx='${highlightedIdx}'], - [aria-posinset='${highlightedIdx + 1}'] - `); - if (item === null) { - console.log( - "[useViewportTracking], is this virtualised ? we're going to have to know rowHeight" - ); - } else { - scrollIntoView(item); - } - } - } - }, [highlightedIdx, root, scrollIntoView]); - - useEffect(() => { - // onsole.log('TODO measure the sticky header') - }, [stickyHeaders]); - - const onResize = useCallback(({ height, scrollHeight }) => { - rootHeight.current = height; - rootScrollHeight.current = scrollHeight; - }, []); - - useResizeObserver(root, HeightOnly, onResize, true); - - return scrolling; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts b/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts deleted file mode 100644 index a79238d58..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/tree/useTree.ts +++ /dev/null @@ -1,134 +0,0 @@ -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, - TreeNodeSelectionHandler, - TreeSelection, - useSelection, -} from "./use-selection"; -import { useTreeKeyboardNavigation } from "./use-tree-keyboard-navigation"; - -const EMPTY_ARRAY: string[] = []; - -export interface TreeHookProps { - defaultSelected?: string[]; - groupSelection: GroupSelection; - onChange: TreeNodeSelectionHandler; - onHighlight?: (index: number) => void; - selected?: string[]; - selection: TreeSelection; - sourceWithIds: NormalisedTreeSourceNode[]; -} - -export const useTree = ({ - defaultSelected, - sourceWithIds, - onChange, - onHighlight: onHighlightProp, - selected: selectedProp, - selection, -}: TreeHookProps) => { - const lastSelection = useRef(EMPTY_ARRAY); - const dataHook = useHierarchicalData(sourceWithIds); - - const handleKeyboardNavigation = (evt: KeyboardEvent, nextIdx: number) => { - selectionHook.listHandlers.onKeyboardNavigation?.(evt, nextIdx); - }; - - const { highlightedIdx, ...keyboardHook } = useKeyboardNavigation({ - treeNodes: dataHook.indexPositions, - onHighlight: onHighlightProp, - onKeyboardNavigation: handleKeyboardNavigation, - selected: lastSelection.current, - }); - - const collapsibleHook = useCollapsibleGroups({ - collapsibleHeaders: true, - highlightedIdx, - treeNodes: dataHook.indexPositions, - setVisibleData: dataHook.setData, - source: dataHook.data, - }); - - const selectionHook = useSelection({ - defaultSelected, - highlightedIdx, - treeNodes: dataHook.indexPositions, - onChange, - selected: selectedProp, - selection, - }); - - const treeNavigationHook = useTreeKeyboardNavigation({ - source: dataHook.data, - highlightedIdx, - hiliteItemAtIndex: keyboardHook.hiliteItemAtIndex, - indexPositions: dataHook.indexPositions, - }); - - const handleClick = useCallback( - (evt) => { - collapsibleHook.listItemHandlers?.onClick(evt); - if (!evt.defaultPrevented) { - selectionHook.listItemHandlers?.onClick?.(evt); - } - }, - [collapsibleHook, selectionHook], - ); - - const handleKeyDown = useCallback( - (evt) => { - keyboardHook.listProps.onKeyDown?.(evt); - if (!evt.defaultPrevented) { - selectionHook.listHandlers.onKeyDown?.(evt); - } - if (!evt.defaultPrevented) { - collapsibleHook.listHandlers.onKeyDown?.(evt); - } - if (!evt.defaultPrevented) { - treeNavigationHook.listHandlers.onKeyDown?.(evt); - } - }, - [ - collapsibleHook.listHandlers, - keyboardHook.listProps, - selectionHook.listHandlers, - treeNavigationHook.listHandlers, - ], - ); - - const getActiveDescendant = () => - highlightedIdx === undefined || highlightedIdx === -1 - ? undefined - : dataHook.indexPositions[highlightedIdx]?.id; - - // We need this on reEntry for navigation hook to handle focus - lastSelection.current = selectionHook.selected; - - const listProps = { - "aria-activedescendant": getActiveDescendant(), - onBlur: keyboardHook.listProps.onBlur, - onFocus: keyboardHook.listProps.onFocus, - onKeyDown: handleKeyDown, - onMouseDownCapture: keyboardHook.listProps.onMouseDownCapture, - onMouseLeave: keyboardHook.listProps.onMouseLeave, - onMouseMove: keyboardHook.listProps.onMouseMove, - }; - - const listItemHandlers = { - onClick: handleClick, - }; - - return { - focusVisible: keyboardHook.focusVisible, - highlightedIdx, - hiliteItemAtIndex: keyboardHook.hiliteItemAtIndex, - listProps, - listItemHandlers, - selected: selectionHook.selected, - visibleData: dataHook.data, - }; -}; diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index ceb0f23ef..1ad6b65d9 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -329,11 +329,15 @@ export const flattenColumnGroup = ( } }; -export function extractGroupColumn( - columns: RuntimeColumnDescriptor[], - groupBy?: VuuGroupBy, +export function extractGroupColumn({ + availableWidth, + columns, + groupBy, confirmed = true, -): [GroupColumnDescriptor | null, RuntimeColumnDescriptor[]] { +}: ColumnGroupProps): [ + GroupColumnDescriptor | null, + RuntimeColumnDescriptor[], +] { if (groupBy && groupBy.length > 0) { const flattenedColumns = flattenColumnGroup(columns); // Note: groupedColumns will be in column order, not groupBy order @@ -362,6 +366,9 @@ export function extractGroupColumn( )} `, ); } + + const groupOnly = rest.length === 0; + const groupCount = groupBy.length; const groupCols: RuntimeColumnDescriptor[] = groupBy.map((name, idx) => { // Keep the cols in same order defined on groupBy @@ -374,6 +381,13 @@ export function extractGroupColumn( }; }); + const width = groupOnly + ? availableWidth + : Math.min( + availableWidth, + groupCols.map((c) => c.width).reduce((a, b) => a + b) + 100, + ); + const groupCol = { ariaColIndex: 1, columns: groupCols, @@ -381,7 +395,7 @@ export function extractGroupColumn( isGroup: true, groupConfirmed: confirmed, name: "group-col", - width: groupCols.map((c) => c.width).reduce((a, b) => a + b) + 100, + width, } as GroupColumnDescriptor; const withAdjustedAriaIndex: RuntimeColumnDescriptor[] = []; @@ -574,24 +588,23 @@ export const setAggregations = ( .concat({ column: column.name, aggType }); }; -export const applyGroupByToColumns = ( - columns: RuntimeColumnDescriptor[], - groupBy: VuuGroupBy, - confirmed = true, -) => { - if (groupBy.length) { - const [groupColumn, nonGroupedColumns] = extractGroupColumn( - columns, - groupBy, - confirmed, - ); +export type ColumnGroupProps = { + columns: RuntimeColumnDescriptor[]; + groupBy: VuuGroupBy; + confirmed?: boolean; + availableWidth: number; +}; + +export const applyGroupByToColumns = (props: ColumnGroupProps) => { + if (props.groupBy.length) { + const [groupColumn, nonGroupedColumns] = extractGroupColumn(props); if (groupColumn) { return [groupColumn as RuntimeColumnDescriptor].concat(nonGroupedColumns); } - } else if (columns[0]?.isGroup) { - return flattenColumnGroup(columns); + } else if (props.columns[0]?.isGroup) { + return flattenColumnGroup(props.columns); } - return columns; + return props.columns; }; export const applySortToColumns = ( diff --git a/vuu-ui/packages/vuu-utils/src/selection-utils.ts b/vuu-ui/packages/vuu-utils/src/selection-utils.ts index 5dd97d3fc..a13f3c2d2 100644 --- a/vuu-ui/packages/vuu-utils/src/selection-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/selection-utils.ts @@ -35,7 +35,7 @@ export const deselectItem = ( selected: Selection, itemIndex: number, rangeSelect: boolean, - keepExistingSelection = false + keepExistingSelection = false, ): Selection => { const singleSelect = selectionModel === "single"; const multiSelect = @@ -53,7 +53,7 @@ export const deselectItem = ( const newSelectedFillsGapOrExtends = ( selection: Selection, - itemIndex: number + itemIndex: number, ): boolean => { for (let i = 0; i < selection.length; i++) { const item = selection[i]; @@ -74,7 +74,7 @@ const newSelectedFillsGapOrExtends = ( const fillGapOrExtendSelection = ( selection: Selection, - itemIndex: number + itemIndex: number, ): Selection => { for (let i = 0; i < selection.length; i++) { const item = selection[i]; @@ -137,13 +137,15 @@ export const selectItem = ( itemIndex: number, rangeSelect: boolean, keepExistingSelection = false, - activeItemIndex = -1 + activeItemIndex = -1, ): Selection => { const singleSelect = selectionModel === "single"; const multiSelect = selectionModel === "extended" || selectionModel === "checkbox"; const actsLikeSingleSelect = - singleSelect || (multiSelect && !keepExistingSelection && !rangeSelect); + singleSelect || + (multiSelect && !keepExistingSelection && !rangeSelect) || + (rangeSelect && activeItemIndex === -1); if (selectionModel === "none") { return NO_SELECTION; @@ -229,7 +231,7 @@ const mergeRanges = (r1: RangeTuple, r2: RangeTuple): RangeTuple => [ const includedInRange = ( selectedItem: SelectionItem | undefined, - index: number + index: number, ) => { if (typeof selectedItem === "undefined" || typeof selectedItem === "number") { return false; @@ -251,7 +253,7 @@ const LAST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.Last; */ export const getSelectionStatus = ( selected: Selection, - itemIndex: number + itemIndex: number, ): number => { for (const item of selected) { if (typeof item === "number") { @@ -331,13 +333,13 @@ export type SelectionDiff = { removed: SelectionItem[]; }; -export const selectionCount = (selected: Selection) => { +export const selectionCount = (selected: Selection = NO_SELECTION) => { let count = selected.length; - for (const selectionItem of selected){ - if (Array.isArray(selectionItem)){ + for (const selectionItem of selected) { + if (Array.isArray(selectionItem)) { const [from, to] = selectionItem; - count += (to - (from + 1)); + count += to - (from + 1); } } return count; -} +}; diff --git a/vuu-ui/packages/vuu-utils/test/selection-utils.test.ts b/vuu-ui/packages/vuu-utils/test/selection-utils.test.ts index ea35a4023..8c260903a 100644 --- a/vuu-ui/packages/vuu-utils/test/selection-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/selection-utils.test.ts @@ -21,7 +21,7 @@ describe("selection-utils", () => { it("returns True when rowIndex is included in selection", () => { expect(getSelectionStatus([1], 1) & RowSelected.True).toEqual( - RowSelected.True + RowSelected.True, ); expect(getSelectionStatus([[0, 3]], 1)).toEqual(RowSelected.True); }); @@ -59,6 +59,9 @@ describe("selection-utils", () => { [2, 7], ]); }); + it("acts like single select if activeItem index not set", () => { + expect(selectItem("extended", [2], 9, true, false, -1)).toEqual([9]); + }); it("creates an additional range from activeItem", () => { expect(selectItem("extended", [[2, 5], 7], 9, true, false, 7)).toEqual([ [2, 5], @@ -106,7 +109,7 @@ describe("selection-utils", () => { [2, 4], ]); expect(selectItem("extended", [0, 2, 4, 7], 3, false, true, 3)).toEqual( - [0, [2, 4], 7] + [0, [2, 4], 7], ); }); }); @@ -124,7 +127,7 @@ describe("selection-utils", () => { describe("extended selection mode", () => { it("deselects a single item, no range required, no preserve selection", () => { expect(deselectItem("extended", [2, 5, 7], 7, false, false)).toEqual( - [] + [], ); }); it("deselects a single item, no range required, preserve selection", () => { diff --git a/vuu-ui/showcase/src/examples/UiControls/Tree.data.ts b/vuu-ui/showcase/src/examples/DataTable/Tree.data.ts similarity index 100% rename from vuu-ui/showcase/src/examples/UiControls/Tree.data.ts rename to vuu-ui/showcase/src/examples/DataTable/Tree.data.ts diff --git a/vuu-ui/showcase/src/examples/DataTable/TreeTable.examples.tsx b/vuu-ui/showcase/src/examples/DataTable/TreeTable.examples.tsx new file mode 100644 index 000000000..57f39f49d --- /dev/null +++ b/vuu-ui/showcase/src/examples/DataTable/TreeTable.examples.tsx @@ -0,0 +1,47 @@ +import { TreeTable } from "@finos/vuu-datatable"; + +import showcaseData from "./Tree.data"; + +let displaySequence = 1; + +export const ShowcaseTree = () => { + return ( + + ); +}; +ShowcaseTree.displaySequence = displaySequence++; + +export const ShowcaseTreeSelected = () => { + return ( +
    +
    + +
    +
    + ); +}; +ShowcaseTreeSelected.displaySequence = displaySequence++; + +export const ShowcaseTreeSelectedAutoReveal = () => { + console.log({ showcaseData }); + + return ( + + ); +}; +ShowcaseTreeSelectedAutoReveal.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/DataTable/index.ts b/vuu-ui/showcase/src/examples/DataTable/index.ts index a094a380c..9b43d276e 100644 --- a/vuu-ui/showcase/src/examples/DataTable/index.ts +++ b/vuu-ui/showcase/src/examples/DataTable/index.ts @@ -1,2 +1,3 @@ export * as FilterTable from "./FilterTable.examples"; export * as JsonTable from "./JsonTable.examples"; +export * as TreeTable from "./TreeTable.examples"; diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/List/List.data.ts b/vuu-ui/showcase/src/examples/ShowcaseControls/List/List.data.ts deleted file mode 100644 index 94caff276..000000000 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/List/List.data.ts +++ /dev/null @@ -1,209 +0,0 @@ -export const usa_states = [ - "Alabama", - "Alaska", - "Arizona", - "Arkansas", - "California", - "Colorado", - "Connecticut", - "Delaware", - "Florida", - "Georgia", - "Hawaii", - "Idaho", - "Illinois", - "Indiana", - "Iowa", - "Kansas", - "Kentucky", - "Louisiana", - "Maine", - "Maryland", - "Massachusetts", - "Michigan", - "Minnesota", - "Mississippi", - "Missouri", - "Montana", - "Nebraska", - "Nevada", - "New Hampshire", - "New Jersey", - "New Mexico", - "New York", - "North Carolina", - "North Dakota", - "Ohio", - "Oklahoma", - "Oregon", - "Pennsylvania", - "Rhode Island", - "South Carolina", - "South Dakota", - "Tennessee", - "Texas", - "Utah", - "Vermont", - "Virginia", - "Washington", - "West Virginia", - "Wisconsin", - "Wyoming", -]; - -const usa_capital_cities: Record = { - Alabama: "Montgomery", - Alaska: "Juneau", - Arizona: "Phoenix", - Arkansas: "Little Rock", - California: "Sacremento", - Colorado: "Denver", - Connecticut: "Hartford", - Delaware: "Dover", - Florida: "Tallahassee", - Georgia: "Atlanta", - Hawaii: "Honululu", - Idaho: "Boise", - Illinois: "Springfield", - Indiana: "Indianapolis", - Iowa: "Des Moines", - Kansas: "Topeka", - Kentucky: "Frankfort", - Louisiana: "Baton Rouge", - Maine: "Augusta", - Maryland: "Annapolis", - Massachusetts: "Boston", - Michigan: "Lansing", - Minnesota: "Saint Paul", - Mississippi: "Jackson", - Missouri: "Jefferson City", - Montana: "Helena", - Nebraska: "Lincoln", - Nevada: "Carson City", - "New Hampshire": "Concord", - "New Jersey": "Trenton", - "New Mexico": "Santa Fe", - "New York": "Albany", - "North Carolina": "Raleigh", - "North Dakota": "Bismarck", - Ohio: "Columbus", - Oklahoma: "Oklahoma City", - Oregon: "Salem", - Pennsylvania: "Harrisburg", - "Rhode Island": "Providence", - "South Carolina": "Columbia", - "South Dakota": "Pierre", - Tennessee: "Nashville", - Texas: "Austin", - Utah: "Salt Lake City", - Vermont: "Montpelier", - Virginia: "Richmond", - Washington: "Olympia", - "West Virginia": "Charleston", - Wisconsin: "Madison", - Wyoming: "Cheyenne", -}; - -const usa_cities: Record = { - Alabama: ["Birmingham", "Dothan", "Huntsville", "Mobile", "Montgomery"], - Alaska: [ - "Anchorage", - "Juneau", - "Nightmute", - "Sitka", - "Unalaska", - "Valdez", - "Wrangell", - ], - Arizona: [ - "Buckeye", - "Case Grande", - "Eloy", - "Tuscson", - "Goodyear", - "Marana", - "Mesa", - "Peoria", - "Phoenix", - "Scottsdale", - "Sierra Vista", - "Surprise", - "Tucson", - "Yuma", - ], - Arkansas: ["Jonesboro", "Little Rock"], - California: [ - "Bakersfield", - "California City", - "Fresno", - "Lancaster", - "Los Angeles", - "Palm Springs", - "Palmdale", - "Riverside", - "Sacramento", - "San Diego", - "San Jose", - ], -}; - -const getCities = (state: string) => { - if (usa_cities[state]) { - return usa_cities[state].map((city: string) => ({ - label: city, - })); - } else { - return []; - } -}; - -export const usa_states_cities = usa_states.map((state) => ({ - label: state, - childNodes: [ - { label: usa_capital_cities[state], capital: true }, - ...getCities(state), - ], -})); - -type WithLabel = { label: string }; -type T = string | WithLabel; - -const bySelfOrLabel = (a: T, b: T) => { - const a1 = (a as WithLabel)?.label ?? a; - const b1 = (b as WithLabel)?.label ?? b; - - return a1 === b1 ? 0 : a1 > b1 ? 1 : -1; -}; - -export const groupByInitialLetter = ( - list: string[] | WithLabel[], - groupMode = "headers-only" -) => { - const sortedList = list.slice().sort(bySelfOrLabel); - const result: unknown[] = []; - const header = true; - let char; - let items = result; - - for (const item of sortedList) { - const label = (item as WithLabel)?.label ?? item; - if (char !== label[0]) { - if (groupMode === "headers-only") { - items.push({ label: label[0], header }); - } else { - result.push({ - label: label[0], - childNodes: (items = []), - }); - } - char = label[0]; - } - items.push(item); - } - - return result; -}; - -export const random_1000 = new Array(1000) - .fill(0) - .map((_, i) => `Item ${i + 1}`); diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts deleted file mode 100644 index 6fb31a953..000000000 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.data.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type { TreeSourceNode } from "@finos/vuu-utils"; - -export const folderData: TreeSourceNode[] = [ - // prettier-ignore - { - id: "2018", - label: "2018", - icon: "folder", - childNodes: [ - { - id: "Q1", - label: "Q1", - childNodes: [ - { - id: "January", - label: "January", - childNodes: [ - { id: "child-1", label: "Child 1", icon: "folder" }, - { id: "child-2", label: "Child 2", icon: "folder" }, - { id: "child-3", label: "Child 3", icon: "folder" }, - { id: "child-4", - label: "Child 4", - icon: "folder", - }, - { id: "child-5", - label: "Child 5", - icon: "folder", - }, - { id: "child-6", - label: "Child 6", - icon: "folder", - }, - { id: "child-7", - label: "Child 7", - icon: "folder", - }, - { - id: "child-8", - label: "Child 8", - icon: "folder", - }, - { - id: "child-9", - label: "Child 9", - icon: "folder", - }, - { - id: "child-10", - label: "Child 10", - icon: "folder", - }, - { - id: "child-11", - label: "Child 11", - icon: "folder", - }, - { - id: "child-12", - label: "Child 12", - icon: "folder", - }, - { - id: "child-13", - label: "Child 13", - icon: "folder", - }, - { - id: "child-14", - label: "Child 14", - icon: "folder", - }, - { - id: "child-15", - label: "Child 15", - icon: "folder", - }, - { - id: "child-16", - label: "Child 16", - icon: "folder", - }, - { - id: "child-17", - label: "Child 17", - icon: "folder", - }, - { - id: "child-18", - label: "Child 18", - icon: "folder", - }, - { - id: "child-19", - label: "Child 19", - icon: "folder", - }, - { - id: "child-20", - label: "Child 20", - icon: "folder", - }, - { - id: "child-21", - label: "Child 21", - icon: "folder", - }, - { - id: "child-22", - label: "Child 22", - icon: "folder", - }, - { id: "child-23", label: "Child 23", icon: "folder" }, - { id: "child-24", label: "Child 24", icon: "folder" }, - { id: "child-25", label: "Child 25", icon: "folder" }, - { id: "child-26", label: "Child 26", icon: "folder" }, - { id: "child-27", label: "Child 27", icon: "folder" }, - { id: "child-28", label: "Child 28", icon: "folder" }, - { id: "child-29", label: "Child 29", icon: "folder" }, - { id: "child-30", label: "Child 30", icon: "folder" }, - { id: "child-31", label: "Child 31", icon: "folder" }, - { id: "child-32", label: "Child 32", icon: "folder" }, - { id: "child-33", label: "Child 33", icon: "folder" }, - { id: "child-34", label: "Child 34", icon: "folder" }, - { id: "child-35", label: "Child 35", icon: "folder" }, - { id: "child-36", label: "Child 36", icon: "folder" }, - { id: "child-37", label: "Child 37", icon: "folder" }, - { id: "child-38", label: "Child 38", icon: "folder" }, - { id: "child-39", label: "Child 39", icon: "folder" }, - { id: "child-40", label: "Child 40", icon: "folder" }, - { id: "child-41", label: "Child 41", icon: "folder" }, - { id: "child-42", label: "Child 42", icon: "folder" }, - { id: "child-43", label: "Child 43", icon: "folder" }, - { id: "child-44", label: "Child 44", icon: "folder" }, - { id: "child-45", label: "Child 45", icon: "folder" }, - { id: "child-46", label: "Child 46", icon: "folder" }, - { id: "child-47", label: "Child 47", icon: "folder" }, - { id: "child-48", label: "Child 48", icon: "folder" }, - { id: "child-49", label: "Child 49", icon: "folder" }, - { id: "child-50", label: "Child 50", icon: "folder" }, - { id: "child-51", label: "Child 51", icon: "folder" }, - { id: "child-52", label: "Child 52", icon: "folder" }, - { id: "child-53", label: "Child 53", icon: "folder" }, - { id: "child-54", label: "Child 54", icon: "folder" }, - { id: "child-55", label: "Child 55", icon: "folder" }, - { id: "child-56", label: "Child 56", icon: "folder" }, - { id: "child-57", label: "Child 57", icon: "folder" }, - { id: "child-58", label: "Child 58", icon: "folder" }, - { id: "child-59", label: "Child 59", icon: "folder" }, - { id: "child-60", label: "Child 60", icon: "folder" }, - { id: "child-61", label: "Child 61", icon: "folder" }, - { id: "child-62", label: "Child 62", icon: "folder" }, - { id: "child-63", label: "Child 63", icon: "folder" }, - { id: "child-64", label: "Child 64", icon: "folder" }, - { id: "child-65", label: "Child 65", icon: "folder" }, - { id: "child-66", label: "Child 66", icon: "folder" }, - { id: "child-67", label: "Child 67", icon: "folder" }, - { id: "child-68", label: "Child 68", icon: "folder" }, - { id: "child-69", label: "Child 69", icon: "folder" }, - { id: "child-70", label: "Child 70", icon: "folder" }, - ], - icon: "folder", - }, - ], - icon: "folder", - }, - { - id: "Q2", - label: "Q2", - childNodes: [ - { - id: "Q2-1", - label: "Q2-1", - childNodes: [ - { id: "Q2-1-1", - label: "Q2-1-1", - childNodes: [ - { - id : "Q2-1-1-1", - label: "Q2-1-1-1", - icon: "folder", - }, - ], - icon: "folder", - }, - ], - icon: "folder", - }, - { - id: "Q2-2", - label: "Q2-2", - childNodes: [ - { - id: "Q2-2-1", - label: "Q2-2-1", - childNodes: [ - { - id: "Q2-2-1-1", - label: "Q2-2-1-1", - icon: "folder", - }, - ], - icon: "folder", - }, - ], - icon: "folder", - }, - ], - icon: "folder", - }, - { - id: "Q3", - label: "Q3", - icon: "folder", - }, - { - id: "Q4", - label: "Q4", - icon: "folder", - }, - ], - }, - { - id: "2019", - label: "2019", - childNodes: [ - { - id: "Q1", - label: "Q1", - icon: "folder", - }, - ], - icon: "folder", - }, -]; diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx b/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx deleted file mode 100644 index e066376fa..000000000 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/Tree.examples.tsx +++ /dev/null @@ -1,155 +0,0 @@ -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; - -export const SimpleTree = () => { - const handleChange = (selected: TreeSourceNode[]) => { - console.log(`selected ${selected.join(",")}`); - }; - return ( -
    - -
    - -
    - -
    - ); -}; - -SimpleTree.displaySequence = displaySequence++; - -const iconTreeStyle = ` - .arrow-toggle { - --hwTree-toggle-collapse: var(--svg-triangle-right); - --hwTree-toggle-expand: var(--svg-triangle-right); - --hwTree-node-expanded-transform: rotate(45deg) translate(1px, 1px); - } -`; - -export const SimpleTreeIcons = () => { - const handleChange = (selected: TreeSourceNode[]) => { - console.log(`selected ${selected.join(",")}`); - }; - return ( -
    - -
    - - -
    - -
    - ); -}; -SimpleTreeIcons.displaySequence = displaySequence++; - -export const DragDropTreeIcons = () => { - const handleChange = (selected: TreeSourceNode[]) => { - console.log(`selected ${selected.join(",")}`); - }; - return ( -
    - -
    - - -
    - -
    - ); -}; - -DragDropTreeIcons.displaySequence = displaySequence++; - -export const RevealSelected = () => { - const handleChange = (selected: TreeSourceNode[]) => { - console.log(`selected ${selected.join(",")}`); - }; - - const [, source] = useItemsWithIds(folderData); - console.log({ source }); - - console.log({ source }); - return ( -
    - -
    - - -
    - -
    - ); -}; - -RevealSelected.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/ShowcaseControls/index.ts b/vuu-ui/showcase/src/examples/ShowcaseControls/index.ts deleted file mode 100644 index fc5df535b..000000000 --- a/vuu-ui/showcase/src/examples/ShowcaseControls/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Tree from "./Tree.examples"; diff --git a/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx index 23ec087a4..72bde03ea 100644 --- a/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx @@ -3,9 +3,9 @@ import { Table, TableProps } from "@finos/vuu-table"; import { useMemo } from "react"; import "./Table.examples.css"; -import { TableSchema } from "@finos/vuu-data-types"; +import { SelectionChangeHandler, TableSchema } from "@finos/vuu-data-types"; import { ColumnLayout, TableConfig } from "@finos/vuu-table-types"; -import { useDataSource } from "@finos/vuu-utils"; +import { toColumnName, useDataSource } from "@finos/vuu-utils"; let displaySequence = 1; @@ -41,8 +41,14 @@ const DataTableTemplate = ({ }, [configProp, schema]); const dataSource = useMemo(() => { - return dataSourceProp ?? new VuuDataSource({ table: schema.table }); - }, [VuuDataSource, dataSourceProp, schema.table]); + return ( + dataSourceProp ?? + new VuuDataSource({ + columns: schema.columns.map(toColumnName), + table: schema.table, + }) + ); + }, [VuuDataSource, dataSourceProp, schema]); return (
    { ); }; CellBlockRowSelection.displaySequence = displaySequence++; + +export const PreSelectedRowByIndex = () => { + const handleSelectionChange: SelectionChangeHandler = (selection) => { + console.log(`selection changed ${JSON.stringify(selection)}`); + }; + return ( + + + + ); +}; +PreSelectedRowByIndex.displaySequence = displaySequence++; + +export const PreSelectedRowsByIndex = () => { + return ( + + + + ); +}; +PreSelectedRowsByIndex.displaySequence = displaySequence++; + +export const PreSelectedRangeByIndex = () => { + return ( + + + + ); +}; +PreSelectedRangeByIndex.displaySequence = displaySequence++; + +export const PreSelectedRowByKey = () => { + const handleSelectionChange: SelectionChangeHandler = (selection) => { + console.log(`selection changed ${JSON.stringify(selection)}`); + }; + return ( + + + + ); +}; +PreSelectedRowByKey.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx deleted file mode 100644 index 0696c393a..000000000 --- a/vuu-ui/showcase/src/examples/UiControls/Tree.examples.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 ( -
    - - {/*
    - -
    */} -
    - -
    -
    - ); -}; -ShowcaseTree.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index ac6a10dea..5eee2b610 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -6,7 +6,6 @@ export * as TablePicker from "./TablePicker.examples"; export * as InstrumentSearch from "./InstrumentSearch.examples"; export * as List from "./List.examples"; export * as OverflowContainer from "./OverflowContainer.examples"; -export * as Tree from "./Tree.examples"; export * as SplitButton from "./SplitButton.examples"; export * as Tabstrip from "./Tabstrip.examples"; export * as TabsNext from "./TabsNext.examples"; diff --git a/vuu-ui/showcase/src/examples/index.ts b/vuu-ui/showcase/src/examples/index.ts index 51d3995f0..ac5d71375 100644 --- a/vuu-ui/showcase/src/examples/index.ts +++ b/vuu-ui/showcase/src/examples/index.ts @@ -9,7 +9,6 @@ export * as Layout from "./Layout"; export * as Popups from "./Popups"; export * as salt from "./salt"; export * as Shell from "./Shell"; -export * as ShowcaseControls from "./ShowcaseControls"; export * as Table from "./Table"; export * as UiControls from "./UiControls"; export * as VUU from "./VUU"; diff --git a/vuu-ui/showcase/vite.config.js.timestamp-1731102872716-3d023f7f24e4b.mjs b/vuu-ui/showcase/vite.config.js.timestamp-1731102872716-3d023f7f24e4b.mjs new file mode 100644 index 000000000..9d960fae2 --- /dev/null +++ b/vuu-ui/showcase/vite.config.js.timestamp-1731102872716-3d023f7f24e4b.mjs @@ -0,0 +1,76 @@ +// vite.config.js +import { defineConfig } from "file:///Users/steve/github/finos/vuu/vuu-ui/node_modules/vite/dist/node/index.js"; + +// ../tools/vite-plugin-inline-css/src/index.ts +import { createFilter } from "file:///Users/steve/github/finos/vuu/vuu-ui/node_modules/vite/dist/node/index.js"; +import MagicString from "file:///Users/steve/github/finos/vuu/vuu-ui/node_modules/magic-string/dist/magic-string.es.mjs"; +function cssInline(options = {}) { + const { + exclude = ["**/**.stories.tsx"], + include = [ + "**/packages/vuu-datatable/**/*.{tsx,jsx}", + "**/packages/vuu-data-react/**/*.{tsx,jsx}", + "**/packages/vuu-filters/**/*.{tsx,jsx}", + "**/packages/vuu-layout/**/*.{tsx,jsx}", + "**/packages/vuu-popups/**/*.{tsx,jsx}", + "**/packages/vuu-shell/**/*.{tsx,jsx}", + "**/packages/vuu-table/**/*.{tsx,jsx}", + "**/packages/vuu-table-extras/**/*.{tsx,jsx}", + "**/packages/vuu-ui-controls/**/*.{tsx,jsx}" + ] + } = options; + const filter = createFilter(include, exclude); + return { + name: "css-inline-plugin", + enforce: "pre", + transform(src, id) { + if (filter(id)) { + const s = new MagicString(src); + s.replaceAll('.css";', '.css?inline";'); + return { + code: s.toString(), + map: s.generateMap({ hires: true, source: id }) + }; + } + } + }; +} + +// vite.config.js +var vite_config_default = defineConfig({ + build: { + minify: false, + sourcemap: true, + target: "esnext" + }, + define: { + "process.env.NODE_DEBUG": false, + "process.env.LOCAL": true, + "process.env.LAYOUT_BASE_URL": `"http://127.0.0.1:8081/api"` + }, + esbuild: { + jsx: `automatic`, + target: "esnext" + }, + plugins: [cssInline()], + server: { + proxy: { + "/api/authn": { + target: "https://localhost:8443", + secure: false + } + } + }, + preview: { + proxy: { + "/api/authn": { + target: "https://localhost:8443", + secure: false + } + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiLCAiLi4vdG9vbHMvdml0ZS1wbHVnaW4taW5saW5lLWNzcy9zcmMvaW5kZXgudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc3RldmUvZ2l0aHViL2Zpbm9zL3Z1dS92dXUtdWkvc2hvd2Nhc2VcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zdGV2ZS9naXRodWIvZmlub3MvdnV1L3Z1dS11aS9zaG93Y2FzZS92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc3RldmUvZ2l0aHViL2Zpbm9zL3Z1dS92dXUtdWkvc2hvd2Nhc2Uvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IHsgY3NzSW5saW5lIH0gZnJvbSBcIi4uL3Rvb2xzL3ZpdGUtcGx1Z2luLWlubGluZS1jc3Mvc3JjXCI7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIGJ1aWxkOiB7XG4gICAgbWluaWZ5OiBmYWxzZSxcbiAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgdGFyZ2V0OiBcImVzbmV4dFwiLFxuICB9LFxuICBkZWZpbmU6IHtcbiAgICBcInByb2Nlc3MuZW52Lk5PREVfREVCVUdcIjogZmFsc2UsXG4gICAgXCJwcm9jZXNzLmVudi5MT0NBTFwiOiB0cnVlLFxuICAgIFwicHJvY2Vzcy5lbnYuTEFZT1VUX0JBU0VfVVJMXCI6IGBcImh0dHA6Ly8xMjcuMC4wLjE6ODA4MS9hcGlcImAsXG4gIH0sXG4gIGVzYnVpbGQ6IHtcbiAgICBqc3g6IGBhdXRvbWF0aWNgLFxuICAgIHRhcmdldDogXCJlc25leHRcIixcbiAgfSxcbiAgcGx1Z2luczogW2Nzc0lubGluZSgpXSxcbiAgc2VydmVyOiB7XG4gICAgcHJveHk6IHtcbiAgICAgIFwiL2FwaS9hdXRoblwiOiB7XG4gICAgICAgIHRhcmdldDogXCJodHRwczovL2xvY2FsaG9zdDo4NDQzXCIsXG4gICAgICAgIHNlY3VyZTogZmFsc2UsXG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG4gIHByZXZpZXc6IHtcbiAgICBwcm94eToge1xuICAgICAgXCIvYXBpL2F1dGhuXCI6IHtcbiAgICAgICAgdGFyZ2V0OiBcImh0dHBzOi8vbG9jYWxob3N0Ojg0NDNcIixcbiAgICAgICAgc2VjdXJlOiBmYWxzZSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvc3RldmUvZ2l0aHViL2Zpbm9zL3Z1dS92dXUtdWkvdG9vbHMvdml0ZS1wbHVnaW4taW5saW5lLWNzcy9zcmNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9zdGV2ZS9naXRodWIvZmlub3MvdnV1L3Z1dS11aS90b29scy92aXRlLXBsdWdpbi1pbmxpbmUtY3NzL3NyYy9pbmRleC50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvc3RldmUvZ2l0aHViL2Zpbm9zL3Z1dS92dXUtdWkvdG9vbHMvdml0ZS1wbHVnaW4taW5saW5lLWNzcy9zcmMvaW5kZXgudHNcIjtpbXBvcnQgdHlwZSB7IFBsdWdpbiB9IGZyb20gXCJ2aXRlXCI7XG5pbXBvcnQgeyBjcmVhdGVGaWx0ZXIgfSBmcm9tIFwidml0ZVwiO1xuaW1wb3J0IE1hZ2ljU3RyaW5nIGZyb20gXCJtYWdpYy1zdHJpbmdcIjtcblxuZXhwb3J0IGludGVyZmFjZSBPcHRpb25zIHtcbiAgLyoqIEdsb2IgcGF0dGVybnMgdG8gaWdub3JlICovXG4gIGV4Y2x1ZGU/OiBzdHJpbmdbXTtcbiAgLyoqIEdsb2IgcGF0dGVybnMgdG8gaW5jbHVkZS4gZGVmYXVsdHMgdG8gdHN8dHN4ICovXG4gIGluY2x1ZGU/OiBzdHJpbmdbXTtcbn1cblxuLy8gVGhpcyBwbHVnaW4gYWRkcyBcIj9pbmxpbmVcIiB0byBlYWNoIGNzcyBpbXBvcnQgd2l0aGluIG91ciBjb21wb25lbnRzIHRvIGRpc2FibGVcbi8vIHZpdGUncyBvd24gc3R5bGUgaW5qZWN0aW9uIHVzZWQgaW4gc3Rvcnlib29rXG5leHBvcnQgZnVuY3Rpb24gY3NzSW5saW5lKG9wdGlvbnM6IE9wdGlvbnMgPSB7fSk6IFBsdWdpbiB7XG4gIGNvbnN0IHtcbiAgICBleGNsdWRlID0gW1wiKiovKiouc3Rvcmllcy50c3hcIl0sXG4gICAgaW5jbHVkZSA9IFtcbiAgICAgIFwiKiovcGFja2FnZXMvdnV1LWRhdGF0YWJsZS8qKi8qLnt0c3gsanN4fVwiLFxuICAgICAgXCIqKi9wYWNrYWdlcy92dXUtZGF0YS1yZWFjdC8qKi8qLnt0c3gsanN4fVwiLFxuICAgICAgXCIqKi9wYWNrYWdlcy92dXUtZmlsdGVycy8qKi8qLnt0c3gsanN4fVwiLFxuICAgICAgXCIqKi9wYWNrYWdlcy92dXUtbGF5b3V0LyoqLyoue3RzeCxqc3h9XCIsXG4gICAgICBcIioqL3BhY2thZ2VzL3Z1dS1wb3B1cHMvKiovKi57dHN4LGpzeH1cIixcbiAgICAgIFwiKiovcGFja2FnZXMvdnV1LXNoZWxsLyoqLyoue3RzeCxqc3h9XCIsXG4gICAgICBcIioqL3BhY2thZ2VzL3Z1dS10YWJsZS8qKi8qLnt0c3gsanN4fVwiLFxuICAgICAgXCIqKi9wYWNrYWdlcy92dXUtdGFibGUtZXh0cmFzLyoqLyoue3RzeCxqc3h9XCIsXG4gICAgICBcIioqL3BhY2thZ2VzL3Z1dS11aS1jb250cm9scy8qKi8qLnt0c3gsanN4fVwiLFxuICAgIF0sXG4gIH0gPSBvcHRpb25zO1xuICBjb25zdCBmaWx0ZXIgPSBjcmVhdGVGaWx0ZXIoaW5jbHVkZSwgZXhjbHVkZSk7XG5cbiAgcmV0dXJuIHtcbiAgICBuYW1lOiBcImNzcy1pbmxpbmUtcGx1Z2luXCIsXG4gICAgZW5mb3JjZTogXCJwcmVcIixcbiAgICB0cmFuc2Zvcm0oc3JjLCBpZCkge1xuICAgICAgaWYgKGZpbHRlcihpZCkpIHtcbiAgICAgICAgY29uc3QgcyA9IG5ldyBNYWdpY1N0cmluZyhzcmMpO1xuICAgICAgICBzLnJlcGxhY2VBbGwoJy5jc3NcIjsnLCAnLmNzcz9pbmxpbmVcIjsnKTtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICBjb2RlOiBzLnRvU3RyaW5nKCksXG4gICAgICAgICAgbWFwOiBzLmdlbmVyYXRlTWFwKHsgaGlyZXM6IHRydWUsIHNvdXJjZTogaWQgfSksXG4gICAgICAgIH07XG4gICAgICB9XG4gICAgfSxcbiAgfTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBeVQsU0FBUyxvQkFBb0I7OztBQ0N0VixTQUFTLG9CQUFvQjtBQUM3QixPQUFPLGlCQUFpQjtBQVdqQixTQUFTLFVBQVUsVUFBbUIsQ0FBQyxHQUFXO0FBQ3ZELFFBQU07QUFBQSxJQUNKLFVBQVUsQ0FBQyxtQkFBbUI7QUFBQSxJQUM5QixVQUFVO0FBQUEsTUFDUjtBQUFBLE1BQ0E7QUFBQSxNQUNBO0FBQUEsTUFDQTtBQUFBLE1BQ0E7QUFBQSxNQUNBO0FBQUEsTUFDQTtBQUFBLE1BQ0E7QUFBQSxNQUNBO0FBQUEsSUFDRjtBQUFBLEVBQ0YsSUFBSTtBQUNKLFFBQU0sU0FBUyxhQUFhLFNBQVMsT0FBTztBQUU1QyxTQUFPO0FBQUEsSUFDTCxNQUFNO0FBQUEsSUFDTixTQUFTO0FBQUEsSUFDVCxVQUFVLEtBQUssSUFBSTtBQUNqQixVQUFJLE9BQU8sRUFBRSxHQUFHO0FBQ2QsY0FBTSxJQUFJLElBQUksWUFBWSxHQUFHO0FBQzdCLFVBQUUsV0FBVyxVQUFVLGVBQWU7QUFDdEMsZUFBTztBQUFBLFVBQ0wsTUFBTSxFQUFFLFNBQVM7QUFBQSxVQUNqQixLQUFLLEVBQUUsWUFBWSxFQUFFLE9BQU8sTUFBTSxRQUFRLEdBQUcsQ0FBQztBQUFBLFFBQ2hEO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0Y7OztBRHpDQSxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixPQUFPO0FBQUEsSUFDTCxRQUFRO0FBQUEsSUFDUixXQUFXO0FBQUEsSUFDWCxRQUFRO0FBQUEsRUFDVjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sMEJBQTBCO0FBQUEsSUFDMUIscUJBQXFCO0FBQUEsSUFDckIsK0JBQStCO0FBQUEsRUFDakM7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNQLEtBQUs7QUFBQSxJQUNMLFFBQVE7QUFBQSxFQUNWO0FBQUEsRUFDQSxTQUFTLENBQUMsVUFBVSxDQUFDO0FBQUEsRUFDckIsUUFBUTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ0wsY0FBYztBQUFBLFFBQ1osUUFBUTtBQUFBLFFBQ1IsUUFBUTtBQUFBLE1BQ1Y7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsY0FBYztBQUFBLFFBQ1osUUFBUTtBQUFBLFFBQ1IsUUFBUTtBQUFBLE1BQ1Y7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== diff --git a/vuu-ui/tools/vuu-showcase/package.json b/vuu-ui/tools/vuu-showcase/package.json index d916d3e35..79a407573 100644 --- a/vuu-ui/tools/vuu-showcase/package.json +++ b/vuu-ui/tools/vuu-showcase/package.json @@ -8,8 +8,12 @@ "build": "node ../../scripts/run-build.mjs", "type-defs": "node ../../scripts/build-type-defs.mjs" }, - "devDependencies": {}, + "devDependencies": { + "@finos/vuu-data-types": "0.0.26", + "@finos/vuu-table-types": "0.0.26" + }, "dependencies": { + "@finos/vuu-datatable": "0.0.26", "@finos/vuu-icons": "0.0.26", "@finos/vuu-layout": "0.0.26", "@finos/vuu-theme": "0.0.26", diff --git a/vuu-ui/tools/vuu-showcase/src/App.tsx b/vuu-ui/tools/vuu-showcase/src/App.tsx index e046982fc..645c5b8d0 100644 --- a/vuu-ui/tools/vuu-showcase/src/App.tsx +++ b/vuu-ui/tools/vuu-showcase/src/App.tsx @@ -1,5 +1,7 @@ -import { Flexbox } from "@finos/vuu-layout"; -import { Tree } from "@finos/vuu-ui-controls"; +import { TreeTable } from "@finos/vuu-datatable"; +import { Flexbox, View } from "@finos/vuu-layout"; +import { ThemeSwitch } from "@finos/vuu-shell"; +import type { TableRowSelectHandler } from "@finos/vuu-table-types"; import type { Density, ThemeMode, TreeSourceNode } from "@finos/vuu-utils"; import { Button, @@ -11,9 +13,13 @@ import { import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { IFrame } from "./components"; -import { byDisplaySequence, ExamplesModule, loadTheme } from "./showcase-utils"; - -import { ThemeSwitch } from "@finos/vuu-shell"; +import { + ExamplesModule, + byDisplaySequence, + keyFromPath, + loadTheme, + pathFromKey, +} from "./showcase-utils"; import "./App.css"; @@ -83,7 +89,12 @@ export const App = ({ stories }: AppProps) => { // // TODO cache source in localStorage const source = useMemo(() => sourceFromImports(stories), [stories]); const { pathname } = useLocation(); - const handleChange = ([selected]: TreeSourceNode[]) => navigate(selected.id); + const handleChange: TableRowSelectHandler = (row) => { + if (row) { + const path = pathFromKey(row.key); + navigate(path); + } + }; const [themeIndex, setThemeIndex] = useState(2); const [themeModeIndex, setThemeModeIndex] = useState(0); const [densityIndex, setDensityIndex] = useState(0); @@ -129,15 +140,23 @@ export const App = ({ stories }: AppProps) => { Vuu Showcase - + + + { } }; +export const pathFromKey = (key: string) => key.slice(5).split("|").join("/"); +export const keyFromPath = (path: string) => + `$root${path.split("/").join("|")}`; + export const pathToExample = (path: string): [string[], string] => { const endOfImportPath = path.lastIndexOf("/"); const importPath =