Interesting delemma

This commit is contained in:
Hein 2025-10-13 18:05:59 +02:00
parent 04c516f702
commit 24227f2110
5 changed files with 326 additions and 52 deletions

View File

@ -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...<Computer /></>;
return (
<>
Loadings...
<Computer />
</>
);
}
return (
<div
@ -105,10 +120,10 @@ export const GridlerDataGrid = () => {
columns={(renderColumns as Array<GridColumn>) ?? []}
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={
<ActionIcon mr="xs" mt="2px" onClick={(e) => 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%"

View File

@ -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<any> = 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<number> = 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 <></>;

View File

@ -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<any>(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<any>) {
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]) {

View File

@ -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<number>;
columns?: GridlerColumns;
data?: Array<any>;
defaultSort?: Array<SortOption>;
@ -58,11 +60,13 @@ export interface GridlerProps extends PropsWithChildren {
col?: GridlerColumn,
defaultItems?: Array<any>
) => Array<any>;
glideProps?: Partial<DataEditorProps>
glideProps?: Partial<DataEditorProps>;
headerHeight?: number;
hideMenu?: (id: string) => void;
keyField?: string;
maxConcurrency?: number;
onChange?: (values: Array<Record<string, any>>) => void;
pageSize?: number;
progressiveScroll?: boolean;
RenderCell?: <TRowType extends Record<string, string>>(
@ -77,11 +81,15 @@ export interface GridlerProps extends PropsWithChildren {
selectedRow?: number;
showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
uniqueid: string;
useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>;
values?: Array<Record<string, any>>;
}
export interface GridlerState {
_active_requests?: Array<{ controller: AbortController; page: number }>;
_glideref?: DataEditorRef;
_gridSelection?: GridSelection;
_gridSelectionRows?: GridSelection['rows'];
_loadingList: CompactSelection;
_page_data: Record<number, Array<any>>;
_scrollTimeout?: any | number;
@ -93,7 +101,6 @@ export interface GridlerState {
colSize?: Record<string, number>;
colSort?: Array<SortOption>;
data?: Array<any>;
errors: Array<string>;
focused?: boolean;
@ -145,7 +152,6 @@ export interface GridlerState {
) => Promise<void>;
toCell: <TRowType extends Record<string, string>>(row: any, col: number) => GridCell;
total_rows: number;
useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>;
}
export type GridlerStoreState = GridlerProps & GridlerState;
@ -206,7 +212,6 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
const state = get();
//const firstPage = Math.max(0, Math.floor(selection.y / state.pageSize));
//const lastPage = Math.floor((selection.y + selection.height) / state.pageSize);
//console.log('Gridler:Debug:getCellsForSelection', selection, firstPage, lastPage);
await state.setStateFN('_visibleArea', (cv) => {
//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<GridlerStoreStat
return selection;
});
// for (const pageChunk of chunk(
// range(firstPage, lastPage + 1).filter((i) => !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<GridlerStoreStat
result.push(row);
}
//console.log('Gridler:Debug:getCellsForSelection', selection, result);
return result as CellArray;
};
},
@ -242,6 +237,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return get()[key];
},
hasLocalData: false,
keyField: 'id',
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
const state = get();
const page = pPage < 0 ? 0 : pPage;
@ -477,7 +473,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
},
];
const items = [
...(coldef.disableSort ? []: sortItems),
...(coldef.disableSort ? [] : sortItems),
{
label: `Filter ${coldef?.title ?? coldef?.id}`,
},
@ -649,11 +645,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
uniqueid: getUUID(),
}),
(props) => {
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<GridlerStoreStat
setState('mounted', true);
}, [setState]);
/// logic to apply the selected row.
useEffect(() => {
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,

View File

@ -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<string | undefined>('');
const [values, setValues] = useState<Array<Record<string, any>>>([]);
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}
>
<APIAdaptorGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} />
</Gridler>
<Divider />
<Group>
<TextInput
onChange={(e) => setSelectRow(e.target.value)}
placeholder="row"
value={selectRow}
w="90px"
/>
<TagsInput
data={[]}
onChange={(str) => setValues(str.map((v) => ({ id_process: String(v) })))}
placeholder="Values"
value={values.map((v) => String(v?.id_process))}
/>
;
</Group>
</Stack>
);
};