From 24227f211072f3fe9cc1418b9331642f5c2e0e53 Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 13 Oct 2025 18:05:59 +0200 Subject: [PATCH] Interesting delemma --- src/Gridler/GridlerDataGrid.tsx | 59 +++++-- src/Gridler/components/APIAdaptorGoLangv2.tsx | 69 +++++++-- src/Gridler/components/Computer.tsx | 144 +++++++++++++++++- src/Gridler/components/Store.tsx | 77 +++++++--- src/Gridler/stories/Examples.goapi.tsx | 29 +++- 5 files changed, 326 insertions(+), 52 deletions(-) diff --git a/src/Gridler/GridlerDataGrid.tsx b/src/Gridler/GridlerDataGrid.tsx index 68236e9..7e9dffd 100644 --- a/src/Gridler/GridlerDataGrid.tsx +++ b/src/Gridler/GridlerDataGrid.tsx @@ -1,5 +1,10 @@ import '@glideapps/glide-data-grid/dist/index.css'; -import { DataEditor, type DataEditorRef, type GridColumn } from '@glideapps/glide-data-grid'; +import { + CompactSelection, + DataEditor, + type DataEditorRef, + type GridColumn, +} from '@glideapps/glide-data-grid'; import { ActionIcon } from '@mantine/core'; import { useElementSize, useMergedRef } from '@mantine/hooks'; import { IconMenu2 } from '@tabler/icons-react'; @@ -20,9 +25,12 @@ export const GridlerDataGrid = () => { const { ref: refWrapper, width } = useElementSize(); const { + _gridSelection, focused, getCellContent, getCellsForSelection, + getState, + glideProps, hasLocalData, headerHeight, mounted, @@ -36,13 +44,16 @@ export const GridlerDataGrid = () => { onVisibleRegionChanged, renderColumns, rowHeight, + setState, setStateFN, total_rows, - glideProps, } = useGridlerStore((s) => ({ + _gridSelection: s._gridSelection, focused: s.focused, getCellContent: s.getCellContent, getCellsForSelection: s.getCellsForSelection, + getState: s.getState, + glideProps: s.glideProps, hasLocalData: s.hasLocalData, headerHeight: s.headerHeight, mounted: s.mounted, @@ -56,13 +67,12 @@ export const GridlerDataGrid = () => { onVisibleRegionChanged: s.onVisibleRegionChanged, renderColumns: s.renderColumns, rowHeight: s.rowHeight, + setState: s.setState, setStateFN: s.setStateFN, total_rows: s.total_rows, - glideProps: s.glideProps, })); const refMerged = useMergedRef(ref, (r) => { - setStateFN('_glideref', () => { return r ?? undefined; }); @@ -80,7 +90,12 @@ export const GridlerDataGrid = () => { const theme = useGridTheme(); if (!mounted) { - return <>Loadings...; + return ( + <> + Loadings... + + + ); } return (
{ columns={(renderColumns as Array) ?? []} columnSelect="none" drawFocusRing - getCellContent={getCellContent} getCellsForSelection={getCellsForSelection} getRowThemeOverride={theme.getRowThemeOverride} + gridSelection={_gridSelection} headerHeight={headerHeight ?? 32} headerIcons={{ sort: SortSprite, sortdown: SortDownSprite, sortup: SortUpSprite }} height={width} @@ -126,6 +141,30 @@ export const GridlerDataGrid = () => { onColumnMoved={onColumnMoved} onColumnProposeMove={onColumnProposeMove} onColumnResize={onColumnResize} + onGridSelectionChange={(selection) => { + let rows = CompactSelection.empty(); + const currentSelection = getState('_gridSelection'); + // const currentRowSelection = getState('_gridSelectionRows') ?? CompactSelection.empty(); + // for (const r of currentRowSelection) { + // rows = rows.hasIndex(r) ? rows : rows.add(r); + // } + for (const r of selection.rows) { + rows = rows.hasIndex(r) ? rows : rows.add(r); + } + + if ( + JSON.stringify(currentSelection?.columns) !== JSON.stringify(selection.columns) || + JSON.stringify(currentSelection?.rows) !== JSON.stringify(selection.rows) || + JSON.stringify(currentSelection?.current) !== JSON.stringify(selection.current) + ) { + setState('_gridSelection', { ...selection, rows }); + if (JSON.stringify(currentSelection?.rows) !== JSON.stringify(selection.rows)) { + setState('_gridSelectionRows', rows); + } + } + + //console.log('Selection', selection); + }} onHeaderClicked={onHeaderClicked} onHeaderContextMenu={(col, event) => { event.preventDefault(); @@ -139,7 +178,7 @@ export const GridlerDataGrid = () => { }} onHeaderMenuClick={onHeaderMenuClick} onVisibleRegionChanged={onVisibleRegionChanged} - rangeSelect="none" + rangeSelect="multi-rect" ref={refMerged as any} rightElement={ onContextClick('menu', e)} variant="subtle"> @@ -151,11 +190,9 @@ export const GridlerDataGrid = () => { //rowMarkersKind='both' rowMarkers={{ checkboxStyle: 'square', - kind: 'both' + kind: 'both', }} - rows={total_rows} - rowSelect="multi" // onGridSelectionChange={(sel) => { // console.log("Selection",sel); @@ -169,7 +206,7 @@ export const GridlerDataGrid = () => { // width: 30 // }} - rowSelectionMode='auto' + rowSelectionMode="auto" spanRangeBehavior="default" theme={theme.gridTheme} width="100%" diff --git a/src/Gridler/components/APIAdaptorGoLangv2.tsx b/src/Gridler/components/APIAdaptorGoLangv2.tsx index 6bafc77..09aacaa 100644 --- a/src/Gridler/components/APIAdaptorGoLangv2.tsx +++ b/src/Gridler/components/APIAdaptorGoLangv2.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useEffect } from 'react'; import type { APIOptions } from '../utils/types'; @@ -11,7 +12,7 @@ export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { s.setState, s.getState, s.addError, - s.mounted + s.mounted, ]); const useAPIQuery: (index: number) => Promise = async (index: number) => { @@ -19,7 +20,7 @@ export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { const pageSize = getState('pageSize'); const colFilters = getState('colFilters'); const _active_requests = getState('_active_requests'); - + //console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props }); if (props && props.url) { const head = new Headers(); head.set('x-limit', String(pageSize ?? 50)); @@ -37,11 +38,13 @@ export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { } if (colFilters?.length && colFilters.length > 0) { - colFilters?.filter((f)=>f.value?.length > 0)?.forEach((filter: any) => { - if (filter.value && filter.value !== '') { - head.set(`x-searchop-${filter.operator}-${filter.id}`, `${filter.value}`); - } - }); + colFilters + ?.filter((f) => f.value?.length > 0) + ?.forEach((filter: any) => { + if (filter.value && filter.value !== '') { + head.set(`x-searchop-${filter.operator}-${filter.id}`, `${filter.value}`); + } + }); } const currentRequestIndex = _active_requests?.findIndex((f) => f.page === index) ?? -1; @@ -56,17 +59,14 @@ export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { } const controller = new AbortController(); - await setStateFN('_active_requests', (cv) => [ - ...(cv ?? []), - { controller, page: index } - ]); + await setStateFN('_active_requests', (cv) => [...(cv ?? []), { controller, page: index }]); const res = await fetch( `${props.url}?x-limit=${String(pageSize ?? 50)}&x-offset=${String((pageSize ?? 50) * index)}`, { headers: head, method: 'GET', - signal: controller?.signal + signal: controller?.signal, } ); @@ -79,17 +79,56 @@ export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { const data = await res.json(); return data ?? []; - } - addError(`${res.status} ${res.statusText}`, 'api', props.url); - + } + addError(`${res.status} ${res.statusText}`, 'api', props.url); await setStateFN('_active_requests', (cv) => [...(cv ?? []).filter((f) => f.page !== index)]); } return []; }; + const askAPIRowNumber: (key: string) => Promise = async (key: string) => { + const colFilters = getState('colFilters'); + + //console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props }); + if (props && props.url) { + const head = new Headers(); + head.set('x-limit', '10'); + head.set('x-fetch-rownumber', String(key)); + + head.set('Authorization', `Token ${props.authtoken}`); + + if (colFilters?.length && colFilters.length > 0) { + colFilters + ?.filter((f) => f.value?.length > 0) + ?.forEach((filter: any) => { + if (filter.value && filter.value !== '') { + head.set(`x-searchop-${filter.operator}-${filter.id}`, `${filter.value}`); + } + }); + } + + const controller = new AbortController(); + + const res = await fetch(`${props.url}?x-fetch-rownumber=${key}}`, { + headers: head, + method: 'GET', + signal: controller?.signal, + }); + + if (res.ok) { + const data = await res.json(); + + return data?.[0]?._rownumber ?? data?._rownumber ?? 0; + } + addError(`${res.status} ${res.statusText}`, 'api', props.url); + } + return []; + }; + useEffect(() => { setState('useAPIQuery', useAPIQuery); + setState('askAPIRowNumber', askAPIRowNumber); }, [props.url, props.authtoken, mounted, setState]); return <>; diff --git a/src/Gridler/components/Computer.tsx b/src/Gridler/components/Computer.tsx index ba2cfe0..68031d6 100644 --- a/src/Gridler/components/Computer.tsx +++ b/src/Gridler/components/Computer.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { CompactSelection } from '@glideapps/glide-data-grid'; import React, { useEffect, useRef } from 'react'; import { useGridlerStore } from './Store'; @@ -8,28 +10,157 @@ export const Computer = React.memo(() => { const refLastFilters = useRef(null); const { _glideref, + _gridSelectionRows, + askAPIRowNumber, colFilters, colOrder, colSize, colSort, columns, + getState, loadPage, + 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, + getState: s.getState, loadPage: s.loadPage, + setState: s.setState, setStateFN: s.setStateFN, - uniqueid: s.uniqueid, + values: s.values, })); + useEffect(() => { + const searchSelection = async () => { + const page_data = getState('_page_data'); + const pageSize = getState('pageSize'); + const keyField = getState('keyField') ?? 'id'; + const rowIndexes = []; + for (const vi in values as Array) { + let rowIndex = -1; + const key = String( + typeof values?.[vi] === 'object' + ? values?.[vi]?.[keyField] + : typeof values?.[vi] === 'string' + ? values?.[vi] + : undefined + ); + for (const p in page_data) { + for (const r in page_data[p]) { + const idx = Number(p) * pageSize + Number(r); + + if (String(page_data[p][r]?.[keyField]) === key) { + //console.log('Found row S', idx, page_data[p][r], page_data[p][r]?.[keyField], key); + rowIndex = idx; + break; + } + } + if (rowIndex >= 0) { + rowIndexes.push(rowIndex); + break; + } + } + if (!(rowIndex >= 0) && typeof askAPIRowNumber === 'function') { + const idx = await askAPIRowNumber(key); + if (idx) { + rowIndexes.push(idx); + } + } + //console.log('Setting SSS', { key, rowIndex }); + } + //console.log('Setting selection', { rowIndexes, values }); + return rowIndexes; + }; + + if (values) { + searchSelection().then((rowIndexes) => { + // const newObj : GridSelection = { + // ...cur, + + // rows: { + // items: rowIndexes.map((r) => [r - 1, r]) ?? [], + // }, + // }; + // console.log('Setting selection', { + // rowIndexes, + // values, + // newObj, + // }); + setStateFN('_gridSelectionRows', () => { + let rows = CompactSelection.empty(); + rowIndexes.forEach((r) => { + rows = rows.add(r); + }); + // for (const r of cur ?? CompactSelection.empty()) { + // rows = rows.add(r); + // } + setStateFN('_gridSelection', (c) => ({ + columns: c?.columns ?? CompactSelection.empty(), + ...c, + rows, + })); + + return rows; + }); + }); + } + }, [values]); + + useEffect(() => { + //console.log('Gridler:Computer: Selection changed', _gridSelectionRows?.toArray()); + const onChange = getState('onChange'); + if (onChange && typeof onChange === 'function') { + const page_data = getState('_page_data'); + const pageSize = getState('pageSize'); + + const buffers = []; + if (_gridSelectionRows) { + for (const range of _gridSelectionRows) { + let buffer = undefined; + + for (const p in page_data) { + for (const r in page_data[p]) { + const idx = Number(p) * pageSize + Number(r); + if (isNaN(idx)) { + continue; + } + + if (Number(page_data[p][r]?._rownumber) === range + 1) { + buffer = page_data[p][r]; + //console.log('Found row', range, idx, page_data[p][r]?._rownumber); + break; + } else if (idx === range + 1) { + buffer = page_data[p][r]; + //console.log('Found row 2', range, idx, page_data[p][r]?._rownumber); + break; + } + } + } + if (buffer !== undefined) { + buffers.push(buffer); + } + } + } + //console.log('Calling onChange with buffers', buffers, _gridSelectionRows?.toArray()); + const _values = getState('values'); + + if (JSON.stringify(_values) !== JSON.stringify(buffers)) { + onChange(buffers); + } + } + }, [JSON.stringify(_gridSelectionRows), getState]); + useEffect(() => { setState( 'renderColumns', @@ -45,6 +176,14 @@ export const Computer = React.memo(() => { if (!colSort) { return; } + + setState('_gridSelection', { + columns: CompactSelection.empty(), + current: undefined, + rows: CompactSelection.empty(), + }); + + setState('_gridSelectionRows', CompactSelection.empty()); setStateFN('renderColumns', (cols) => { return cols?.map((c) => ({ ...c, @@ -64,7 +203,7 @@ export const Computer = React.memo(() => { if (!colFilters) { return; } - + if (JSON.stringify(refLastFilters.current) !== JSON.stringify(colFilters)) { loadPage(0, 'all'); refLastFilters.current = colFilters; @@ -87,6 +226,7 @@ export const Computer = React.memo(() => { if (!colOrder) { return; } + setStateFN('renderColumns', (cols) => { const result = cols?.sort((a, b) => { if (colOrder[a.id] > colOrder[b.id]) { diff --git a/src/Gridler/components/Store.tsx b/src/Gridler/components/Store.tsx index ca2278e..a0a2d19 100644 --- a/src/Gridler/components/Store.tsx +++ b/src/Gridler/components/Store.tsx @@ -10,9 +10,10 @@ import { type GridCell, GridCellKind, type GridColumn, + type GridSelection, type HeaderClickedEventArgs, type Item, - type Rectangle + type Rectangle, } from '@glideapps/glide-data-grid'; import { getUUID } from '@warkypublic/artemis-kit'; import { getNestedValue } from '@warkypublic/artemis-kit/object'; @@ -47,6 +48,7 @@ export type FilterOptionOperator = | 'startswith'; export interface GridlerProps extends PropsWithChildren { + askAPIRowNumber?: (key: string) => Promise; columns?: GridlerColumns; data?: Array; defaultSort?: Array; @@ -58,11 +60,13 @@ export interface GridlerProps extends PropsWithChildren { col?: GridlerColumn, defaultItems?: Array ) => Array; - glideProps?: Partial + glideProps?: Partial; headerHeight?: number; hideMenu?: (id: string) => void; + keyField?: string; maxConcurrency?: number; + onChange?: (values: Array>) => void; pageSize?: number; progressiveScroll?: boolean; RenderCell?: >( @@ -77,11 +81,15 @@ export interface GridlerProps extends PropsWithChildren { selectedRow?: number; showMenu?: (id: string, options?: Partial) => void; uniqueid: string; + useAPIQuery?: (index: number) => Promise>>; + values?: Array>; } export interface GridlerState { _active_requests?: Array<{ controller: AbortController; page: number }>; _glideref?: DataEditorRef; + _gridSelection?: GridSelection; + _gridSelectionRows?: GridSelection['rows']; _loadingList: CompactSelection; _page_data: Record>; _scrollTimeout?: any | number; @@ -93,7 +101,6 @@ export interface GridlerState { colSize?: Record; colSort?: Array; data?: Array; - errors: Array; focused?: boolean; @@ -145,7 +152,6 @@ export interface GridlerState { ) => Promise; toCell: >(row: any, col: number) => GridCell; total_rows: number; - useAPIQuery?: (index: number) => Promise>>; } export type GridlerStoreState = GridlerProps & GridlerState; @@ -206,7 +212,6 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { //if (r.x === cv.x && r.y === cv.y && r.width === cv.width && r.height === cv.height) @@ -214,17 +219,6 @@ const { Provider, useStore: useGridlerStore } = createSyncStore !state._loadingList.hasIndex(i)), - // state.maxConcurrency - // )) { - // await Promise.all(pageChunk.map(state.loadPage)); - // } - - // for (const page of range(firstPage - 1, lastPage + 1, 1)) { - // state.loadPage(page); - // } - const result: GridCell[][] = []; for (let y = selection.y; y < selection.y + selection.height; y++) { @@ -235,6 +229,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { const state = get(); const page = pPage < 0 ? 0 : pPage; @@ -477,7 +473,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { - const [setState, glideref, getState] = props.useStore((s) => [ - s.setState, - s._glideref, - s.getState, - ]); + const [setState, getState] = props.useStore((s) => [s.setState, s.getState]); const menus = useMantineBetterMenus(); @@ -661,6 +653,47 @@ const { Provider, useStore: useGridlerStore } = createSyncStore { + 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) { + 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); + } else if (typeof askAPIRowNumber === 'function') { + askAPIRowNumber(String(selectedRow)) + .then((r) => { + if (r >= 0) { + ref.scrollTo(0, r); + } + }) + .catch((e) => { + console.warn('Error in askAPIRowNumber', e); + }); + } + } + }, [props.selectedRow]); + return { ...props, hasLocalData: props.data && props.data.length > 0, diff --git a/src/Gridler/stories/Examples.goapi.tsx b/src/Gridler/stories/Examples.goapi.tsx index c845bed..8c46418 100644 --- a/src/Gridler/stories/Examples.goapi.tsx +++ b/src/Gridler/stories/Examples.goapi.tsx @@ -1,5 +1,6 @@ -import { Divider, Stack, TextInput } from '@mantine/core'; +import { Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core'; import { useLocalStorage } from '@mantine/hooks'; +import { useState } from 'react'; import type { GridlerColumns } from '../components/Column'; @@ -12,7 +13,8 @@ export const GridlerGoAPIExampleEventlog = () => { key: 'apiurl', }); const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' }); - + const [selectRow, setSelectRow] = useState(''); + const [values, setValues] = useState>>([]); const columns: GridlerColumns = [ { id: 'id_process', @@ -58,10 +60,33 @@ export const GridlerGoAPIExampleEventlog = () => { // }, ]; }} + keyField="id_process" + onChange={(v) => { + console.log('GridlerGoAPIExampleEventlog onChange', v); + setValues(v); + }} + selectedRow={selectRow ? parseInt(selectRow, 10) : undefined} uniqueid="gridtest" + values={values} > + + + setSelectRow(e.target.value)} + placeholder="row" + value={selectRow} + w="90px" + /> + setValues(str.map((v) => ({ id_process: String(v) })))} + placeholder="Values" + value={values.map((v) => String(v?.id_process))} + /> + ; + ); };