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))}
+ />
+ ;
+
);
};