diff --git a/src/Gridler/Gridler.tsx b/src/Gridler/Gridler.tsx index 10aef38..40e79d1 100644 --- a/src/Gridler/Gridler.tsx +++ b/src/Gridler/Gridler.tsx @@ -1,12 +1,14 @@ import '@glideapps/glide-data-grid/dist/index.css'; +import React, { type Ref } from 'react'; import { MantineBetterMenusProvider } from '../MantineBetterMenu'; import { GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor'; import { GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor'; -import { type GridlerProps, Provider } from './components/GridlerStore'; +import { type GridlerProps, type GridlerRef, Provider } from './components/GridlerStore'; +import { GridlerRefHandler } from './components/RefHandler'; import { GridlerDataGrid } from './GridlerDataGrid'; -const Gridler = (props: GridlerProps) => { +const _Gridler = (props: GridlerProps, ref: Ref | undefined) => { return ( { }} > + {props.children} ); }; +type GridlerComponentType = { + FormAdaptor: typeof GlidlerFormAdaptor; + LocalDataAdaptor: typeof GlidlerLocalDataAdaptor; +} & React.ForwardRefExoticComponent>; + +const Gridler = React.forwardRef(_Gridler) as GridlerComponentType; + Gridler.FormAdaptor = GlidlerFormAdaptor; Gridler.LocalDataAdaptor = GlidlerLocalDataAdaptor; diff --git a/src/Gridler/GridlerDataGrid.tsx b/src/Gridler/GridlerDataGrid.tsx index 9157d83..03e80b7 100644 --- a/src/Gridler/GridlerDataGrid.tsx +++ b/src/Gridler/GridlerDataGrid.tsx @@ -40,6 +40,7 @@ export const GridlerDataGrid = () => { const { _gridSelection, + allowMultiSelect, focused, getCellContent, getCellsForSelection, @@ -69,6 +70,7 @@ export const GridlerDataGrid = () => { widthProp, } = useGridlerStore((s) => ({ _gridSelection: s._gridSelection, + allowMultiSelect: s.allowMultiSelect, focused: s.focused, getCellContent: s.getCellContent, getCellsForSelection: s.getCellsForSelection, @@ -157,16 +159,16 @@ export const GridlerDataGrid = () => { height={height ?? 400} overscrollX={16} overscrollY={32} - rangeSelect="multi-rect" + rangeSelect={allowMultiSelect ? 'multi-rect' : 'cell'} rightElementProps={{ fill: false, sticky: true, }} rowMarkers={{ checkboxStyle: 'square', - kind: 'both', + kind: allowMultiSelect ? 'both' : 'clickable-number', }} - rowSelect="multi" + rowSelect={allowMultiSelect ? 'multi' : 'single'} rowSelectionMode="auto" spanRangeBehavior="default" {...glideProps} diff --git a/src/Gridler/components/Computer.tsx b/src/Gridler/components/Computer.tsx index d7b5095..72a471c 100644 --- a/src/Gridler/components/Computer.tsx +++ b/src/Gridler/components/Computer.tsx @@ -10,32 +10,36 @@ export const Computer = React.memo(() => { const { _glideref, _gridSelectionRows, - askAPIRowNumber, colFilters, colOrder, colSize, colSort, columns, + getRowIndexByKey, getState, loadPage, ready, + scrollToRowKey, + selectedRowKey, setState, setStateFN, values, } = useGridlerStore((s) => ({ _glideref: s._glideref, _gridSelectionRows: s._gridSelectionRows, - askAPIRowNumber: s.askAPIRowNumber, colFilters: s.colFilters, colOrder: s.colOrder, colSize: s.colSize, colSort: s.colSort, columns: s.columns, + getRowIndexByKey: s.getRowIndexByKey, getState: s.getState, loadPage: s.loadPage, ready: s.ready, + scrollToRowKey: s.scrollToRowKey, + selectedRowKey: s.selectedRowKey, setState: s.setState, setStateFN: s.setStateFN, uniqueid: s.uniqueid, @@ -72,8 +76,8 @@ export const Computer = React.memo(() => { break; } } - if (!(rowIndex >= 0) && typeof askAPIRowNumber === 'function') { - const idx = await askAPIRowNumber(key); + if (!(rowIndex >= 0)) { + const idx = await getRowIndexByKey(key); if (idx) { rowIndexes.push(idx); } @@ -180,12 +184,14 @@ export const Computer = React.memo(() => { : (c.defaultIcon ?? 'sort'), })); }).then(() => { - loadPage(0, 'all'); - getState('_events')?.dispatchEvent?.( - new CustomEvent('onColumnSorted', { - detail: { cols: colSort }, - }) - ); + loadPage(0, 'all').then(() => { + getState('refreshCells')?.(); + getState('_events')?.dispatchEvent?.( + new CustomEvent('onColumnSorted', { + detail: { cols: colSort }, + }) + ); + }); }); }, [colSort]); @@ -195,13 +201,15 @@ export const Computer = React.memo(() => { } if (JSON.stringify(refLastFilters.current) !== JSON.stringify(colFilters)) { - loadPage(0, 'all'); + loadPage(0, 'all').then(() => { + getState('refreshCells')?.(); + getState('_events')?.dispatchEvent?.( + new CustomEvent('onColumnFiltered', { + detail: { filters: colFilters }, + }) + ); + }); refLastFilters.current = colFilters; - getState('_events')?.dispatchEvent?.( - new CustomEvent('onColumnFiltered', { - detail: { filters: colFilters }, - }) - ); } }, [colFilters]); @@ -214,6 +222,8 @@ export const Computer = React.memo(() => { ...c, width: c.id && colSize?.[c.id] ? colSize?.[c.id] : c.width, })); + }).then(() => { + getState('refreshCells')?.(); }); }, [colSize]); @@ -231,6 +241,8 @@ export const Computer = React.memo(() => { }); return result; + }).then(() => { + getState('refreshCells')?.(); }); }, [colOrder]); @@ -242,7 +254,9 @@ export const Computer = React.memo(() => { return; } refFirstRun.current = 1; - loadPage(0); + loadPage(0).then(() => { + getState('refreshCells')?.(); + }); }, [ready, loadPage]); useEffect(() => { @@ -250,30 +264,29 @@ export const Computer = React.memo(() => { const loadPage = () => { const selectFirstRowOnMount = getState('selectFirstRowOnMount'); if (selectFirstRowOnMount) { - const selectedRow = getState('selectedRow'); - if (selectedRow && selectedRow >= 0) { + const scrollToRowKey = getState('scrollToRowKey'); + if (scrollToRowKey && scrollToRowKey >= 0) { return; } const keyField = getState('keyField') ?? 'id'; const page_data = getState('_page_data'); + const firstBuffer = page_data?.[0]?.[0]; const firstRow = firstBuffer?.[keyField]; + const currentValues = getState('values') ?? []; - if (firstRow && firstRow > 0) { - const values = [ - firstBuffer, - ...((getState('values') ?? []) as Array>), - ]; + if (firstRow && firstRow > 0 && (currentValues.length ?? 0) === 0) { + const values = [firstBuffer, ...(currentValues as Array>)]; const onChange = getState('onChange'); - console.log('Selecting first row:', firstRow, firstBuffer, values); + //console.log('Selecting first row:', firstRow, firstBuffer, values); if (onChange) { onChange(values); } else { setState('values', values); } - setState('selectedRow', firstRow); + setState('scrollToRowKey', firstRow); } } }; @@ -284,6 +297,64 @@ export const Computer = React.memo(() => { _events?.removeEventListener('loadPage', loadPage); }; }, []); + + /// logic to apply the selected row. + useEffect(() => { + const ready = getState('ready'); + const ref = getState('_glideref'); + const getRowIndexByKey = getState('getRowIndexByKey'); + + if (scrollToRowKey && ref && ready) { + getRowIndexByKey?.(scrollToRowKey).then((r) => { + if (r !== undefined) { + console.log('Scrolling to selected row:', scrollToRowKey, r); + ref.scrollTo(0, r); + getState('_events').dispatchEvent( + new CustomEvent('scrollToRowKeyFound', { + detail: { rowNumber: r, scrollToRowKey: scrollToRowKey }, + }) + ); + } + }); + } + }, [scrollToRowKey]); + + useEffect(() => { + const ready = getState('ready'); + const ref = getState('_glideref'); + const getRowIndexByKey = getState('getRowIndexByKey'); + const key = selectedRowKey ?? scrollToRowKey; + + if (key && ref && ready) { + getRowIndexByKey?.(key).then((r) => { + if (r !== undefined) { + console.log('Scrolling to selected row:', r, selectedRowKey, scrollToRowKey); + + if (selectedRowKey) { + const onChange = getState('onChange'); + const selected = [{ [getState('keyField') ?? 'id']: selectedRowKey }]; + if (onChange) { + onChange(selected); + } else { + setState('values', selected); + } + } + + ref.scrollTo(0, r); + getState('_events').dispatchEvent( + new CustomEvent('scrollToRowKeyFound', { + detail: { + rowNumber: r, + scrollToRowKey: scrollToRowKey, + selectedRowKey: selectedRowKey, + }, + }) + ); + } + }); + } + }, [scrollToRowKey, selectedRowKey]); + // console.log('Gridler:Debug:Computer', { // colFilters, // colOrder, diff --git a/src/Gridler/components/GridlerStore.tsx b/src/Gridler/components/GridlerStore.tsx index 2af190f..d20eeeb 100644 --- a/src/Gridler/components/GridlerStore.tsx +++ b/src/Gridler/components/GridlerStore.tsx @@ -56,6 +56,7 @@ export type FilterOptionOperator = | 'startswith'; export interface GridlerProps extends PropsWithChildren { + allowMultiSelect?: boolean; columns?: GridlerColumns; defaultSort?: Array; @@ -88,6 +89,7 @@ export interface GridlerProps extends PropsWithChildren { ) => GridCell; rowHeight?: number; + scrollToRowKey?: number; sections?: { bottom?: React.ReactNode; left?: React.ReactNode; @@ -97,7 +99,7 @@ export interface GridlerProps extends PropsWithChildren { rightElementStart?: React.ReactNode; top?: React.ReactNode; }; - selectedRow?: number; + selectedRowKey?: number; selectFirstRowOnMount?: boolean; selectMode?: 'cell' | 'row'; showMenu?: (id: string, options?: Partial) => void; @@ -110,6 +112,17 @@ export interface GridlerProps extends PropsWithChildren { width?: number | string; } +export interface GridlerRef { + getGlideRef: () => DataEditorRef | undefined; + getState: GridlerState['getState']; + refresh: (parms?: any) => Promise; + reload: (parms?: any) => Promise; + reloadRow: (key: number | string) => Promise; + scrollToRow: (key: number | string) => Promise; + selectRow: (key: number | string) => Promise; + setStateFN: GridlerState['setStateFN']; +} + export interface GridlerState { _active_requests?: Array<{ controller: AbortController; page: number }>; _activeTooltip?: ReactNode; @@ -140,6 +153,7 @@ export interface GridlerState { abortSignal: AbortSignal ) => CellArray | GetCellsThunk; getRowBuffer: (row: number) => Record; + getRowIndexByKey: (key: number | string) => Promise; getState: (key: K) => GridlerStoreState[K]; hasLocalData: boolean; @@ -174,7 +188,9 @@ export interface GridlerState { pageSize: number; ready: boolean; + refreshCells: (fromRow?: number, toRow?: number, col?: number) => void; reload?: () => Promise; + renderColumns?: GridlerColumns; setState: ( key: K, @@ -250,6 +266,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { const state = get(); //Handle local data @@ -272,6 +289,43 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { + const state = get(); + + let rowIndex = -1; + if (state.ready) { + const page_data = state._page_data; + const pageSize = state.pageSize; + const keyField = state.keyField ?? 'id'; + for (const p in page_data) { + for (const r in page_data[p]) { + const idx = Number(p) * pageSize + Number(r); + //console.log('Found row', idx, page_data[p][r]?.[keyField], scrollToRowKey); + if (String(page_data[p][r]?.[keyField]) === String(key)) { + rowIndex = + page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx > 0 ? idx : -1; + break; + } + } + if (rowIndex > 0) { + console.log('Local row index', rowIndex, key); + return rowIndex; + } + } + + if (rowIndex > 0) { + return rowIndex; + } else if (typeof state.askAPIRowNumber === 'function') { + const rn = await state.askAPIRowNumber(String(key)); + if (rn && rn >= 0) { + console.log('Remote row index', rowIndex, key); + return rn; + } + } + } + + return undefined; + }, getState: (key) => { return get()[key]; }, @@ -702,6 +756,30 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { + const state = get(); + const damageList: { cell: [number, number] }[] = []; + const colLen = Object.keys(state.renderColumns ?? [1, 2, 3]).length; + + const from = fromRow && fromRow > 0 ? fromRow : 0; + const to = toRow && toRow >= from ? toRow : from + state.pageSize; + + for (let row = from; row <= to; row++) { + if (col && col > 0) { + damageList.push({ + cell: [col, row], + }); + } else { + for (let c = 0; c <= colLen; c++) { + damageList.push({ + cell: [c, row], + }); + } + } + } + + state._glideref?.updateCells(damageList); + }, setState: (key, value) => { set( produce((state) => { @@ -816,60 +894,9 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { - const ready = getState('ready'); - const ref = getState('_glideref'); - const keyField = getState('keyField') ?? 'id'; - const selectedRow = getState('selectedRow') ?? props.selectedRow; - const askAPIRowNumber = getState('askAPIRowNumber'); - let rowIndex = -1; - if (selectedRow && ref && ready) { - const page_data = getState('_page_data'); - const pageSize = getState('pageSize'); - for (const p in page_data) { - for (const r in page_data[p]) { - const idx = Number(p) * pageSize + Number(r); - //console.log('Found row', idx, page_data[p][r]?.[keyField], selectedRow); - if (String(page_data[p][r]?.[keyField]) === String(selectedRow)) { - rowIndex = - page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx > 0 ? idx : -1; - break; - } - } - if (rowIndex > 0) { - break; - } - } - - if (rowIndex > 0) { - ref.scrollTo(0, rowIndex); - getState('_events').dispatchEvent( - new CustomEvent('selectedRowFound', { - detail: { rowNumber: rowIndex, selectedRow: selectedRow }, - }) - ); - } else if (typeof askAPIRowNumber === 'function') { - askAPIRowNumber(String(selectedRow)) - .then((r) => { - if (r >= 0) { - ref.scrollTo(0, r); - getState('_events').dispatchEvent( - new CustomEvent('selectedRowFound', { - detail: { rowNumber: r, selectedRow: selectedRow }, - }) - ); - } - }) - .catch((e) => { - console.warn('Error in askAPIRowNumber', e); - }); - } - } - }, [props.selectedRow]); - getState('_events').addEventListener('reload', (_e: Event) => { getState('reload')?.(); + getState('refreshCells')?.(); }); return { @@ -877,6 +904,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore | undefined) { + const [setStateFN, getstate] = useGridlerStore((s) => [s.setStateFN, s.getState]); + + useImperativeHandle(ref, () => { + return { + getGlideRef: () => { + return getstate('_glideref'); + }, + getState: getstate, + refresh: async (parms?: any) => { + const refreshCells = getstate('refreshCells'); + const loadPage = getstate('loadPage'); + loadPage?.(parms?.pageIndex ?? 0, 'all'); + refreshCells?.(); + }, + reload: async (parms?: any) => { + const refreshCells = getstate('refreshCells'); + const loadPage = getstate('loadPage'); + loadPage?.(parms?.pageIndex ?? 0, 'all'); + refreshCells?.(); + }, + reloadRow: async (key: number | string) => { + const refreshCells = getstate('refreshCells'); + //const loadPage = getstate('loadPage'); + const getRowIndexByKey = getstate('getRowIndexByKey'); + const rn = await getRowIndexByKey?.(String(key)); + if (rn && rn >= 0) { + refreshCells?.(rn, rn + 1); + //todo loadpage or row from server + } + }, + scrollToRow: async (key: number | string) => { + if (key && Number(key) >= 0) { + setStateFN('scrollToRowKey', (cv) => Number(key ?? cv)); + } + }, + selectRow: async (key: number | string) => { + if (key && Number(key) >= 0) { + setStateFN('selectedRowKey', (cv) => Number(key ?? cv)); + } + }, + setStateFN: setStateFN, + }; + }, []); + return <>{props.children}; +} + +export const GridlerRefHandler = React.forwardRef(_GridlerRefHandler); diff --git a/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx b/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx index fbc5214..10eefda 100644 --- a/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx +++ b/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx @@ -56,6 +56,7 @@ function _GlidlerLocalDataAdaptor(props: GlidlerLocalDataAdaptorPro setState('total_rows', sortedData.length); setState('data', sortedData); refChanged.current.colSort = colSort; + getState('refreshCells')?.(); } }, [colSort, props.onColumnSort]); @@ -65,6 +66,7 @@ function _GlidlerLocalDataAdaptor(props: GlidlerLocalDataAdaptorPro setState('total_rows', filteredData.length); setState('data', filteredData); refChanged.current.colFilters = colFilters; + getState('refreshCells')?.(); } }, [colFilters, props.onColumnFilter]); diff --git a/src/Gridler/hooks/use-grid-theme.ts b/src/Gridler/hooks/use-grid-theme.ts index ff61811..0a07348 100644 --- a/src/Gridler/hooks/use-grid-theme.ts +++ b/src/Gridler/hooks/use-grid-theme.ts @@ -102,8 +102,8 @@ export const useGridTheme = () => { // }[colorScheme]; - // for (const selectedRow of gridSelection?.rows) { - // if (selectedRow === row) { + // for (const scrollToRowKey of gridSelection?.rows) { + // if (scrollToRowKey === row) { // return { // bgCell: rowColor.bgCell, // bgCellMedium: rowColor.bgCellMedium diff --git a/src/Gridler/stories/Examples.goapi.tsx b/src/Gridler/stories/Examples.goapi.tsx index 8d7dab1..7d56550 100644 --- a/src/Gridler/stories/Examples.goapi.tsx +++ b/src/Gridler/stories/Examples.goapi.tsx @@ -1,10 +1,11 @@ -import { Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core'; +import { Button, Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core'; import { useLocalStorage } from '@mantine/hooks'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import type { GridlerColumns } from '../components/Column'; import { GlidlerAPIAdaptorForGoLangv2 } from '../components/adaptors'; +import { type GridlerRef } from '../components/GridlerStore'; import { Gridler } from '../Gridler'; export const GridlerGoAPIExampleEventlog = () => { @@ -12,6 +13,7 @@ export const GridlerGoAPIExampleEventlog = () => { defaultValue: 'http://localhost:8080/api', key: 'apiurl', }); + const ref = useRef(null); const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' }); const [selectRow, setSelectRow] = useState(''); const [values, setValues] = useState>>([]); @@ -106,8 +108,9 @@ export const GridlerGoAPIExampleEventlog = () => { //console.log('GridlerGoAPIExampleEventlog onChange', v); setValues(v); }} + ref={ref} + scrollToRowKey={selectRow ? parseInt(selectRow, 10) : undefined} sections={{ ...sections, rightElementDisabled: false }} - selectedRow={selectRow ? parseInt(selectRow, 10) : undefined} selectFirstRowOnMount={true} selectMode="row" title="Go API Example" @@ -142,6 +145,43 @@ export const GridlerGoAPIExampleEventlog = () => { /> ; + + + + + + + ); };