From 11ecb4f985ce11f89261d638214f62910d980407 Mon Sep 17 00:00:00 2001 From: heswell Date: Tue, 12 Nov 2024 14:42:50 +0000 Subject: [PATCH 1/4] AppPattern example to show search with recents --- .../array-data-source/array-data-source.ts | 4 + vuu-ui/packages/vuu-icons/index.css | 12 + vuu-ui/packages/vuu-table-types/index.d.ts | 7 + vuu-ui/packages/vuu-table/src/Table.css | 10 +- vuu-ui/packages/vuu-table/src/Table.tsx | 13 + vuu-ui/packages/vuu-table/src/useTable.ts | 5 +- .../packages/vuu-table/src/useTableModel.ts | 4 + .../vuu-theme/css/components/button.css | 8 +- .../measured-container/MeasuredContainer.tsx | 3 +- .../useMeasuredContainer.ts | 6 +- vuu-ui/packages/vuu-utils/src/column-utils.ts | 90 ++--- .../vuu-utils/test/column-utils.test.ts | 39 ++- .../ClientSourcedTableColumn.examples.tsx | 312 ++++++++++++++++++ .../ClientTableColumnProvider.tsx | 110 ++++++ .../ClientSourcedTableColumn/index.ts | 1 + .../pin-button-cell/PinButtonCell.css | 45 +++ .../pin-button-cell/PinButtonCell.tsx | 41 +++ .../pin-button-cell/index.ts | 1 + .../src/examples/AppPatterns/index.ts | 1 + .../src/examples/Table/Table.examples.tsx | 34 +- .../Table/TableSelection.examples.tsx | 6 +- ....examples.tsx => TableSearch.examples.tsx} | 78 ++++- .../showcase/src/examples/UiControls/index.ts | 2 +- 23 files changed, 752 insertions(+), 80 deletions(-) create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientSourcedTableColumn.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientTableColumnProvider/ClientTableColumnProvider.tsx create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/index.ts create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.css create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.tsx create mode 100644 vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/index.ts rename vuu-ui/showcase/src/examples/UiControls/{InstrumentSearch.examples.tsx => TableSearch.examples.tsx} (64%) 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 8ee204d59..f0bdbf4f0 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 @@ -139,6 +139,7 @@ export class ArrayDataSource constructor({ aggregations, + baseFilterSpec, // different from RemoteDataSource columnDescriptors, data, @@ -176,6 +177,7 @@ export class ArrayDataSource this.config = { ...this._config, aggregations: aggregations || this._config.aggregations, + baseFilterSpec, columns, filterSpec: filterSpec || this._config.filterSpec, groupBy: groupBy || this._config.groupBy, @@ -190,6 +192,7 @@ export class ArrayDataSource viewport = this.viewport ?? (this.viewport = uuid()), columns, aggregations, + baseFilterSpec, range, selectedIndexValues, selectedKeyValues, @@ -220,6 +223,7 @@ export class ArrayDataSource config = { ...config, aggregations: aggregations || this._config.aggregations, + baseFilterSpec: baseFilterSpec || this._config.baseFilterSpec, columns: columns || this._config.columns, filterSpec: filterSpec || this._config.filterSpec, groupBy: groupBy || this._config.groupBy, diff --git a/vuu-ui/packages/vuu-icons/index.css b/vuu-ui/packages/vuu-icons/index.css index 9501d5b2f..788144b9e 100644 --- a/vuu-ui/packages/vuu-icons/index.css +++ b/vuu-ui/packages/vuu-icons/index.css @@ -54,6 +54,8 @@ --vuu-svg-history: url('data:image/svg+xml;utf8,'); --vuu-svg-more-horiz: url('data:image/svg+xml;utf8,'); --vuu-svg-more-vert: url('data:image/svg+xml;utf8,'); + --vuu-svg-pin-off: url('data:image/svg+xml;utf8,'); + --vuu-svg-pin-on: url('data:image/svg+xml;utf8,'); --vuu-svg-plus: url('data:image/svg+xml;utf8,'); --vuu-svg-price-arrow: url('data:image/svg+xml;utf8,'); --vuu-svg-radio: url('data:image/svg+xml;utf8,'); @@ -63,8 +65,11 @@ --vuu-svg-triangle-right: url('data:image/svg+xml;utf8,'); --vuu-svg-info-circle: url('data:image/svg+xml;utf8, '); --vuu-svg-warn-triangle: url('data:image/svg+xml;utf8,'); + } + + span[data-icon] { display: inline-block; height: var(--vuu-icon-height, var(--vuu-icon-size, 18px)); @@ -219,6 +224,13 @@ span[data-icon] { --vuu-icon-svg: var(--svg-open-in); } +[data-icon="pin-off"] { + --vuu-icon-svg: var(--vuu-svg-pin-off); +} +[data-icon="pin-on"] { + --vuu-icon-svg: var(--vuu-svg-pin-on); +} + [data-icon="price-arrow"] { --vuu-icon-svg: var(--vuu-svg-price-arrow); } diff --git a/vuu-ui/packages/vuu-table-types/index.d.ts b/vuu-ui/packages/vuu-table-types/index.d.ts index 3e80a7ced..be6679bda 100644 --- a/vuu-ui/packages/vuu-table-types/index.d.ts +++ b/vuu-ui/packages/vuu-table-types/index.d.ts @@ -268,6 +268,13 @@ export interface ColumnDescriptor extends DataValueDescriptor { pin?: PinLocation; resizeable?: boolean; sortable?: boolean; + /** + * 'client' columns will not receive data from dataSource. + * They can be used with a custom renderer, e.g to render + * action buttons. + * default is 'server' + */ + source?: "client" | "server"; width?: number; } diff --git a/vuu-ui/packages/vuu-table/src/Table.css b/vuu-ui/packages/vuu-table/src/Table.css index 215597144..ca183c262 100644 --- a/vuu-ui/packages/vuu-table/src/Table.css +++ b/vuu-ui/packages/vuu-table/src/Table.css @@ -129,15 +129,15 @@ } .vuuTable-table { + border: none; + border-collapse: separate; + border-spacing: 0; + left: 0; + margin: 0; position: absolute; top: 0; - left: 0; table-layout: fixed; width: var(--content-width); - margin: 0; - border: none; - border-collapse: separate; - border-spacing: 0; } .vuuTable-body { diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index 17c8ba191..0104aafff 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -32,6 +32,7 @@ import { useWindow } from "@salt-ds/window"; import cx from "clsx"; import { CSSProperties, + ComponentType, FC, ForwardedRef, RefObject, @@ -59,6 +60,10 @@ export type TableNavigationStyle = "none" | "cell" | "row" | "tree"; export interface TableProps extends Omit { + /** + * A react function componnet that will be rendered if there are no rows to display + */ + EmptyDisplay?: ComponentType; Row?: FC; /** * Allow a block of cells to be selected. Typically to be copied. @@ -249,6 +254,7 @@ export interface TableProps } const TableCore = ({ + EmptyDisplay, Row = DefaultRow, allowCellBlockSelection, allowDragColumnHeader = true, @@ -384,6 +390,10 @@ const TableCore = ({ const headersReady = showColumnHeaders === false || headerHeight > 0; const readyToRenderTableBody = headersReady && data.length > 0; + if (dataSource.size === 0 && EmptyDisplay) { + return ; + } + return ( 0 ? `${rowHeight}px` : undefined, } as CSSProperties } @@ -639,6 +651,7 @@ export const Table = forwardRef(function Table( rowHeight && (footerHeight || showPaginationControls !== true) ? ( viewportBodyHeight ? 10 : 0; - const availableWidth = size.width - (verticalScrollbarWidth + 8); + const availableWidth = + size.width - (verticalScrollbarWidth + (2 & selectionBookendWidth)); const rowClassNameGenerator = useRowClassNameGenerators(config); diff --git a/vuu-ui/packages/vuu-table/src/useTableModel.ts b/vuu-ui/packages/vuu-table/src/useTableModel.ts index e6ab8c055..10c94614f 100644 --- a/vuu-ui/packages/vuu-table/src/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/useTableModel.ts @@ -309,6 +309,8 @@ function init( tableSchema, ); + console.log(`useTableModel availableWidth ${availableWidth}`); + const runtimeColumns: RuntimeColumnDescriptor[] = []; let colIndex = 1; for (const column of columns.filter( @@ -380,6 +382,7 @@ const columnDescriptorToRuntimeColumDescriptor = align = getDefaultAlignment(serverDataType), name, label = getColumnLabel(column), + source = "server", width = columnDefaultWidth, ...rest } = column; @@ -398,6 +401,7 @@ const columnDescriptorToRuntimeColumDescriptor = name, originalIdx: ariaColIndex, serverDataType, + source, valueFormatter: getValueFormatter(column, serverDataType), width, }; diff --git a/vuu-ui/packages/vuu-theme/css/components/button.css b/vuu-ui/packages/vuu-theme/css/components/button.css index 926e7551f..c829ecd5f 100644 --- a/vuu-ui/packages/vuu-theme/css/components/button.css +++ b/vuu-ui/packages/vuu-theme/css/components/button.css @@ -28,7 +28,7 @@ --vuu-icon-color: var(--button-text-color-disabled); } - .saltButton-primary { + .saltButton-neutral { --saltButton-borderColor: var( --vuuButton-borderColor, var(--salt-actionable-bold-foreground) @@ -37,7 +37,7 @@ --saltButton-borderStyle: solid; } - .saltButton-primary:not(:disabled):not(:active):not( + .saltButton-neutral:not(:disabled):not(:active):not( .saltButton-active ):hover { --saltButton-borderColor: var( @@ -46,3 +46,7 @@ ); } } + +.saltButton[data-embedded]{ + border: none; +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-ui-controls/src/measured-container/MeasuredContainer.tsx b/vuu-ui/packages/vuu-ui-controls/src/measured-container/MeasuredContainer.tsx index 397f0066e..e4802d60e 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/measured-container/MeasuredContainer.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/measured-container/MeasuredContainer.tsx @@ -31,7 +31,7 @@ export const MeasuredContainer = forwardRef(function MeasuredContainer( width, ...htmlAttributes }: MeasuredContainerProps, - forwardedRef: ForwardedRef + forwardedRef: ForwardedRef, ) { const targetWindow = useWindow(); useComponentCssInjection({ @@ -52,6 +52,7 @@ export const MeasuredContainer = forwardRef(function MeasuredContainer( const getStyle = () => { return unmeasured ? ({ + ...style, "--measured-css-height": `${cssSize.height}`, "--measured-css-width": `${cssSize.width}`, } as CSSProperties) diff --git a/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts b/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts index efb2955d2..3fea222e5 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/measured-container/useMeasuredContainer.ts @@ -213,7 +213,11 @@ export const useMeasuredContainer = ({ useEffect(() => { if (size.inner) { - onResizeProp?.(size.inner); + if (containerRef.current) { + // reassign using clientWidth to correctly account for borders + size.inner.width = containerRef.current.clientWidth; + onResizeProp?.(size.inner); + } } }, [onResizeProp, size.inner]); diff --git a/vuu-ui/packages/vuu-utils/src/column-utils.ts b/vuu-ui/packages/vuu-utils/src/column-utils.ts index 1ad6b65d9..909682106 100644 --- a/vuu-ui/packages/vuu-utils/src/column-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/column-utils.ts @@ -900,13 +900,13 @@ export const getTypeFormattingFromColumn = ( }; /** - * - * return a filter predicate that will reject columns, names of which - * are not in provided list. + * Return a filter predicate that will reject columns, names of which + * are not in provided list. Exception made for columns explicitly + * configured as client columns. */ export const subscribedOnly = (columnNames?: string[]) => (column: ColumnDescriptor) => - columnNames?.includes(column.name); + column.source === "client" || columnNames?.includes(column.name); export const addColumnToSubscribedColumns = ( subscribedColumns: ColumnDescriptor[], @@ -1142,7 +1142,6 @@ export function applyWidthToColumns( totalWidth, defaultMaxWidth, defaultWidth, - flexCount, ); } } @@ -1213,52 +1212,61 @@ const shrinkColumnsToFitAvailableSpace = ( } }; +const hasFlex = ({ flex }: ColumnDescriptor) => typeof flex === "number"; + const stretchColumnsToFillAvailableSpace = ( columns: RuntimeColumnDescriptor[], availableWidth: number, totalWidth: number, defaultMaxWidth: number, defaultWidth: number, - flexCount: number, ) => { let freeSpaceToBeFilled = availableWidth - totalWidth; - const additionalWidthPerColumn = Math.floor( - freeSpaceToBeFilled / (flexCount || columns.length), - ); - const newColumns = columns.map((column) => { - const { - maxWidth = defaultMaxWidth, - width = defaultWidth, - flex = 0, - } = column; - if (flexCount > 0 && flex === 0) { - return column; - } - const adjustedWidth = width + additionalWidthPerColumn; - if (adjustedWidth > maxWidth) { - return { ...column, width: maxWidth }; - } else { - freeSpaceToBeFilled -= additionalWidthPerColumn; - return { ...column, width: adjustedWidth, canStretch: true }; - } - }); - const columnsNotYetAtMaxWidth = newColumns.filter( - (col) => col.canStretch, - ).length; - const finalAdjustmentPerColumn = Math.min( - 1, - Math.ceil(freeSpaceToBeFilled / columnsNotYetAtMaxWidth), - ); - return newColumns.map( - ({ canStretch, ...column }) => { - if (canStretch && freeSpaceToBeFilled) { - freeSpaceToBeFilled -= finalAdjustmentPerColumn; - return { ...column, width: column.width + finalAdjustmentPerColumn }; - } else { + let adjustedColumns = columns; + + const canGrow = ({ + width = defaultWidth, + maxWidth = defaultMaxWidth, + }: ColumnDescriptor) => width < maxWidth; + + while (freeSpaceToBeFilled > 0) { + const flexCols = adjustedColumns.filter( + (col) => hasFlex(col) && canGrow(col), + ); + const columnsNotYetAtMaxWidth = + flexCols.length || adjustedColumns.filter(canGrow).length; + + // THis deos not take flex correctly into account + const additionalWidthPerColumn = Math.ceil( + freeSpaceToBeFilled / columnsNotYetAtMaxWidth, + ); + adjustedColumns = columns.map((column) => { + const { + maxWidth = defaultMaxWidth, + width = defaultWidth, + flex = 0, + } = column; + if (flexCols.length > 0 && flex === 0) { return column; } - }, - ); + + // we rounded the additionalWidthPerColumn up, so make sure + // we don't over-assign + const adjustmentAmount = Math.min( + additionalWidthPerColumn, + freeSpaceToBeFilled, + ); + const adjustedWidth = width + adjustmentAmount; + if (adjustedWidth > maxWidth) { + freeSpaceToBeFilled -= adjustedWidth - maxWidth; + return { ...column, width: maxWidth }; + } else { + freeSpaceToBeFilled -= adjustmentAmount; + return { ...column, width: adjustedWidth }; + } + }); + } + return adjustedColumns; }; /** diff --git a/vuu-ui/packages/vuu-utils/test/column-utils.test.ts b/vuu-ui/packages/vuu-utils/test/column-utils.test.ts index 16611aed1..e6b4a8baa 100644 --- a/vuu-ui/packages/vuu-utils/test/column-utils.test.ts +++ b/vuu-ui/packages/vuu-utils/test/column-utils.test.ts @@ -46,7 +46,7 @@ describe("applyWidthToColumns", () => { }); }); describe("Fit layouts", () => { - it("applies fit layout when the total column width is less than available width", () => { + it("stretches column widths to fit available space when total column width is less than available width", () => { const columns: Partial[] = [ { name: "ID", label: "id", width: 80 }, { name: "ID", label: "id", width: 80 }, @@ -62,7 +62,7 @@ describe("applyWidthToColumns", () => { { name: "ID", label: "id", width: 100 }, ]); }); - it("applies fit layout when the total column width is greater than available width", () => { + it("squeezes columns widths to fit the available space when the total column width is greater than available width", () => { const columns: Partial[] = [ { name: "ID", label: "id", width: 120 }, { name: "ID", label: "id", width: 120 }, @@ -78,7 +78,7 @@ describe("applyWidthToColumns", () => { { name: "ID", label: "id", width: 100 }, ]); }); - it("applies fit layout when the total column width is greater than available width, one column minWidth", () => { + it("squeezes columns widths of some columns, when the total column width is greater than available width, one column minWidth", () => { const columns: Partial[] = [ { name: "ID", label: "id", width: 120 }, { name: "ID", label: "id", width: 120, minWidth: 120 }, @@ -171,7 +171,7 @@ describe("applyWidthToColumns", () => { columnLayout: "fit", availableWidth: 300, defaultMinWidth: 50, - } + }, ); expect(result).toEqual([ { name: "ID", label: "id", width: 50 }, @@ -192,7 +192,7 @@ describe("applyWidthToColumns", () => { columnLayout: "fit", availableWidth: 500, defaultMaxWidth: 250, - } + }, ); expect(result).toEqual([ { name: "ID", label: "id", width: 100 }, @@ -235,6 +235,33 @@ describe("applyWidthToColumns", () => { ]); }); + it("grows one column to fit available space where we only have two columns and one is non-resizeable", () => { + const columns: Partial[] = [ + { name: "ID", label: "id", width: 100 }, + { name: "ID", label: "id", width: 50, maxWidth: 50 }, + ]; + const result = applyWidthToColumns(columns as RuntimeColumnDescriptor[], { + columnLayout: "fit", + availableWidth: 300, + }); + expect(result).toEqual([ + { name: "ID", label: "id", width: 250 }, + { name: "ID", label: "id", width: 50, maxWidth: 50 }, + ]); + }); + + it("grows single column to fit available space where we only have one column and a custom maxWidth", () => { + const columns: Partial[] = [ + { name: "ID", label: "id", width: 100 }, + ]; + const result = applyWidthToColumns(columns as RuntimeColumnDescriptor[], { + columnLayout: "fit", + availableWidth: 300, + defaultMaxWidth: 500, + }); + expect(result).toEqual([{ name: "ID", label: "id", width: 300 }]); + }); + describe("WHEN available size exceeds total max widths", () => { it("THEN all columns are assigned max width", () => { const columns: Partial[] = [ @@ -247,7 +274,7 @@ describe("applyWidthToColumns", () => { { columnLayout: "fit", availableWidth: 1000, - } + }, ); expect(result).toEqual([ { name: "ID", label: "id", width: 250 }, diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientSourcedTableColumn.examples.tsx b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientSourcedTableColumn.examples.tsx new file mode 100644 index 000000000..7148a3d46 --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientSourcedTableColumn.examples.tsx @@ -0,0 +1,312 @@ +import { getSchema, LocalDataSourceProvider } from "@finos/vuu-data-test"; +import { DataSourceFilter, TableSchema } from "@finos/vuu-data-types"; +import { Table, TableProps } from "@finos/vuu-table"; +import { + ColumnDescriptor, + TableConfig, + TableRowSelectHandler, +} from "@finos/vuu-table-types"; +import { + registerComponent, + toColumnName, + useDataSource, +} from "@finos/vuu-utils"; +import { useCallback, useMemo } from "react"; +import { PinButtonCell } from "./pin-button-cell"; +import { + ClientTableColumnProvider, + useClientTableColumn, +} from "./ClientTableColumnProvider/ClientTableColumnProvider"; +import { TableSearch } from "@finos/vuu-ui-controls"; +import { Flexbox, View } from "@finos/vuu-layout"; + +registerComponent("pin-button", PinButtonCell, "cell-renderer", { + userCanAssign: false, +}); + +let displaySequence = 0; + +const TableTemplate = ({ + filter, + config, + highlightedIndex, + maxViewportRowLimit, + navigationStyle, + schema, + viewportRowLimit, + height = viewportRowLimit === undefined && maxViewportRowLimit === undefined + ? 645 + : undefined, + width = 723, + ...props +}: { + columns?: ColumnDescriptor[]; + filter?: DataSourceFilter; + schema: TableSchema; +} & Partial) => { + const { VuuDataSource } = useDataSource(); + + const tableConfig = useMemo(() => { + return ( + config ?? { + columns: schema.columns, + rowSeparators: true, + zebraStripes: true, + } + ); + }, [config, schema]); + + const dataSource = useMemo(() => { + return new VuuDataSource({ + columns: tableConfig.columns.map(toColumnName), + // baseFilter would be ideal for this but it doesn't work + filterSpec: filter, + table: schema.table, + }); + }, [VuuDataSource, tableConfig.columns, filter, schema.table]); + + return ( + + ); +}; + +const TableSearchTemplate = ({ + schema, + TableProps, +}: { + schema: TableSchema; + TableProps?: Partial; +}) => { + const { VuuDataSource } = useDataSource(); + const dataSource = useMemo(() => { + const { table } = schema; + const dataSource = new VuuDataSource({ + columns: schema.columns.map((c) => c.name), + table, + }); + return dataSource; + }, [VuuDataSource, schema]); + + return ( + + ); +}; + +const PinColumn: ColumnDescriptor = { + className: "vuuIconCell", + label: "", + maxWidth: 24, + name: "pinned", + source: "client", + type: { + name: "boolean", + renderer: { + name: "pin-button", + }, + }, + width: 24, +}; + +export const PinItemButton = () => { + const schema = getSchema("instruments"); + + return ( + + + + + + ); +}; +PinItemButton.displaySequence = displaySequence++; + +export const SearchWithPin = () => { + const schema = getSchema("instruments"); + + return ( + + + + + + ); +}; +SearchWithPin.displaySequence = displaySequence++; + +const EmptyDisplay = () => { + return ( +
+ No Instruments have been pinned. Use the pin icon in the search list below +
+ ); +}; + +const RecentlyUsedItemsTable = ({ schema }: { schema: TableSchema }) => { + const { recent } = useClientTableColumn(); + const filter = + recent.length === 0 + ? 'ric in ["NA"]' + : `ric in [${recent.map((v) => `"${v}"`).join(",")}]`; + + return ( + + ); +}; + +const PinnedItemsTable = ({ schema }: { schema: TableSchema }) => { + const { allValues } = useClientTableColumn(); + const filter = + allValues.length === 0 + ? 'ric in ["NA"]' + : `ric in [${allValues.map((v) => `"${v}"`).join(",")}]`; + + return ( + + ); +}; + +const SearchItemsTable = ({ schema }: { schema: TableSchema }) => { + const { itemUsed } = useClientTableColumn(); + + const onSelect = useCallback( + (row) => { + if (row) { + itemUsed(row.key); + } + }, + [itemUsed], + ); + + return ( + + ); +}; + +export const SearchAndPinned = () => { + const schema = getSchema("instruments"); + + return ( + + + + + + + + + + + + + + + + ); +}; +SearchAndPinned.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientTableColumnProvider/ClientTableColumnProvider.tsx b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientTableColumnProvider/ClientTableColumnProvider.tsx new file mode 100644 index 000000000..43f46dfd0 --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/ClientTableColumnProvider/ClientTableColumnProvider.tsx @@ -0,0 +1,110 @@ +import { createContext, ReactNode, useContext, useMemo, useState } from "react"; + +export interface TableColumnContext { + allValues: unknown[]; + getValue: (key: string) => unknown; + recent: string[]; + setValue: (key: string, value: unknown) => void; + itemUsed: (key: string) => void; +} + +const getOldestEntry = (map: Map) => { + const [key] = map + .entries() + .reduce( + (entry, min) => (entry[1] < min[1] ? entry : min), + ["", Number.MAX_SAFE_INTEGER], + ); + return key; +}; + +class TableColumnContextStore implements TableColumnContext { + #map = new Map(); + #maxRecent: number; + #onUpdate: () => void; + /** + * map of key value to most recent use time + */ + #recent = new Map(); + + constructor(onUpdate: () => void, maxRecent = 10) { + this.#maxRecent = maxRecent; + this.#onUpdate = onUpdate; + } + get allValues() { + return Array.from(this.#map.keys()); + } + // Limitation: there is currently no way to have the recent list appear + // in time order in the list. Thats because the data is sorted by the + // dataSource and that does not have client side information. + get recent() { + return Array.from(this.#recent.entries()).map(([key]) => key); + } + + getValue = (key: string) => { + return this.#map.get(key); + }; + setValue = (key: string, value: unknown) => { + if (value) { + this.#map.set(key, value); + } else { + this.#map.delete(key); + } + this.#onUpdate(); + }; + /** + * Using the item means we make sure its in the cache. The caller + * decides what constitutes 'using'. + */ + itemUsed = (key: string) => { + const alreadyInCache = this.#recent.has(key); + this.#recent.set(key, performance.now()); + + if (!alreadyInCache && this.#recent.size > this.#maxRecent) { + const oldestKey = getOldestEntry(this.#recent); + this.#recent.delete(oldestKey); + } + this.#onUpdate(); + }; +} + +const ClientTableColumnContext = createContext({ + allValues: [], + getValue: () => { + console.warn(`no TableColumnProvider has been installed`); + }, + recent: [], + setValue: () => { + console.warn(`no TableColumnProvider has been installed`); + }, + itemUsed: () => { + console.warn(`no TableColumnProvider has been installed`); + }, +}); + +export const ClientTableColumnProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [, forceRefresh] = useState({}); + const store = useMemo( + () => + new TableColumnContextStore(() => { + console.log("force refresh"); + forceRefresh({}); + }, 6), + [], + ); + + const { allValues, getValue, recent, setValue, itemUsed: useItem } = store; + return ( + + {children} + + ); +}; + +export const useClientTableColumn = () => useContext(ClientTableColumnContext); diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/index.ts b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/index.ts new file mode 100644 index 000000000..666b00ed2 --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/index.ts @@ -0,0 +1 @@ +export * from "./ClientSourcedTableColumn.examples"; diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.css b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.css new file mode 100644 index 000000000..38a85204e --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.css @@ -0,0 +1,45 @@ + +.vuuPinButtonCell { + font-weight: 500; + position: relative; + +} + + + +.vuuIconCell.vuuTableCell { + align-items: center; + display: inline-flex; + justify-content: center; + padding: 0; + + .vuuIconButton { + --saltButton-minWidth: 20px; + --saltButton-borderRadius: 0px; + --saltButton-height: 20px; + --saltButton-width: 20px; + + .vuuIcon { + --vuu-icon-left: 2px; + --vuu-icon-size: 14px; + --vuu-icon-top: 1px; + height: 18px; + width: 18px; + } + .vuuIcon:after{ + --vuu-icon-color: var(--salt-separable-secondary-borderColor); + } + + } + + .vuuPinButtonCell-pinned { + .vuuIcon:after{ + --vuu-icon-color: var(--salt-content-primary-foreground); + } + } + + } + + + + diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.tsx b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.tsx new file mode 100644 index 000000000..635219432 --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/PinButtonCell.tsx @@ -0,0 +1,41 @@ +import { TableCellRendererProps } from "@finos/vuu-table-types"; +import { IconButton } from "@finos/vuu-ui-controls"; +import { metadataKeys } from "@finos/vuu-utils"; +import { MouseEventHandler, useCallback } from "react"; +import { useClientTableColumn } from "../ClientTableColumnProvider/ClientTableColumnProvider"; +import cx from "clsx"; + +import "./PinButtonCell.css"; + +const classBase = "vuuPinButtonCell"; + +const { KEY } = metadataKeys; + +export const PinButtonCell = ({ row }: TableCellRendererProps) => { + const { getValue, setValue } = useClientTableColumn(); + + const value = getValue(row[KEY] as string); + console.log(`value = ${value}`); + + const handleClick = useCallback>( + (evt) => { + setValue(row[KEY], !value); + evt.stopPropagation(); + }, + [row, setValue, value], + ); + + const icon = value ? "pin-on" : "pin-off"; + + return ( + + ); +}; diff --git a/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/index.ts b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/index.ts new file mode 100644 index 000000000..a6bd5b6ae --- /dev/null +++ b/vuu-ui/showcase/src/examples/AppPatterns/ClientSourcedTableColumn/pin-button-cell/index.ts @@ -0,0 +1 @@ +export * from "./PinButtonCell"; diff --git a/vuu-ui/showcase/src/examples/AppPatterns/index.ts b/vuu-ui/showcase/src/examples/AppPatterns/index.ts index 55cddc908..5422ab889 100644 --- a/vuu-ui/showcase/src/examples/AppPatterns/index.ts +++ b/vuu-ui/showcase/src/examples/AppPatterns/index.ts @@ -1,3 +1,4 @@ +export * as ClientSourcedTableColumn from "./ClientSourcedTableColumn"; export * as CrossTableFiltering from "./CrossTableFiltering"; export * as LayoutAndSettings from "./LayoutAndSettings"; export * as TableEditing from "./TableEditing"; diff --git a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx index 484ac035a..fa5d45d49 100644 --- a/vuu-ui/showcase/src/examples/Table/Table.examples.tsx +++ b/vuu-ui/showcase/src/examples/Table/Table.examples.tsx @@ -51,18 +51,15 @@ let displaySequence = 1; export const TestTable = ({ columnLayout, + config: configProp, height = 625, renderBufferSize = 5, rowCount = 1000, rowHeight = 20, width = 1000, -}: { +}: Partial & { columnLayout?: ColumnLayout; - height?: string | number; - renderBufferSize?: number; rowCount?: number; - rowHeight?: number; - width?: string | number; }) => { const config = useMemo( () => ({ @@ -70,8 +67,9 @@ export const TestTable = ({ rowSeparators: true, zebraStripes: true, columnLayout, + ...configProp, }), - [columnLayout], + [columnLayout, configProp], ); const dataSource = useMemo(() => { @@ -100,12 +98,12 @@ export const TestTable = ({ TestTable.displaySequence = displaySequence++; const TableTemplate = ({ + config, height = 645, highlightedIndex, navigationStyle, schema, width = 723, - columns = schema.columns, ...props }: { columns?: ColumnDescriptor[]; @@ -113,20 +111,22 @@ const TableTemplate = ({ } & Partial) => { const { VuuDataSource } = useDataSource(); + const tableConfig = useMemo(() => { + return ( + config ?? { + columns: schema.columns, + rowSeparators: true, + zebraStripes: true, + } + ); + }, [config, schema]); + const dataSource = useMemo(() => { return new VuuDataSource({ - columns: columns.map(toColumnName), + columns: tableConfig.columns.map(toColumnName), table: schema.table, }); - }, [VuuDataSource, columns, schema]); - - const tableConfig = useMemo(() => { - return { - columns, - rowSeparators: true, - zebraStripes: true, - }; - }, [columns]); + }, [VuuDataSource, tableConfig.columns, schema]); return (
{ }; InstrumentSearchDragDrop.displaySequence = displaySequence++; + +const EnhancedInstrumentSearch = () => { + const { VuuDataSource } = useDataSource(); + const schema = getSchema("instruments"); + const pinnedConfig = useMemo( + () => ({ + columns: [{ name: "description", serverDataType: "string" }], + }), + [], + ); + const pinnedDataSource = useMemo( + () => new VuuDataSource({ table: schema.table }), + [VuuDataSource, schema.table], + ); + + const searchTableProps = useMemo>( + () => ({ + config: { + columns: [ + { name: "description" }, + { name: "pinned", serverDataType: "boolean", width: 60 }, + ], + columnLayout: "fit", + }, + }), + [], + ); + return ( + + + Pinned Instruments + +
+ + + + Instrument Search + + + + + + ); +}; + +export const InstrumentSearchFavourites = () => { + return ( + + + + ); +}; + +InstrumentSearchFavourites.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index 5eee2b610..eb6136944 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -3,7 +3,7 @@ export * as ColumnPicker from "./ColumnPicker.examples"; export * as DragDrop from "./DragDrop.examples"; export * as EditableLabel from "./EditableLabel.examples"; export * as TablePicker from "./TablePicker.examples"; -export * as InstrumentSearch from "./InstrumentSearch.examples"; +export * as TableSearch from "./TableSearch.examples"; export * as List from "./List.examples"; export * as OverflowContainer from "./OverflowContainer.examples"; export * as SplitButton from "./SplitButton.examples"; From ee56e7a4543c5e7609c2b6611815865341a05207 Mon Sep 17 00:00:00 2001 From: heswell Date: Tue, 12 Nov 2024 17:23:00 +0000 Subject: [PATCH 2/4] fix behaviour of showcase left pane resize --- vuu-ui/tools/vuu-showcase/src/App.tsx | 177 +++++++++++++------------- 1 file changed, 91 insertions(+), 86 deletions(-) diff --git a/vuu-ui/tools/vuu-showcase/src/App.tsx b/vuu-ui/tools/vuu-showcase/src/App.tsx index 6c84592b5..2d439a95b 100644 --- a/vuu-ui/tools/vuu-showcase/src/App.tsx +++ b/vuu-ui/tools/vuu-showcase/src/App.tsx @@ -1,5 +1,5 @@ import { TreeTable } from "@finos/vuu-datatable"; -import { Flexbox, View } from "@finos/vuu-layout"; +import { FlexboxLayout, LayoutProvider, 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"; @@ -133,95 +133,100 @@ export const App = ({ stories }: AppProps) => { return themeReady ? ( - -
- Vuu Showcase -
- - + +
- - - -
Vuu Showcase +
+ + - - No Theme - SALT - VUU - TAR - - - - - - High - Medium - Low - Touch - - -
-
+ -